@stacksjs/defaults 0.72.103 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/ai/AGENTS.md +25 -4
  2. package/ai/README.md +26 -4
  3. package/ai/skills/stacks-actions/SKILL.md +1 -1
  4. package/ai/skills/stacks-ai/SKILL.md +1 -1
  5. package/ai/skills/stacks-alias/SKILL.md +1 -1
  6. package/ai/skills/stacks-analytics/SKILL.md +1 -1
  7. package/ai/skills/stacks-api/SKILL.md +1 -1
  8. package/ai/skills/stacks-arrays/SKILL.md +1 -1
  9. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  10. package/ai/skills/stacks-browse/SKILL.md +1 -1
  11. package/ai/skills/stacks-browser/SKILL.md +1 -1
  12. package/ai/skills/stacks-buddy/SKILL.md +1 -1
  13. package/ai/skills/stacks-build/SKILL.md +79 -5
  14. package/ai/skills/stacks-cache/SKILL.md +1 -1
  15. package/ai/skills/stacks-calendar/SKILL.md +1 -1
  16. package/ai/skills/stacks-chat/SKILL.md +1 -1
  17. package/ai/skills/stacks-cli/SKILL.md +1 -1
  18. package/ai/skills/stacks-cloud/SKILL.md +1 -1
  19. package/ai/skills/stacks-cms/SKILL.md +1 -1
  20. package/ai/skills/stacks-codebase-design/DEEPENING.md +79 -0
  21. package/ai/skills/stacks-codebase-design/DESIGN-IT-TWICE.md +72 -0
  22. package/ai/skills/stacks-codebase-design/SKILL.md +180 -0
  23. package/ai/skills/stacks-collections/SKILL.md +1 -1
  24. package/ai/skills/stacks-commerce/SKILL.md +141 -35
  25. package/ai/skills/stacks-composables/SKILL.md +1 -1
  26. package/ai/skills/stacks-config/SKILL.md +1 -1
  27. package/ai/skills/stacks-configuration/SKILL.md +1 -1
  28. package/ai/skills/stacks-cron/SKILL.md +1 -1
  29. package/ai/skills/stacks-crosswind/SKILL.md +1 -1
  30. package/ai/skills/stacks-database/SKILL.md +1 -1
  31. package/ai/skills/stacks-datetime/SKILL.md +1 -1
  32. package/ai/skills/stacks-dependencies/SKILL.md +1 -1
  33. package/ai/skills/stacks-deploy/SKILL.md +1 -1
  34. package/ai/skills/stacks-desktop/SKILL.md +1 -1
  35. package/ai/skills/stacks-development/SKILL.md +1 -1
  36. package/ai/skills/stacks-dns/SKILL.md +1 -1
  37. package/ai/skills/stacks-docs/SKILL.md +1 -1
  38. package/ai/skills/stacks-domain-modeling/FORMATS.md +123 -0
  39. package/ai/skills/stacks-domain-modeling/SKILL.md +109 -0
  40. package/ai/skills/stacks-enums/SKILL.md +1 -1
  41. package/ai/skills/stacks-error-handling/SKILL.md +1 -1
  42. package/ai/skills/stacks-events/SKILL.md +1 -1
  43. package/ai/skills/stacks-faker/SKILL.md +1 -1
  44. package/ai/skills/stacks-flow/PHASE-BOUNDARIES.md +91 -0
  45. package/ai/skills/stacks-flow/SKILL.md +117 -0
  46. package/ai/skills/stacks-git/SKILL.md +37 -9
  47. package/ai/skills/stacks-grilling/SKILL.md +85 -0
  48. package/ai/skills/stacks-guard/SKILL.md +86 -11
  49. package/ai/skills/stacks-guard/scripts/block-destructive.sh +67 -0
  50. package/ai/skills/stacks-handoff/SKILL.md +70 -0
  51. package/ai/skills/stacks-health/SKILL.md +1 -1
  52. package/ai/skills/stacks-http/SKILL.md +1 -1
  53. package/ai/skills/stacks-i18n/SKILL.md +1 -1
  54. package/ai/skills/stacks-investigate/SKILL.md +234 -106
  55. package/ai/skills/stacks-investigate/scripts/hitl-loop.template.sh +47 -0
  56. package/ai/skills/stacks-jobs/SKILL.md +1 -1
  57. package/ai/skills/stacks-listeners/SKILL.md +1 -1
  58. package/ai/skills/stacks-logging/SKILL.md +1 -1
  59. package/ai/skills/stacks-mail/SKILL.md +1 -1
  60. package/ai/skills/stacks-middleware/SKILL.md +1 -1
  61. package/ai/skills/stacks-migrations/SKILL.md +1 -1
  62. package/ai/skills/stacks-models/SKILL.md +1 -1
  63. package/ai/skills/stacks-new-feature/SKILL.md +65 -5
  64. package/ai/skills/stacks-notifications/SKILL.md +1 -1
  65. package/ai/skills/stacks-objects/SKILL.md +1 -1
  66. package/ai/skills/stacks-office-hours/SKILL.md +16 -2
  67. package/ai/skills/stacks-orm/SKILL.md +1 -1
  68. package/ai/skills/stacks-path/SKILL.md +1 -1
  69. package/ai/skills/stacks-payments/SKILL.md +35 -1
  70. package/ai/skills/stacks-plan-review/SKILL.md +27 -6
  71. package/ai/skills/stacks-plugins/SKILL.md +1 -1
  72. package/ai/skills/stacks-prototype/LOGIC.md +103 -0
  73. package/ai/skills/stacks-prototype/SKILL.md +66 -0
  74. package/ai/skills/stacks-prototype/UI.md +112 -0
  75. package/ai/skills/stacks-push/SKILL.md +1 -1
  76. package/ai/skills/stacks-query-builder/SKILL.md +1 -1
  77. package/ai/skills/stacks-queue/SKILL.md +1 -1
  78. package/ai/skills/stacks-realtime/SKILL.md +1 -1
  79. package/ai/skills/stacks-registry/SKILL.md +1 -1
  80. package/ai/skills/stacks-repl/SKILL.md +1 -1
  81. package/ai/skills/stacks-retro/SKILL.md +121 -75
  82. package/ai/skills/stacks-review/SKILL.md +182 -74
  83. package/ai/skills/stacks-router/SKILL.md +1 -1
  84. package/ai/skills/stacks-routes/SKILL.md +1 -1
  85. package/ai/skills/stacks-scaffolding/SKILL.md +1 -1
  86. package/ai/skills/stacks-scheduler/SKILL.md +1 -1
  87. package/ai/skills/stacks-search-engine/SKILL.md +1 -1
  88. package/ai/skills/stacks-security/SKILL.md +1 -1
  89. package/ai/skills/stacks-security-audit/SKILL.md +1 -1
  90. package/ai/skills/stacks-server/SKILL.md +1 -1
  91. package/ai/skills/stacks-shell/SKILL.md +1 -1
  92. package/ai/skills/stacks-slug/SKILL.md +1 -1
  93. package/ai/skills/stacks-sms/SKILL.md +1 -1
  94. package/ai/skills/stacks-socials/SKILL.md +1 -1
  95. package/ai/skills/stacks-storage/SKILL.md +1 -1
  96. package/ai/skills/stacks-strings/SKILL.md +1 -1
  97. package/ai/skills/stacks-stx/SKILL.md +1 -1
  98. package/ai/skills/stacks-tdd/EXAMPLES.md +136 -0
  99. package/ai/skills/stacks-tdd/SKILL.md +125 -0
  100. package/ai/skills/stacks-testing/SKILL.md +13 -3
  101. package/ai/skills/stacks-tunnel/SKILL.md +1 -1
  102. package/ai/skills/stacks-types/SKILL.md +1 -1
  103. package/ai/skills/stacks-ui/SKILL.md +1 -1
  104. package/ai/skills/stacks-utils/SKILL.md +1 -1
  105. package/ai/skills/stacks-validation/SKILL.md +1 -1
  106. package/ai/skills/stacks-whois/SKILL.md +1 -1
  107. package/ai/skills/stacks-wizard/SKILL.md +127 -0
  108. package/ai/skills/stacks-wizard/scripts/template.sh +208 -0
  109. package/ai/skills/stacks-writing-for-agents/MECHANICS.md +125 -0
  110. package/ai/skills/stacks-writing-for-agents/SKILL.md +218 -0
  111. package/app/Actions/Auth/GenerateTwoFactorSecretAction.ts +12 -2
  112. package/app/Actions/Commerce/Shipping/{DriverDestroyAction.ts → CourierDestroyAction.ts} +6 -6
  113. package/app/Actions/Commerce/Shipping/{DriverIndexAction.ts → CourierIndexAction.ts} +3 -3
  114. package/app/Actions/Commerce/Shipping/CourierPingStoreAction.ts +61 -0
  115. package/app/Actions/Commerce/Shipping/{DriverShowAction.ts → CourierShowAction.ts} +5 -5
  116. package/app/Actions/Commerce/Shipping/{DriverStoreAction.ts → CourierStoreAction.ts} +4 -4
  117. package/app/Actions/Commerce/Shipping/{DriverUpdateAction.ts → CourierUpdateAction.ts} +6 -6
  118. package/app/Actions/Commerce/Shipping/DeliveryRouteStartAction.ts +38 -0
  119. package/app/Actions/Commerce/Shipping/DeliveryStopCompleteAction.ts +40 -0
  120. package/app/Actions/Commerce/Shipping/DeliveryStopFailAction.ts +44 -0
  121. package/app/Actions/Commerce/Shipping/DeliveryStopStartAction.ts +39 -0
  122. package/app/Actions/Commerce/Shipping/courier-session.ts +75 -0
  123. package/app/Actions/Commerce/commerce-action.test.ts +5 -5
  124. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +5 -5
  125. package/app/Actions/Dashboard/Commerce/CourierIndexAction.ts +24 -0
  126. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +7 -7
  127. package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +11 -11
  128. package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +29 -29
  129. package/app/Actions/Dashboard/Commerce/{driver-records.test.ts → courier-records.test.ts} +9 -9
  130. package/app/Actions/Dashboard/Commerce/{driver-records.ts → courier-records.ts} +14 -14
  131. package/app/Actions/Dashboard/Commerce/delivery-route-records.test.ts +13 -13
  132. package/app/Actions/Dashboard/Commerce/delivery-route-records.ts +25 -25
  133. package/app/Models/User.ts +1 -1
  134. package/app/Models/commerce/{Driver.ts → Courier.ts} +7 -7
  135. package/app/Models/commerce/{DriverPing.ts → CourierPing.ts} +7 -7
  136. package/app/Models/commerce/DeliveryRoute.ts +6 -6
  137. package/app/Models/commerce/DeliveryStop.ts +31 -8
  138. package/bootstrap.ts +7 -0
  139. package/functions/commerce/shippings/couriers.ts +19 -0
  140. package/ide/vscode/package.json +1 -1
  141. package/package.json +4 -3
  142. package/resources/components/Dashboard/Commerce/Delivery/{DriverDeleteDialog.stx → CourierDeleteDialog.stx} +5 -5
  143. package/resources/components/Dashboard/Commerce/Delivery/{DriverDialog.stx → CourierDialog.stx} +10 -10
  144. package/resources/components/Dashboard/Commerce/Delivery/{DriversDashboard.stx → CouriersDashboard.stx} +43 -43
  145. package/resources/components/Dashboard/Commerce/Delivery/{DriversTable.stx → CouriersTable.stx} +21 -21
  146. package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +18 -18
  147. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +2 -2
  148. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +22 -22
  149. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +24 -24
  150. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +8 -8
  151. package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +1 -1
  152. package/resources/functions/dashboard/data.ts +1 -1
  153. package/resources/functions/dashboard/sidebar.ts +2 -2
  154. package/routes/dashboard-api.ts +6 -6
  155. package/routes/dashboard.ts +7 -7
  156. package/routes/delivery.ts +24 -0
  157. package/types/defaults.ts +3 -3
  158. package/views/dashboard/.discovered-models.json +18 -18
  159. package/views/dashboard/AUDIT.md +1 -1
  160. package/views/dashboard/commerce/delivery/{drivers.stx → couriers.stx} +2 -2
  161. package/views/dashboard/composables/useChart.ts +16 -2
  162. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +0 -24
  163. package/functions/commerce/shippings/drivers.ts +0 -19
