Slice 4 — Service Tickets
The bread-and-butter of a bike shop. Builds the kanban board, today's tickets list, drop-off form, lifecycle transitions, line items with the service menu, customer card hover, and the SMS composer (logging-only until Twilio is wired).
Status: Built. Replaces the mockup with live data backed by 2,506 migrated tickets, 5,057 ticket lines, 7,340 status-history rows, and 81 active service-menu items.
Focus steward: (none on kanban view). Service is not single-search-driven at the screen level — the drop-off form is modal-internal and manages its own customer-search autofocus. See focus stewardship for the rationale on which screens get a steward.
Scope
service_tickets,service_ticket_lines,service_ticket_status_history,service_ticket_messagesservice_categories(the 81-item service menu, migrated from AIMscrepair)- Drop-off form: pick customer → reveals customer's bikes → shows that bike's history
- Kanban board with drag-to-transition
- Today's tickets list (separate view, sorted by status + age)
- Ticket detail with line items, status, history, messages
- Customer card hover (full record visible without navigating away)
- Bike service history (shown in two places: customer profile + ticket detail)
- Text composer (logs to
service_ticket_messages, no Twilio yet) - Cashout flow (creates a
transactionsrow, transitions topicked_up)
Schema
See service ticket entity and service ticket lifecycle.
service_categories from AIM screpair:
| Column | Source |
|---|---|
id | INTEGER PK |
code | screpair.repair_pk |
name | screpair.repair_desc |
default_labour_minutes | derived |
default_price_cents | screpair.repair_price |
is_active | INTEGER (0/1) |
81 active items migrated.
Endpoints
GET /api/tickets/today— today's viewGET /api/tickets/search?q=...— smart search (numeric → ticket # / account #; long → customer/bike/issue)GET /api/tickets/kanban— kanban dataGET /api/tickets/:id— detailPOST /api/tickets— drop-off creationPUT /api/tickets/:id— edit (issue, notes, assigned staff)DELETE /api/tickets/:id— with blockersPOST /api/tickets/:id/lines— add line (labour/parts)PUT /api/tickets/:id/lines/:lineId— edit lineDELETE /api/tickets/:id/lines/:lineId— remove linePOST /api/tickets/:id/status— transitionPOST /api/tickets/:id/messages— log message (Twilio when wired)GET /api/customer-bikes/:id/tickets— bike service historyGET /api/service-menu— list service_categories
UI
Service screen has three views:
- Today — list view, sorted by lifecycle bucket
- Kanban — card-per-ticket, columns by status, drag to transition
- Search — for finding old tickets
Drop-off form:
- Step 1: pick customer (reuses customer search)
- Step 2: pick bike (reveals customer's bikes); inline "+ Add bike"
- Step 3: describe issue (free text + service-menu chips)
- Step 4: optional assigned mechanic
- Submit: creates ticket, returns to kanban with the new ticket highlighted
Ticket detail page:
- Header: ticket number, customer name (hover for customer card), bike, status, dates
- Lines section: labour and parts; in edit mode → drag/×/+
- Messages tab: SMS log + composer
- History tab: status transitions
Customer card hover (anywhere on Service screen):
- Pop-up showing customer's full record without navigating away
- Click "Open" to navigate to the full profile
In edit mode:
- Kanban columns reorderable, headers editable,
?opens column settings - Ticket card field visibility editable
- Ticket detail fields editable inline
What's built
- All endpoints above ✓
- Drop-off → ready → picked-up complete cycle ✓
- Smart search (4-char threshold) ✓
- Customer hover card ✓
- Bike history in two places ✓
- Text composer (logs only, ready for Twilio swap) ✓
- AIM migration: 2,506 tickets + 5,057 lines + 7,340 status-history rows ✓
- Service menu migration: 81 active screpair items ✓
- Audit-event writes on every mutation (drop-off, status transition, line CRUD, edit, delete) ✓
Service Slot redesign + invoice-from-ticket (live 2026-05-22/23)
The Slot is the per-ticket detail panel that opens when a service ticket is selected. Multiple iterations:
- Slot redesigned to match the Till + Bucket look (v0.4.242) — same header pattern, same line-item layout vocabulary, same color tokens. Visual consistency across the three "working-on-a-thing" surfaces (Sales Till, Inventory Bucket, Service Slot).
- Slot header mirrors Sales-Till + cross-source main search (v0.4.243) — the search input at the top of the Slot is now the same multi-source search the Till uses (products + customers + tickets), so mid-ticket "find that part" works without a screen switch.
- Multitool icon (v0.5.9–.11) — wrench emoji replaced with
Multitool.png(64×64). Joins the Till + Bucket header-icon doctrine: dim when empty, full color when populated. - Invoice-from-ticket: Charge button on the Slot (v0.4.241,
POST /api/tickets/:id/invoice) ✓ — closes the ticket → sale loop without leaving the Service screen. Pre-fills a realtransactionsrow from the ticket's lines + customer + bike, opens the Till's cash-tender modal, and on confirm transitions the ticket topicked_upwithtransaction_idset. Wrapped inwithIdempotency. Replaces the older flow of "drag-to-picked-up then go ring the sale separately." - Date-header treatment on the Service ledger (v0.5.13) — the ledger heading now uses the same picker pattern as the Sales ledger (red start / green end), so date-scoping a service-ticket review uses the same muscle memory.
- Service ledger semantics: Outstanding-by-default + Outstanding ↔ Completed toggle (v0.5.14–.18) — the ledger now opens to Outstanding tickets (
status_open=1) instead of the previous date-range default; a per-row Outstanding ↔ Completed toggle flips the view scope. Outstanding tickets render on top; today's pickups render below. The toggle replaces the older "is dropped-off vs has-picked-up-at" derived filter — switched to the explicitstatus_openflag for correctness (v0.5.16). Outstanding-filter fix in v0.5.24 finished the migration. - Status evolution: Ready → Completed + new Notified status (live 2026-05-24, v0.5.17, migration 056) ✓ — the workflow has a step the v1 status set didn't model: after work is complete, the shop CALLS / TEXTS the customer. That's a distinct state from "ready but customer doesn't know yet" — important operationally because the owner can see who hasn't been reached. Old:
dropped_off → in_progress → awaiting_parts → ready → picked_up | cancelled. New:dropped_off → in_progress → awaiting_parts → completed → notified → picked_up | cancelled. Implementation: the code's status string stays'ready'(existing transition-map JS doesn't break); only the display label changes to'Completed'. A new'notified'status row inserts between ready and picked_up (teal #14b8a6, display_order=51). All operations idempotent (INSERT OR IGNORE + UPDATE); existing tickets keep their status_id FK. See service ticket lifecycle. - Slot Generate Ticket flow (live 2026-05-25, v0.6.2/.3) ✓ — clicking the Multitool icon on the Service Slot opens a New Ticket modal; selecting services from a Quick Services pill row + Generate Ticket creates a parked sale (preserves the customer-attached cart for the till). The Charge → invoice-from-ticket flow (v0.4.241) is now triggered from the icon-driven dual-action header instead of a bottom-of-Slot button. Closes the loop: drop-off → service ticket → ringing the till, all from the Service screen.
- Unified work-area header on the Slot (live 2026-05-24, v0.6.12) ✓ — the Slot header carries the same anatomy as the Till and Bucket: line count +
+ Customerpill on the left, Clear×on the right. The icon-driven dual-action doctrine that drove the v0.6.5/.6 header was rolled back in v0.6.11; see work-area headers for the full story. - Service search scoped to service menu + inventory only (live 2026-05-24, v0.6.15 + v0.6.17) ✓ — two passes tightened the Service-tab main search to the things the operator actually stages onto the Slot. v0.6.15 removed customers from the dropdown (the Slot
+ Customerpill owns customer attach; v0.5.20 had already required a customer before adding lines). v0.6.17 removed past tickets from the dropdown too — past-ticket loading is now reachable exclusively from the ledger row click, which calls the sameloadDetailhandler. What the search returns now: service menu (pinned to the top since it's the most-common slot-add), products, bulk items. Doctrine for the Service tab: one-affordance-per-job. Main search = staging stuff to add to the slot. Ledger = Outstanding / Completed tickets.+ Customerpill = attach a customer. The/api/tickets/searchendpoint stays — Cmd-K universal palette still uses it.
Service tab ledger restructure (v0.6.28 → v0.6.35)
A nine-commit run that brought the Service tab into structural parity with the Sales tab and replaced the v0.5.18 Outstanding/Completed toggle with grouped-by-status presentation.
- Grouped by status (v0.6.28) ✓ — the ticket ledger now groups every visible ticket by status, in operator-priority order: 1. In Progress · 2. Awaiting Parts · 3. Dropped Off · 4. Completed · 5. Notified · 6. Picked Up (scoped to the last 14 days; older picked-ups are reachable via the date picker). Cancelled tickets are excluded by default. Each group gets a header row spanning all 5 columns, tinted with the status badge colour so the operator can scan by colour as well as by label (
● STATUS NAME · count). Server:/api/tickets/today's default mode used to return onlyin_progress + awaiting_parts + dropped_off; now it also returnsready + notified + picked_up-within-14-days. The Outstanding/Completed toggle from v0.5.18 became functionally redundant. - Structural mirror of the Sales tab (v0.6.29) ✓ — the Today's Tickets header strip lifted out of its nested card and into a sibling
[Today's tickets header]above the card (matching how Today's Sales lives on the Sales tab). The card itself now holds only the table. The Quick Services pill moved into its own wrapper between the search bar and the ledger header. The Outstanding/Completed toggle button hidden (later fully purged in v0.6.32). Search bar gained a clear-X button matching the Sales pattern. - Services pill back inline +
+ Add serviceinline form (v0.6.31) ✓ — v0.6.29 had moved the Quick Services pill out of the header on the mistaken belief Sales did the same with Bulk Items — Sales doesn't, the bulks pill sits inside the section header. v0.6.31 pulled the Services pill back inline. Quick Services panel also gained a+ Add servicebutton at the header's right (owner / sys_admin only, gated in JS + on the server). Inline form: service name (required) + default price + default labour minutes. Submit callsPOST /api/service-menu(new endpoint), which inserts aservice_categoriesrow with a slugified name (auto-suffixed on UNIQUE collision, same retry pattern as bulk-items). Audit-chained asservice.menu.create. On success the form closes, the Services pill re-fetches via the newhelm:service-menu-changedcross-tab event, and the cross-source slot-search cache is busted so the new item is discoverable from the main service search too. - Ledger heading cleanup + Date column (v0.6.32) ✓ — heading renamed from "Outstanding tickets" (v0.5.18 leftover) to "Today's Tickets"; static text again, not a clickable popover trigger. v0.5.18's toggle JS state (
svcLedgerView), branching insvcUpdateHeading+svcBuildTicketsUrl, click handler, and stub button — all purged (svcUpdateHeadingis now four lines). First column changed fromTicket(monoT-12345numbers) to Date (shortMon DDdrop-off date) — operators care how long a ticket has been in the shop more than the internal number; the number is discoverable on hover and shows on the ticket card after clicking. Sort key changed todropped_off_atdesc. - Date picker filters by drop-off date (v0.6.34) ✓ — the heading is clickable again and opens the date popover (rewired after v0.6.32 unwired it). Pick a date or range → ledger filters to tickets dropped off on those days (not picked-up). Server WHERE clauses on
/api/tickets/todayswitched frompicked_up_attodropped_off_at; modes renamedcompleted_date → dropoff_date,completed_range → dropoff_range(the client accepts both names during the deploy window). Bike-shop mental model is "show me what came in," not "what shipped out." Empty state copy: "No tickets were created on the selected date." Date-mode count: "N tickets created." Today jump button appears next to the heading when a date is picked. - Customer-history mode on attach (v0.6.35) ✓ — symmetry with the Sales tab. Attach a customer to the Slot — either by clicking the
+ Customerpill or by loading an existing ticket from the search /[Today's tickets card]— and the ledger swaps to that customer's full service history. Implementation: Slot fires ahelm:service-customer-attachedevent with the customer's id + display name; the ledger listens, flips its heading to "<Customer> — service history," and re-fetches/api/tickets/today?customer_id=N. Detach (×on the pill, or load a different ticket) and the ledger flips back to the default grouped view (or whatever date filter was set before). Server:/api/tickets/todaynow accepts?customer_id=Nand returns every non-deleted ticket for that customer, newest first. - Inline price edit on Slot lines (v0.6.33) ✓ — clicking any line's price on the Service tab makes it an editable text input (Excel-style cell). Enter / blur commits, Esc cancels. Shared helper
window.helmStartInlinePriceEdit(cellEl, currentCents, onSave)near the top of the body; each surface supplies its ownonSave. Staged lines (pre-commit):rate_centsback-calculated from the new extended; the slot's recompute pass updates totals immediately. Committed (ticket) lines:PUT /api/tickets/:tid/lines/:lidwith the newrate_cents; on success reloads the ticket so server-recomputed totals show. Editable.pricecells get a dotted-underline-on-hover for discoverability; active edit shows a solid blue underline. (Sales-Till lines got the same treatment in the same commit — see slice 5.) - Slice 6 nav restructure (v0.6.0) — Reports tab is folded onto the Owner dashboard tile grid (the Today's Till tile + the Sales-report YoY tile + a new Service-summary tile + a new Outstanding-tickets tile). The standalone Reports nav-tab is gone. Service tab remains as the kanban + Slot surface. This isn't a renaming of the bible's slice 6 (Purchase Orders); it's the
build_info.slicefield bumping to 6 as the next Alpha milestone.
Service Slot redesigned to mirror Sales Till — full overhaul (v0.6.85 → v0.6.208)
A ~125-commit run that re-architected the Service tab end-to-end. The Service Slot now reads as a structural twin of the Sales Till, the lifecycle gained an invoice state between Notified and Picked Up, deposits flow through a dedicated Charge button on the Slot footer, and Sales is the only cashier surface (the Slot stages money but never collects it).
Service Slot structural mirror of the Sales Till (v0.6.85 → v0.6.92)
- Slot mirrors Till anatomy (v0.6.85) ✓ — title row reads
Service · TKT-#### · <status badge>; header subline shows customer name + account / phone / email (the same Sales pattern, sourced fromapiTicketDetail's nestedcustomer_contact.phones[0]/.emails[0]). The actual ticket fields (bike, promised time, mechanic, reported issues, findings, staff notes) lift OUT of the dense old subline and INTO a "Ticket details" block that sits as.till-linerows above the lines area. Empty fields drop out; a truly-empty ticket shows a "click ✎ Edit" hint instead of padded—rows. - Service ledger stays on shop workload; customer history → Slot (v0.6.86) ✓ — reversed v0.6.35's "attach customer → ledger flips to customer history" behaviour. The Service ledger is now always the shop's workload view (Today's Tickets, grouped by status). When a customer is attached, the customer's full service history surfaces inside the Slot via a historic-ticket popup (v0.6.87), not by replacing the ledger. The shop's mental model: the ledger is what work is on the floor; the Slot is what's on the bench right now. Customer-history lookup belongs adjacent to the bench work, not in place of the floor view.
- "Ticket details" title is the edit affordance (v0.6.88) ✓ — replaces the ✎ pencil button (which violated the chassis doctrine — no pencil icons as static buttons anywhere else). The "Ticket details" text in the section header now carries
.helm-clickable+ cursor-pointer + hover treatment; click or Enter / Space opens the edit-ticket modal. - Ticket Details inline-editable, right-justified (v0.6.89) ✓ — bike, promised, mechanic, reported issues, findings, staff notes all editable in place via the
.helm-editablepattern (click → contenteditable → blur or Enter saves → Esc cancels). Right-justified values for visual rhythm with the.till-linepattern on the other surfaces. - Status dropdown in Ticket Details + Completed-notify popup (v0.6.90, v0.6.92) ✓ — current status is now a clickable colour-tinted popover element in the Ticket Details block. Clicking opens a row of status-action buttons that vary by current state (next-status-forward primary, others secondary). When the operator picks "Completed" on a ticket with a customer attached + a phone on file, a Completed-notify popup offers two paths: Send SMS & mark notified (logs the message, sets status=notified) and Just mark complete (no SMS) (sets status=ready). The status popover gets re-rendered as a coloured popover instead of a vanilla
<select>for visual consistency with the surface-colour palette. - Service-Slot Save button wired (v0.6.91) ✓ — the Save button in the slot footer is now functional, posting any pending field-edits in one round-trip (vs. inline-save-per-field). Useful when the operator has typed into multiple fields and wants a single explicit commit point.
Service ledger metrics + shop-workload posture (v0.6.106, v0.6.107)
- Service header metrics: "N open" + "X mechanics on duty" (v0.6.106) ✓ — two muted spans next to the Service page title and the Multitool icon. "N open" counts tickets in truly-open states only (Dropped Off, In Progress, Parts-Hold) — excludes Completed, Notified, Invoice, Picked Up (all in the post-mechanic workflow). "X mechanics on duty" counts active staff with role
mechanicfrom/api/staff, refreshed at boot + every 5 minutes + on every ledger refresh so staff-roster changes from Settings track immediately. Tooltips explain each metric. - N open filter excludes Completed / Notified / Picked Up (v0.6.107) ✓ — earlier the header metric was reading the server's full
outstanding[]array (which the server returns inclusive of Ready / Notified / Picked-Up-within-14-days for ledger grouping). The count now filters client-side to the three truly-open codes only. The server payload stays unchanged so the ledger can render its grouped view with all bands. - Inline edits auto-refresh the ledger (v0.6.106) ✓ — every successful
saveField(bike, promised, mechanic, reported, findings, notes) and bike-picker save now fireshelm:service-ledger-refreshso the ledger row's bike snippet, mechanic initials, and badge data stay in sync with the Slot in real time. Status changes already had this from v0.6.94.
"Parts - Hold" rename + Multitool icon doctrine (v0.6.108, v0.6.116)
- "Awaiting parts" → "Parts - Hold" (v0.6.108, migration 061) ✓ — operator request: the old label read like a passive observation ("we are awaiting parts"). The new label reads as the actionable state it represents on the kanban — there's a ticket on hold because parts haven't landed yet. Code stays
'awaiting_parts'so the status transition allowlist, the client-side transition map, theOPEN_CODESheader-metric filter, and historical ticket data all keep compiling. Migration 061 just UPDATEs thedisplay_name; idempotent. Ticket history with the old display string is unchanged (history rows storestatus_id, the JOIN resolves to the new label). - Multitool icon doctrine unified with the chassis (v0.6.110 → v0.6.116) ✓ — v0.6.110 had inverted the Service icon ("dim when active so the slot stays the focus"); v0.6.116 settled the chassis doctrine: ALL page-title icons are dim when the surface is idle, bright when there's active work on it. Sales (Till) lights up when
till.length > 0OR a customer is attached. Service (Multitool) lights up when a ticket is loaded. Customers (Customers.png) lights up when a customer card is open. Products (Products.png) lights up when the Bucket has staged lines. Same rule across all four tabs; no per-tab inversions. See work-area headers — title-icon doctrine for the full doctrine and history of how the rule settled.
Sales is the cashier surface — no charging from Slot (v0.6.109, v0.6.95, v0.6.193 → v0.6.203)
- Doctrine: no charging from the Service Slot (v0.6.109) ✓ — the cashier surface is Sales. The Slot stages money decisions (taking a drop-off deposit, agreeing on a quote) and writes them to the customer's ticket + account ledger, but the actual ringing-up of cash / card / on-account happens at the Sales Till. Two reasons: (1) cashiers are physically at the till, mechanics are at the bench; the workflow shouldn't conflate the surfaces, (2) the Sales tab already owns tender, split-tender, change-calc, receipt-print, and the cash-drawer kick — duplicating those affordances on the Slot bloats the bench-side UI without earning anything.
- Completed → auto-create Service parked sale (v0.6.95) ✓ — when the operator marks a ticket Completed (either Send-SMS-and-Notified, or Just-Mark-Complete), a Service parked sale is upserted server-side keyed by
ticket_id(deduplicated, so re-marking Completed updates the existing parked-sale row in place rather than creating duplicates). The parked sale appears on the Sales ledger as a yellow "Service" badge (#fef9c3 bg / #854d0e text / #fde047 border— matches the Service tab's ROYGBIV accent). The cashier sees at a glance which parked rows close a service ticket vs. resume an abandoned Sales cart. - New 'invoice' status between Notified and Picked Up (v0.6.193, migration 067) ✓ — James's correction: the "customer is ringing it up" moment was missing from the workflow. Ready / Notified jumped straight to Picked Up, but the parked-sale auto-create needs to fire BEFORE the bike walks out. Migration 067 inserts an
invoicerow (display'Invoice', display_order=55, amber#f59e0b, is_open=1, is_terminal=0) between Notified=51 and Picked Up=60. The workflow becomes:dropped_off → in_progress → parts-hold → ready → notified → invoice → picked_up(Notify is optional; Ready can flow directly to Invoice). The auto-park trigger moves from Picked Up → Invoice (v0.6.177 superseded). Dedup-by-ticket_id keeps re-opens safe. - Invoice status hand-off: switch to Sales tab on click (v0.6.197 → v0.6.199) ✓ — clicking the Invoice action on the Slot transitions the ticket AND switches the operator to the Sales tab so the parked sale (which auto-created server-side per v0.6.193) is immediately in view. v0.6.198 fires the same hand-off from the dropdown path. v0.6.199 also auto-resumes the parked sale into the till so the cashier lands ready to take the payment with no extra clicks.
- Invoiced ≠ finished; Slot locked read-only (v0.6.200) ✓ — invoicing isn't completion; the bike is still in the shop and the customer hasn't picked it up. Three protections: (1) server returns
invoicetickets in the outstanding-mode response so they show in their own band on the ledger; (2)STATUS_GROUP_ORDERslotsinvoicebetween Notified and Picked Up in grouped renders; (3) the Slot goes read-only when status isinvoice—addLineToSlot/slot-qty-dec/slot-qty-inc/beginEdit(ticket-details inline edits) all blocked with ahelmAlertpointing the operator at the till. Status edits stay allowed so the operator can flip to Picked Up or back out via Reopen. An amber banner paints at the top of the Slot: "🔒 Invoiced — read-only on this slot. Lines + details are locked while the parked sale is at the Sales till. Cancel the parked sale on the Sales tab to edit again, or hit Mark Picked Up once the customer takes the bike." Doctrine: invoice is a holding state — money flowing at the till, bike still on the floor. - Cancelling the parked sale reverts the linked ticket's invoice status (v0.6.196) ✓ — completes the round-trip. If the cashier discards or cancels the parked sale on the Sales side, the linked ticket flips back from Invoice to whatever it was before (typically Ready or Notified), the Slot's read-only banner clears, and bench edits become possible again.
- Service workflow lockdown: status gates + Charge rename + deposit cap (v0.6.203) ✓ — three coordinated fixes from James's intake-to-pickup workflow review: (1) Status popover lockdown —
invoiceis always filtered out from the manual dropdown (reachable only via the Charge flow's parked-sale hand-off);picked_upfiltered unless current is in[ready, notified, invoice, picked_up](stops bike-in-progress being marked picked up). (2) Slot footer rename: the+ Depositpill becomesCharge(id unchanged, wiring unchanged). Matches James's mental model: the same button takes any partial pre-payment AND the final pickup payment. Modal title swaps to "Take a payment" when opened from the Slot button; drop-off intake still shows "Deposit at drop-off?". (3) Server cap onPOST /api/tickets/:id/deposit: rejects with a 400 ifamount >= outstanding(oroutstanding <= 0), pointing the operator at the Invoice → Sales-till path. Draft tickets (total = 0) keep the no-cap behaviour for pre-scope deposits.
Deposit-at-drop-off + Slot Charge button (v0.6.194 → v0.6.195)
- Drop-off deposit prompt (v0.6.194) ✓ — the drop-off intake popup gains a Yes/No deposit question. On Yes, an amount + payment-method form appears; submit
POST /api/tickets/:id/depositcreates atransactionsrow +paymentrow + bumpsticket.amount_received_cents+ writes acash_depositentry on the customer's account ledger. This is the canonical "drop-off and pay a $50 deposit so we know you're serious" moment. - + Deposit pill on the Slot footer (v0.6.195, renamed to Charge in v0.6.203) ✓ — a button between Print and Charge for post-drop-off deposits ("you said $50 at drop-off, now you want to add another $200 toward the labour"). Visible once a ticket is loaded with a customer attached; disabled in draft mode. Reuses the v0.6.194 prompt with
skipAsk:true(jumps straight to the amount + payment-method form). Slot auto-refreshes after success. - Service-ticket deposits live broadcast + own label/colour (v0.6.204) ✓ — server
POST /api/tickets/:id/depositnow firesbroadcastTopicInvalidation('parked_sales')so the Sales-ledger badge updates on every other terminal in real time. Deposit-tagged parked sales render with their own subdued tint so the cashier can tell a half-paid ticket from a fully-staged one at a glance.
Drop-off intake polish (v0.6.96 → v0.6.104, v0.6.111, v0.6.208)
- Customer-pick creates the ticket; Slot lands in loaded mode (v0.6.97) ✓ — earlier flow: pick a customer → Slot lands in "draft" mode with no ticket-id yet, lines can't post until the operator clicks Generate Ticket. New flow: customer-pick auto-creates a minimal ticket (status=
dropped_off, customer attached, no bike yet) and lands the Slot in loaded mode. Eliminates the draft / loaded toggle in 95% of cases. - "+ New ticket" on the Slot for the second-bike case (v0.6.98) ✓ — the rare "same customer brings in a second bike on the same visit" case is the one place that needs an explicit Generate Ticket. A
+ New ticketbutton appears in the Slot when a ticket is already loaded for that customer; click creates a second ticket against the same customer attachment. - "Generate Ticket" retired; button only shows as Charge (v0.6.99) ✓ — the explicit Generate Ticket button is gone from the default flow per v0.6.97. The Slot footer's primary button is now just Charge (the v0.6.203 rename).
- Bike field is a picker from customer's bikes on file (v0.6.100) ✓ — instead of a free-text input, the bike field opens a list of the customer's registered bikes (from
customer_bikes) + an "+ Add new bike" option that opens the build-bike modal (the same one from slice 8). One ticket can only point at one bike, so the picker enforces that constraint without the operator typing. - Bike required to create ticket (v0.6.101) ✓ — the picker must resolve to a bike before the ticket can be saved past draft. Eliminates the "service ticket with no associated bike" data-quality problem that plagued AIM.
- × delete in Ticket Details (v0.6.101) ✓ —
×button in the Ticket Details section header deletes the draft ticket (with a confirm prompt). For non-draft tickets, the same×opens the Cancel flow (transitions tocancelledstatus, retains the ticket for history). - × now cancels (status) instead of hard-deleting (v0.6.104) ✓ — refinement of v0.6.101: the
×on a non-draft ticket transitions to cancelled (preserves audit) rather than DELETE (loses history). Hard-delete still works for draft tickets where nothing has been posted yet. - Service reorder prompt after staging a part (v0.6.111) ✓ — adding a part line to a ticket fires a reorder check; if the part is at or below its reorder threshold (or will hit zero after this allocation), a prompt offers to drop the part into the Products bucket for restock. Operator picks "Add to bucket" → reorder row is queued + the ticket is auto-flipped to Parts - Hold status. The bench operator doesn't have to remember to walk to the Products tab.
- Reorder prompt gating, qty stepper, auto-flip (v0.6.151, .182, .184, .186, .187) ✓ — refinements: (1) gate on minimum reorder threshold (v0.6.151); (2) fire on stock-hits-zero too, not just below-minimum (v0.6.182); (3)
-/+stepper on the reorder qty input (v0.6.184); (4) input accepts typing (v0.6.186); (5) Bucket dedup — same variant increments existing pending row instead of stacking duplicates (v0.6.187). - "+ New ticket" → "New Drop Off"; always opens customer picker (v0.6.208) ✓ — final rename + behaviour: the entry-point button reads "New Drop Off" (matches operator vocabulary) and always opens the customer picker first, regardless of slot state. Drop-off is a customer-attached operation; routing through the picker enforces that.
Service slot UX polish (v0.6.152 → v0.6.192)
- Switch-ticket prompt (v0.6.152, v0.6.153, v0.6.157) ✓ — clicking a different ticket in the ledger while the Slot has unsaved work used to silently discard. Now opens a three-button prompt: Save/Close · Discard & Switch · Stay on current ticket. Predicate fires on ANY active slot — staged lines OR draft customer OR loaded ticket. Summary line picks the strongest signal (staged-line count > loaded ticket number > draft customer name). Sales-tab parity:
resumeParkedSale/resumeEstimate/resumeDepositnow route through the same gate, with Save/Close parking the current till before loading the resumed transaction. - Slot qty stepper (v0.6.154) ✓ —
+and−buttons next to each line's qty cell, mirroring the Sales-Till pattern. Click to increment/decrement; dec-to-zero on a staged line removes it from the slot. Auto-POSTs when a ticket is loaded (v0.6.188). - Dec-to-zero removes committed line (v0.6.192) ✓ — refinement:
-on a committed (server-side) ticket line at qty 1 deletes the line from the server (vs. silently bottoming at 1). Mirrors the Bucket dec-to-zero behaviour from v0.6.40.1. - Service slot search restored customer results (v0.6.156) ✓ — v0.6.17 had scoped the search to service menu + inventory + bulks (removing past tickets and customers). v0.6.156 reverses the customer removal — the search now also returns customers, click to attach (the past-ticket result removal stays). Realised the
+ Customerbutton isn't always faster than typing the name into the search box that's already focused. - Sortable column headers on Service ledger (v0.6.178) ✓ — Today's Tickets ledger gains
data-sortable/data-sort-keyon every column header, matching the Sales ledger. - Hide Notified from manual dropdown + drop Save/Close (v0.6.179) ✓ — the manual status dropdown filters out
notifiedbecause the SMS popup is the only legitimate path TO Notified (every Notified ticket must have a corresponding SMS log entry). Slot footer's Save/Close pair drops to just Close (Save was redundant with inline-edit auto-save).
Service status doctrine (v0.6.122, v0.6.169 → v0.6.205)
- End of Day moved from Sales to My Hub (v0.6.122) ✓ — the End of Day cash-out button left the Sales page-actions row. EOD is an Owner-only operation that belongs on the dashboard, not the cashier surface. Also in this commit: the "Owner" nav tab is renamed "My Hub" (data-screen still
dashboard, permission gate stillscreen.owner). - "My Hub" greeting: "Good [period], [first name]" (v0.6.170, v0.6.171) ✓ — the My Hub page title is now a contextual greeting based on the time of day ("Good morning, Tom"). The previous subtitle counts ("3 tickets · 2 estimates pending") were stale and removed.
- Picked-up tickets only show on the day they were picked up (v0.6.202) ✓ — the default Service ledger view used to show picked-up tickets for 14 days after pickup. Now picked-up tickets only show in the default view on the day they were picked up (operators wanted a cleaner view; 14 days of pickups crowded the list). Older picked-ups are still reachable via the date picker.
- Service pill yellow + ticket owing; Sales attach defensive (v0.6.205) ✓ — the Service parked-sale badge gets a deeper yellow when the underlying ticket has an outstanding balance (subtle visual cue for cashiers). Sales-side customer-attach is defensive against the rare race where a ticket is invoiced + the parked-sale ledger row is clicked before the broadcast bridge has synced.
- Slot owing reduction + sortable PO + cust-history ledgers (v0.6.206) ✓ — the Slot's owing-balance display deducts already-paid deposits from the running total. PO ledger + customer-history ledger get the same sortable column treatment as Sales / Service ledgers.
- Resume service deposit pulls ticket lines (v0.6.207) ✓ — when the cashier resumes a Service parked sale where a deposit was taken at drop-off, the Sales-Till now pulls the ticket's committed lines so the till shows the correct balance owing (line total − deposit), not the full line total. Matches the operator's mental model: "they already paid $50; this is what's left."
Service ticket workflow today (post-v0.6.208)
┌─────────────┐
│ Dropped Off │
└──────┬──────┘
▼
┌─────────────┐
│ In Progress │
└──────┬──────┘
▼
┌───────────────────────┐
│ Parts - Hold (waiting │
│ on supplier delivery) │
└──────────┬────────────┘
▼
┌─────────┐
│ Ready │ (display: "Completed")
└────┬────┘
┌────────┴────────┐
▼ (SMS path) ▼ (skip SMS)
┌──────────┐ ┌────────────────┐
│ Notified │ │ Invoice direct │
└─────┬────┘ └────────┬───────┘
▼ ▼
┌──────────┐ ┌────────────────┐
│ Invoice │ ── │ (Slot locked │
│ (parked │ │ read-only; │
│ sale on │ │ bike still in │
│ Sales) │ │ shop) │
└─────┬────┘ └────────────────┘
▼
┌──────────┐
│ Picked │ (terminal — bike out the door)
│ Up │
└──────────┘
Cancelled state available at any point; Reopen path goes back to Ready (from Notified / Invoice / Picked Up) or back to In Progress (from Parts-Hold).
What's not yet built
- Twilio wiring — composer logs locally; sending is one adapter call away
- Quoted state UI (it works on the backend; no UI button yet to send a quote)
- Late-status detection — daily cron reports it, but the Today dashboard card needs to surface it
- Reopen-ticket UI — the transition is allowed; the ↺ button isn't on the picked-up state yet (operators delete-and-recreate today, which is wrong)
Migration from AIM
- 2,506 tickets from
sctic - Issue from
sctic.tic_desc, promised date fromsctic.tic_ptime - Bike linkage via serial number
sctic.tic_serno→customer_bikes.serial_number - Line items 1:1 from
sctic_line
A migration test caught a problem: ~12 tickets had tic_desc=NULL. We backfilled with "(no description on AIM record)" and audit-logged the backfill.
See also
- Service ticket entity
- Service ticket lifecycle
- Customer-bike entity
- Data flow diagrams — flow #2 is this slice