@@ -1,5 +1,5 @@
1
1
  <script client>
2
- interface DriverOption {
2
+ interface CourierOption {
3
3
  id: number
4
4
  name: string
5
5
  vehicle_number: string
@@ -8,8 +8,8 @@ interface DriverOption {
8
8
 
9
9
  interface DeliveryRouteRecord {
10
10
  id: number
11
- driver: string
12
- driver_id?: number | null
11
+ courier: string
12
+ courier_id?: number | null
13
13
  vehicle: string
14
14
  stops: number
15
15
  delivery_time: number
@@ -20,11 +20,11 @@ interface DeliveryRouteRecord {
20
20
  const open = useReactiveProp('open', false)
21
21
  const mode = useReactiveProp('mode', 'create')
22
22
  const record = useReactiveProp('record', null as DeliveryRouteRecord | null)
23
- const drivers = useReactiveProp('drivers', [] as DriverOption[])
23
+ const couriers = useReactiveProp('couriers', [] as CourierOption[])
24
24
  const busy = useReactiveProp('busy', false)
25
25
  const error = useReactiveProp('error', '')
26
26
  const emit = defineEmits()
27
- const driverId = state('')
27
+ const courierId = state('')
28
28
  const stops = state('0')
29
29
  const deliveryTime = state('0')
30
30
  const totalDistance = state('0')
@@ -64,26 +64,26 @@ effect(() => {
64
64
  if (hydrationKey === nextKey)
65
65
  return
66
66
  hydrationKey = nextKey
67
- driverId.set(current?.driver_id ? String(current.driver_id) : '')
67
+ courierId.set(current?.courier_id ? String(current.courier_id) : '')
68
68
  stops.set(String(current?.stops ?? 0))
69
69
  deliveryTime.set(String(current?.delivery_time ?? 0))
70
70
  totalDistance.set(String(current?.total_distance ?? 0))
71
71
  lastActive.set(toDateTimeLocal(current?.last_active))
72
72
  })
73
73
 
74
- function selectedDriver(): DriverOption | undefined {
75
- const id = Number(driverId())
76
- return drivers().find(driver => driver.id === id)
74
+ function selectedCourier(): CourierOption | undefined {
75
+ const id = Number(courierId())
76
+ return couriers().find(courier => courier.id === id)
77
77
  }
78
78
 
79
79
  function submit(): void {
80
- const driver = selectedDriver()
80
+ const courier = selectedCourier()
81
81
  const parsedStops = Number(stops())
82
82
  const parsedDeliveryTime = Number(deliveryTime())
83
83
  const parsedDistance = Number(totalDistance())
84
84
 
85
- if (!driver) {
86
- emit('invalid', 'Select a persisted driver.')
85
+ if (!courier) {
86
+ emit('invalid', 'Select a persisted courier.')
87
87
  return
88
88
  }
89
89
  if (!Number.isInteger(parsedStops) || parsedStops < 0) {
@@ -104,9 +104,9 @@ function submit(): void {
104
104
  }
105
105
 
106
106
  emit('submit', {
107
- driver: driver.name,
108
- driverId: driver.id,
109
- vehicle: driver.vehicle_number,
107
+ courier: courier.name,
108
+ courierId: courier.id,
109
+ vehicle: courier.vehicle_number,
110
110
  stops: parsedStops,
111
111
  deliveryTime: parsedDeliveryTime,
112
112
  totalDistance: parsedDistance,
@@ -118,7 +118,7 @@ function submit(): void {
118
118
  <Modal
119
119
  :isOpen="open()"
120
120
  :title="mode() === 'edit' ? 'Edit delivery route' : 'Add delivery route'"
121
- description="Assign a persisted driver and record the operational route metrics."
121
+ description="Assign a persisted courier and record the operational route metrics."
122
122
  size="lg"
123
123
  @close="emit('close')"
124
124
  >
@@ -127,15 +127,15 @@ function submit(): void {
127
127
 
128
128
  <div class="space-y-4">
129
129
  <label class="block">
130
- <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Driver</span>
131
- <select x-model="driverId" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700">
132
- <option value="">Select a driver</option>
133
- <template :for="driver in drivers()" :key="driver.id"><option :value="driver.id">{{ driver.name }} - {{ driver.vehicle_number }}</option></template>
130
+ <span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Courier</span>
131
+ <select x-model="courierId" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700">
132
+ <option value="">Select a courier</option>
133
+ <template :for="courier in couriers()" :key="courier.id"><option :value="courier.id">{{ courier.name }} - {{ courier.vehicle_number }}</option></template>
134
134
  </select>
135
135
  </label>
136
- <div :if="selectedDriver()" class="flex gap-3 items-center p-3 text-gray-600 text-sm dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-lg dark:border-neutral-700">
136
+ <div :if="selectedCourier()" class="flex gap-3 items-center p-3 text-gray-600 text-sm dark:text-neutral-300 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-lg dark:border-neutral-700">
137
137
  <span class="h-5 w-5 text-blue-600 i-hugeicons-delivery-truck-02"></span>
138
- <span><strong class="text-gray-900 dark:text-white">{{ selectedDriver()?.vehicle_number }}</strong> will be stored as the route vehicle snapshot.</span>
138
+ <span><strong class="text-gray-900 dark:text-white">{{ selectedCourier()?.vehicle_number }}</strong> will be stored as the route vehicle snapshot.</span>
139
139
  </div>
140
140
  <div class="grid gap-4 grid-cols-1 sm:grid-cols-3">
141
141
  <label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Stops</span><input x-model="stops" type="number" required min="0" step="1" inputmode="numeric" class="mt-1 px-3 py-2 w-full tabular-nums text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
@@ -2,7 +2,7 @@
2
2
  import { dashboardApi } from '../../../../../functions/dashboard-api'
3
3
  import { pushToast } from '../../../../../functions/toasts'
4
4
 
5
- interface DriverRecord {
5
+ interface CourierRecord {
6
6
  id: number
7
7
  name: string
8
8
  vehicle_number: string
@@ -11,9 +11,9 @@ interface DriverRecord {
11
11
 
12
12
  interface DeliveryRouteRecord {
13
13
  id: number
14
- driver: string
15
- driver_id?: number | null
16
- driver_record?: DriverRecord
14
+ courier: string
15
+ courier_id?: number | null
16
+ courier_record?: CourierRecord
17
17
  vehicle: string
18
18
  stops: number
19
19
  delivery_time: number
@@ -22,8 +22,8 @@ interface DeliveryRouteRecord {
22
22
  }
23
23
 
24
24
  interface DeliveryRoutePayload {
25
- driver: string
26
- driverId: number
25
+ courier: string
26
+ courierId: number
27
27
  vehicle: string
28
28
  stops: number
29
29
  deliveryTime: number
@@ -32,7 +32,7 @@ interface DeliveryRoutePayload {
32
32
  }
33
33
 
34
34
  const records = state<DeliveryRouteRecord[]>([])
35
- const drivers = state<DriverRecord[]>([])
35
+ const couriers = state<CourierRecord[]>([])
36
36
  const loading = state(true)
37
37
  const loadError = state('')
38
38
  const search = state('')
@@ -47,14 +47,14 @@ const deleting = state<DeliveryRouteRecord | null>(null)
47
47
  const deleteBusy = state(false)
48
48
  const deleteError = state('')
49
49
 
50
- function driverFor(id?: number | null): DriverRecord | undefined {
51
- return id ? drivers().find(driver => driver.id === id) : undefined
50
+ function courierFor(id?: number | null): CourierRecord | undefined {
51
+ return id ? couriers().find(courier => courier.id === id) : undefined
52
52
  }
53
53
 
54
- function withDriver(record: DeliveryRouteRecord): DeliveryRouteRecord {
54
+ function withCourier(record: DeliveryRouteRecord): DeliveryRouteRecord {
55
55
  return {
56
56
  ...record,
57
- driver_record: driverFor(record.driver_id) || record.driver_record,
57
+ courier_record: courierFor(record.courier_id) || record.courier_record,
58
58
  }
59
59
  }
60
60
 
@@ -70,8 +70,8 @@ function filteredRecords(): DeliveryRouteRecord[] {
70
70
  return records()
71
71
  return records().filter(record => [
72
72
  record.id,
73
- record.driver,
74
- record.driver_record?.name,
73
+ record.courier,
74
+ record.courier_record?.name,
75
75
  record.vehicle,
76
76
  record.stops,
77
77
  record.delivery_time,
@@ -90,7 +90,7 @@ function metricCards(): Array<{ label: string, value: string, detail: string }>
90
90
  const activeCutoff = Date.now() - (24 * 60 * 60 * 1000)
91
91
  const activeRoutes = records().filter(record => routeTime(record.last_active) >= activeCutoff).length
92
92
  return [
93
- { label: 'Routes', value: records().length.toLocaleString(), detail: `${records().filter(record => record.driver_id).length} linked to drivers` },
93
+ { label: 'Routes', value: records().length.toLocaleString(), detail: `${records().filter(record => record.courier_id).length} linked to couriers` },
94
94
  { label: 'Stops', value: totalStops.toLocaleString(), detail: 'Across the current route plan' },
95
95
  { label: 'Distance', value: `${totalDistance.toLocaleString()} mi`, detail: 'Planned route distance' },
96
96
  { label: 'Active in 24h', value: activeRoutes.toLocaleString(), detail: 'Based on persisted activity time' },
@@ -101,14 +101,14 @@ async function loadRoutes(): Promise<void> {
101
101
  loading.set(true)
102
102
  loadError.set('')
103
103
  try {
104
- const [routeData, driverData] = await Promise.all([
104
+ const [routeData, courierData] = await Promise.all([
105
105
  dashboardApi<DeliveryRouteRecord[]>('/api/dashboard/commerce/delivery-routes'),
106
- dashboardApi<DriverRecord[]>('/api/dashboard/commerce/drivers'),
106
+ dashboardApi<CourierRecord[]>('/api/dashboard/commerce/couriers'),
107
107
  ])
108
- if (!Array.isArray(routeData) || !Array.isArray(driverData))
108
+ if (!Array.isArray(routeData) || !Array.isArray(courierData))
109
109
  throw new TypeError('Delivery route resources were not returned in the expected shape.')
110
- drivers.set(driverData)
111
- records.set(routeData.map(withDriver))
110
+ couriers.set(courierData)
111
+ records.set(routeData.map(withCourier))
112
112
  const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
113
113
  if (page() > lastPage)
114
114
  page.set(lastPage)
@@ -167,7 +167,7 @@ async function saveRoute(payload: DeliveryRoutePayload): Promise<void> {
167
167
  body: payload,
168
168
  },
169
169
  )
170
- const hydrated = withDriver(updated)
170
+ const hydrated = withCourier(updated)
171
171
  if (current) {
172
172
  records.set(records().map(record => record.id === current.id ? hydrated : record))
173
173
  }
@@ -240,16 +240,16 @@ onMount(() => {
240
240
  <header class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
241
241
  <div>
242
242
  <h1 class="font-bold text-2xl text-gray-900 dark:text-white">Delivery routes</h1>
243
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Assign persisted drivers and track route workload, distance, and activity.</p>
243
+ <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Assign persisted couriers and track route workload, distance, and activity.</p>
244
244
  </div>
245
245
  <div class="flex gap-2">
246
246
  <Button :loading="loading()" variant="secondary" @click="loadRoutes()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
247
- <Button :disabled="loading() || drivers().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add route</Button>
247
+ <Button :disabled="loading() || couriers().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add route</Button>
248
248
  </div>
249
249
  </header>
250
250
 
251
251
  <div :if="loadError()" class="flex gap-4 items-center justify-between p-4 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/30 border border-red-200 rounded-lg dark:border-red-900"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadRoutes()">Retry</Button></div>
252
- <div :if="!loading() && drivers().length === 0" class="p-4 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">Add a driver before creating a delivery route.</div>
252
+ <div :if="!loading() && couriers().length === 0" class="p-4 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/30 border border-amber-200 rounded-lg dark:border-amber-900">Add a courier before creating a delivery route.</div>
253
253
 
254
254
  <section aria-label="Delivery route metrics" class="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
255
255
  <template :for="item in metricCards()" :key="item.label"><article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700"><p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p><p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p></article></template>
@@ -268,5 +268,5 @@ onMount(() => {
268
268
  </section>
269
269
  </div>
270
270
 
271
- <DeliveryRouteDialog :open="editorOpen()" :mode="editorMode()" :record="editing()" :drivers="drivers()" :busy="saving()" :error="editorError()" @submit="saveRoute" @invalid="setEditorError" @close="closeEditor()" />
271
+ <DeliveryRouteDialog :open="editorOpen()" :mode="editorMode()" :record="editing()" :couriers="couriers()" :busy="saving()" :error="editorError()" @submit="saveRoute" @invalid="setEditorError" @close="closeEditor()" />
272
272
  <DeliveryRouteDeleteDialog :open="Boolean(deleting())" :record="deleting()" :busy="deleteBusy()" :error="deleteError()" @confirm="confirmDelete()" @close="closeDelete()" />
@@ -1,9 +1,9 @@
1
1
  <script client>
2
2
  interface DeliveryRouteRow {
3
3
  id: number
4
- driver: string
5
- driver_id?: number | null
6
- driver_record?: { id: number, name: string, vehicle_number: string }
4
+ courier: string
5
+ courier_id?: number | null
6
+ courier_record?: { id: number, name: string, vehicle_number: string }
7
7
  vehicle: string
8
8
  stops: number
9
9
  delivery_time: number
@@ -39,7 +39,7 @@ function formatNumber(value: number): string {
39
39
  <thead class="bg-stone-50/80 dark:bg-neutral-800">
40
40
  <tr>
41
41
  <th scope="col" class="pl-6 pr-3 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Route</th>
42
- <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Driver</th>
42
+ <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Courier</th>
43
43
  <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Vehicle</th>
44
44
  <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Stops</th>
45
45
  <th scope="col" class="px-4 py-3.5 font-semibold text-left text-stone-500 text-xs tracking-wider uppercase dark:text-neutral-400">Time</th>
@@ -57,8 +57,8 @@ function formatNumber(value: number): string {
57
57
  #{{ route.id }}
58
58
  </td>
59
59
  <td class="px-4 py-3.5 font-medium text-sm text-stone-800 whitespace-nowrap dark:text-white">
60
- <span class="block">{{ route.driver_record?.name || route.driver }}</span>
61
- <span class="block font-normal text-stone-400 text-xs dark:text-neutral-500">{{ route.driver_id ? 'Linked driver #' + route.driver_id : 'Legacy snapshot' }}</span>
60
+ <span class="block">{{ route.courier_record?.name || route.courier }}</span>
61
+ <span class="block font-normal text-stone-400 text-xs dark:text-neutral-500">{{ route.courier_id ? 'Linked courier #' + route.courier_id : 'Legacy snapshot' }}</span>
62
62
  </td>
63
63
  <td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
64
64
  {{ route.vehicle }}
@@ -81,7 +81,7 @@ function formatNumber(value: number): string {
81
81
  size="sm"
82
82
  iconOnly
83
83
  @click="emit('edit', route)"
84
- :ariaLabel="'Edit delivery route ' + route.id + ' for ' + route.driver"
84
+ :ariaLabel="'Edit delivery route ' + route.id + ' for ' + route.courier"
85
85
  >
86
86
  <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
87
87
  </Button>
@@ -90,7 +90,7 @@ function formatNumber(value: number): string {
90
90
  size="sm"
91
91
  iconOnly
92
92
  @click="emit('delete', route)"
93
- :ariaLabel="'Delete delivery route ' + route.id + ' for ' + route.driver"
93
+ :ariaLabel="'Delete delivery route ' + route.id + ' for ' + route.courier"
94
94
  >
95
95
  <span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
96
96
  </Button>
@@ -13,7 +13,7 @@ const tabs = [
13
13
  { name: 'Digital Delivery', value: 'digital', href: '/commerce/delivery/digital-delivery' },
14
14
  { name: 'License Keys', value: 'license', href: '/commerce/delivery/license-keys' },
15
15
  { name: 'Delivery Routes', value: 'routes', href: '/commerce/delivery/delivery-routes' },
16
- { name: 'Drivers', value: 'drivers', href: '/commerce/delivery/drivers' },
16
+ { name: 'Couriers', value: 'couriers', href: '/commerce/delivery/couriers' },
17
17
  ]
18
18
  </script>
19
19
  <div ref="pageRoot">
@@ -102,7 +102,7 @@ const MODEL_PATHS: Record<string, string[]> = {
102
102
  ShippingZone: ['app/Models/ShippingZone.ts', 'storage/framework/defaults/app/Models/commerce/ShippingZone.ts'],
103
103
  DeliveryRoute: ['app/Models/DeliveryRoute.ts', 'storage/framework/defaults/app/Models/commerce/DeliveryRoute.ts'],
104
104
  DigitalDelivery: ['app/Models/DigitalDelivery.ts', 'storage/framework/defaults/app/Models/commerce/DigitalDelivery.ts'],
105
- Driver: ['app/Models/Driver.ts', 'storage/framework/defaults/app/Models/commerce/Driver.ts'],
105
+ Courier: ['app/Models/Courier.ts', 'storage/framework/defaults/app/Models/commerce/Courier.ts'],
106
106
  TaxRate: ['app/Models/TaxRate.ts', 'storage/framework/defaults/app/Models/commerce/TaxRate.ts'],
107
107
  WaitlistProduct: ['app/Models/WaitlistProduct.ts', 'storage/framework/defaults/app/Models/commerce/WaitlistProduct.ts'],
108
108
  WaitlistRestaurant: ['app/Models/WaitlistRestaurant.ts', 'storage/framework/defaults/app/Models/commerce/WaitlistRestaurant.ts'],
@@ -417,7 +417,7 @@ export function buildNavSections(
417
417
  'manufacturer', 'review', 'coupon', 'gift-card', 'payment', 'category',
418
418
  'cart', 'cart-item', 'order-item', 'tax-rate',
419
419
  'shipping-method', 'shipping-rate', 'shipping-zone',
420
- 'delivery-route', 'driver', 'license-key', 'digital-delivery',
420
+ 'delivery-route', 'courier', 'license-key', 'digital-delivery',
421
421
  'print-device', 'receipt',
422
422
  'waitlist-product', 'waitlist-restaurant',
423
423
  ])),
@@ -431,7 +431,7 @@ export function buildNavSections(
431
431
  { to: '/commerce/delivery/shipping-methods', icon: 'truck', text: 'Shipping Methods' },
432
432
  { to: '/commerce/delivery/shipping-rates', icon: 'percent', text: 'Shipping Rates' },
433
433
  { to: '/commerce/delivery/shipping-zones', icon: 'globe', text: 'Shipping Zones' },
434
- { to: '/commerce/delivery/drivers', icon: 'user', text: 'Drivers' },
434
+ { to: '/commerce/delivery/couriers', icon: 'user', text: 'Couriers' },
435
435
  { to: '/commerce/delivery/delivery-routes', icon: 'globe', text: 'Routes' },
436
436
  { to: '/commerce/delivery/license-keys', icon: 'lock', text: 'License Keys' },
437
437
  { to: '/commerce/delivery/digital-delivery', icon: 'package', text: 'Digital Delivery' },
@@ -351,7 +351,7 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
351
351
  // guard so production totals are never protected by client-side role checks.
352
352
  guard(route.get('/commerce/stats', 'Actions/Dashboard/Commerce/CommerceDashboardAction'))
353
353
 
354
- // Delivery operations overview. Guarded because route and driver data is
354
+ // Delivery operations overview. Guarded because route and courier data is
355
355
  // operational information even though the underlying models expose their
356
356
  // own generated useApi endpoints.
357
357
  guard(route.get('/commerce/delivery', 'Actions/Dashboard/Commerce/CommerceDeliveryAction'))
@@ -383,11 +383,11 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
383
383
  guard(route.patch('/commerce/delivery-routes/{id}', 'Actions/Commerce/Shipping/DeliveryRouteUpdateAction'))
384
384
  guard(route.delete('/commerce/delivery-routes/{id}', 'Actions/Commerce/Shipping/DeliveryRouteDestroyAction'))
385
385
 
386
- guard(route.get('/commerce/drivers', 'Actions/Dashboard/Commerce/DriverIndexAction'))
387
- guard(route.get('/commerce/drivers/{id}', 'Actions/Commerce/Shipping/DriverShowAction'))
388
- guard(route.post('/commerce/drivers', 'Actions/Commerce/Shipping/DriverStoreAction'))
389
- guard(route.patch('/commerce/drivers/{id}', 'Actions/Commerce/Shipping/DriverUpdateAction'))
390
- guard(route.delete('/commerce/drivers/{id}', 'Actions/Commerce/Shipping/DriverDestroyAction'))
386
+ guard(route.get('/commerce/couriers', 'Actions/Dashboard/Commerce/CourierIndexAction'))
387
+ guard(route.get('/commerce/couriers/{id}', 'Actions/Commerce/Shipping/CourierShowAction'))
388
+ guard(route.post('/commerce/couriers', 'Actions/Commerce/Shipping/CourierStoreAction'))
389
+ guard(route.patch('/commerce/couriers/{id}', 'Actions/Commerce/Shipping/CourierUpdateAction'))
390
+ guard(route.delete('/commerce/couriers/{id}', 'Actions/Commerce/Shipping/CourierDestroyAction'))
391
391
 
392
392
  guard(route.get('/commerce/digital-deliveries', 'Actions/Dashboard/Commerce/DigitalDeliveryIndexAction'))
393
393
  guard(route.get('/commerce/digital-deliveries/{id}', 'Actions/Commerce/Shipping/DigitalDeliveryShowAction'))
@@ -439,9 +439,9 @@ route.group({ prefix: '/api/commerce', middleware: 'auth' }, () => {
439
439
  // Shipping (moved from a top-level /shipping group on 2026-05-08).
440
440
  // The frontend composables (defaults/functions/commerce/shippings/*.ts)
441
441
  // all expect these resources under /commerce/*, so the prior /shipping
442
- // group was orphaned — every shipping/driver/license/digital call
442
+ // group was orphaned — every shipping/courier/license/digital call
443
443
  // from the dashboard was 404ing. Resource names match the composable
444
- // file names: shipping-methods/rates/zones, delivery-routes, drivers,
444
+ // file names: shipping-methods/rates/zones, delivery-routes, couriers,
445
445
  // digital-deliveries, license-keys.
446
446
  // ----------------------------------------------------------------
447
447
  route.get('/shipping-methods', 'Actions/Commerce/Shipping/ShippingMethodIndexAction')
@@ -468,11 +468,11 @@ route.group({ prefix: '/api/commerce', middleware: 'auth' }, () => {
468
468
  route.patch('/delivery-routes/{id}', 'Actions/Commerce/Shipping/DeliveryRouteUpdateAction')
469
469
  route.delete('/delivery-routes/{id}', 'Actions/Commerce/Shipping/DeliveryRouteDestroyAction')
470
470
 
471
- route.get('/drivers', 'Actions/Commerce/Shipping/DriverIndexAction')
472
- route.get('/drivers/{id}', 'Actions/Commerce/Shipping/DriverShowAction')
473
- route.post('/drivers', 'Actions/Commerce/Shipping/DriverStoreAction')
474
- route.patch('/drivers/{id}', 'Actions/Commerce/Shipping/DriverUpdateAction')
475
- route.delete('/drivers/{id}', 'Actions/Commerce/Shipping/DriverDestroyAction')
471
+ route.get('/couriers', 'Actions/Commerce/Shipping/CourierIndexAction')
472
+ route.get('/couriers/{id}', 'Actions/Commerce/Shipping/CourierShowAction')
473
+ route.post('/couriers', 'Actions/Commerce/Shipping/CourierStoreAction')
474
+ route.patch('/couriers/{id}', 'Actions/Commerce/Shipping/CourierUpdateAction')
475
+ route.delete('/couriers/{id}', 'Actions/Commerce/Shipping/CourierDestroyAction')
476
476
 
477
477
  // Renamed from `/digital` — frontend composable expects `/digital-deliveries`.
478
478
  route.get('/digital-deliveries', 'Actions/Commerce/Shipping/DigitalDeliveryIndexAction')
@@ -0,0 +1,24 @@
1
+ import { route } from '@stacksjs/router'
2
+
3
+ /**
4
+ * Courier-facing delivery endpoints.
5
+ *
6
+ * The tracking pipeline in `@stacksjs/commerce` has always been able to ingest
7
+ * a position, recompute an ETA and broadcast it - it just had no way in from a
8
+ * device. These are that way in.
9
+ *
10
+ * Every route authorises against the courier the session belongs to, not
11
+ * against an id in the body, so one courier cannot post positions or complete
12
+ * stops as another. `CourierPing` deliberately has no `useApi` surface: the
13
+ * generated CRUD would let a client write the ping row directly and skip the
14
+ * position update, ETA recompute, broadcast and arrival latching that make a
15
+ * ping mean anything.
16
+ */
17
+
18
+ route.post('/delivery/pings', 'Actions/Commerce/Shipping/CourierPingStoreAction')
19
+
20
+ route.post('/delivery/routes/{id}/start', 'Actions/Commerce/Shipping/DeliveryRouteStartAction')
21
+
22
+ route.post('/delivery/stops/{id}/start', 'Actions/Commerce/Shipping/DeliveryStopStartAction')
23
+ route.post('/delivery/stops/{id}/complete', 'Actions/Commerce/Shipping/DeliveryStopCompleteAction')
24
+ route.post('/delivery/stops/{id}/fail', 'Actions/Commerce/Shipping/DeliveryStopFailAction')
package/types/defaults.ts CHANGED
@@ -195,7 +195,7 @@ export interface Reviews {
195
195
 
196
196
  export type NewReview = Omit<Reviews, 'id'>
197
197
 
198
- export interface Drivers {
198
+ export interface Couriers {
199
199
  id: number
200
200
  user_id: number
201
201
  name: string
@@ -207,7 +207,7 @@ export interface Drivers {
207
207
 
208
208
  }
209
209
 
210
- export type NewDriver = Omit<Drivers, 'id'>
210
+ export type NewCourier = Omit<Couriers, 'id'>
211
211
 
212
212
  export interface LicenseKeys {
213
213
  id: number
@@ -242,7 +242,7 @@ export interface LicenseTemplates {
242
242
 
243
243
  export interface DeliveryRoutes {
244
244
  id: number
245
- driver: string
245
+ courier: string
246
246
  vehicle: string
247
247
  stops: number
248
248
  delivery_time: number
@@ -151,6 +151,23 @@
151
151
  "highlight": true
152
152
  }
153
153
  },
154
+ {
155
+ "id": "courier",
156
+ "name": "Courier",
157
+ "icon": "car.fill",
158
+ "hasDedicatedPage": false,
159
+ "category": "commerce",
160
+ "dashboard": {
161
+ "highlight": true
162
+ }
163
+ },
164
+ {
165
+ "id": "courier-ping",
166
+ "name": "CourierPing",
167
+ "icon": "tablecells.fill",
168
+ "hasDedicatedPage": false,
169
+ "category": "commerce"
170
+ },
154
171
  {
155
172
  "id": "customer",
156
173
  "name": "Customer",
@@ -201,23 +218,6 @@
201
218
  "highlight": true
202
219
  }
203
220
  },
204
- {
205
- "id": "driver",
206
- "name": "Driver",
207
- "icon": "car.fill",
208
- "hasDedicatedPage": false,
209
- "category": "commerce",
210
- "dashboard": {
211
- "highlight": true
212
- }
213
- },
214
- {
215
- "id": "driver-ping",
216
- "name": "DriverPing",
217
- "icon": "tablecells.fill",
218
- "hasDedicatedPage": false,
219
- "category": "commerce"
220
- },
221
221
  {
222
222
  "id": "email-idempotency",
223
223
  "name": "EmailIdempotency",
@@ -696,4 +696,4 @@
696
696
  }
697
697
  },
698
698
  "nav": []
699
- }
699
+ }
@@ -19,7 +19,7 @@ described the componentized dashboard.
19
19
  | Direct `fetch()` calls in dashboard views, components, and stores | 0 |
20
20
 
21
21
  The eleven models without `useApi` are internal, relationship-owned, or
22
- provider-owned records: driver pings, order idempotency keys, order items,
22
+ provider-owned records: courier pings, order idempotency keys, order items,
23
23
  errors, jobs, failed jobs, payment methods, payment products, payment
24
24
  transactions, social accounts, and subscriptions. They are not missing generic
25
25
  CRUD surfaces.
@@ -1,10 +1,10 @@
1
1
  @extends('layouts/default')
2
2
 
3
3
  @section('content')
4
- @title('Dashboard - Delivery Drivers')
4
+ @title('Dashboard - Delivery Couriers')
5
5
 
6
6
  <div class="p-4 lg:p-8 min-h-[100dvh] dark:bg-neutral-900">
7
- <DriversDashboard />
7
+ <CouriersDashboard />
8
8
  </div>
9
9
 
10
10
  @endsection
@@ -57,10 +57,15 @@ export interface ChartHandle {
57
57
  update: () => void
58
58
  }
59
59
 
60
+ /** A handle that renders nothing, for the cases where there is no canvas to draw on. */
61
+ function inertHandle(): ChartHandle {
62
+ return { instance: null, destroy: () => {}, update: () => {} }
63
+ }
64
+
60
65
  export function useChart(opts: UseChartOptions): ChartHandle {
61
66
  // SSR-safe — no document means no canvas.
62
67
  if (typeof document === 'undefined')
63
- return { instance: null, destroy: () => {}, update: () => {} }
68
+ return inertHandle()
64
69
 
65
70
  const el = document.getElementById(opts.id)
66
71
  if (!el) {
@@ -69,7 +74,16 @@ export function useChart(opts: UseChartOptions): ChartHandle {
69
74
  // missing chart and can grep the id.
70
75
  // eslint-disable-next-line no-console
71
76
  console.warn(`[useChart] no element with id "${opts.id}". Chart not initialised.`)
72
- return { instance: null, destroy: () => {}, update: () => {} }
77
+ return inertHandle()
78
+ }
79
+
80
+ // Chart draws on a canvas, and an id that resolves to a `<div>` is the same
81
+ // class of page mistake as an id that resolves to nothing: worth the same
82
+ // warning rather than a stack trace from inside the chart library.
83
+ if (!(el instanceof HTMLCanvasElement)) {
84
+ // eslint-disable-next-line no-console
85
+ console.warn(`[useChart] element with id "${opts.id}" is a <${el.tagName.toLowerCase()}>, not a <canvas>. Chart not initialised.`)
86
+ return inertHandle()
73
87
  }
74
88
 
75
89
  const instance = new opts.Chart(el, { type: opts.type, data: opts.data, options: opts.options })
@@ -1,24 +0,0 @@
1
- import { Action } from '@stacksjs/actions'
2
- import { Driver, User } from '@stacksjs/orm'
3
- import { dashboardOperationalError } from '../dashboard-response'
4
- import { indexDriverUsers, normalizeDriverRecord } from './driver-records'
5
-
6
- export default new Action({
7
- name: 'Dashboard Drivers',
8
- description: 'Returns validated drivers and their optional User relationships.',
9
- method: 'GET',
10
- apiResponse: true,
11
-
12
- async handle() {
13
- try {
14
- const drivers = await Driver.orderBy('name', 'asc').limit(500).get()
15
- const userIds = [...new Set(drivers.map(driver => driver.get('user_id')).filter(Boolean))]
16
- const users = userIds.length ? await User.whereIn('id', userIds).get() : []
17
- const usersById = indexDriverUsers(users)
18
- return drivers.map(driver => normalizeDriverRecord(driver, usersById))
19
- }
20
- catch (error) {
21
- return dashboardOperationalError(error, 'Driver records could not be read.', 'DriverIndexAction')
22
- }
23
- },
24
- })
@@ -1,19 +0,0 @@
1
- import type { Drivers, NewDriver } from '../../../types/defaults'
2
- import { createDashboardResource } from './dashboard-resource'
3
-
4
- const resource = createDashboardResource<Drivers, NewDriver>({
5
- path: '/api/dashboard/commerce/drivers',
6
- storageKey: 'drivers',
7
- singular: 'driver',
8
- plural: 'drivers',
9
- })
10
-
11
- export function useDrivers() {
12
- return {
13
- drivers: resource.items,
14
- fetchDrivers: resource.fetchAll,
15
- createDriver: resource.create,
16
- updateDriver: resource.update,
17
- deleteDriver: resource.destroy,
18
- }
19
- }