@stacksjs/defaults 0.72.102 → 0.73.0
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.
- package/ai/AGENTS.md +25 -4
- package/ai/README.md +26 -4
- package/ai/skills/stacks-actions/SKILL.md +1 -1
- package/ai/skills/stacks-ai/SKILL.md +1 -1
- package/ai/skills/stacks-alias/SKILL.md +1 -1
- package/ai/skills/stacks-analytics/SKILL.md +1 -1
- package/ai/skills/stacks-api/SKILL.md +1 -1
- package/ai/skills/stacks-arrays/SKILL.md +1 -1
- package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-browse/SKILL.md +1 -1
- package/ai/skills/stacks-browser/SKILL.md +1 -1
- package/ai/skills/stacks-buddy/SKILL.md +1 -1
- package/ai/skills/stacks-build/SKILL.md +79 -5
- package/ai/skills/stacks-cache/SKILL.md +1 -1
- package/ai/skills/stacks-calendar/SKILL.md +1 -1
- package/ai/skills/stacks-chat/SKILL.md +1 -1
- package/ai/skills/stacks-cli/SKILL.md +1 -1
- package/ai/skills/stacks-cloud/SKILL.md +1 -1
- package/ai/skills/stacks-cms/SKILL.md +1 -1
- package/ai/skills/stacks-codebase-design/DEEPENING.md +79 -0
- package/ai/skills/stacks-codebase-design/DESIGN-IT-TWICE.md +72 -0
- package/ai/skills/stacks-codebase-design/SKILL.md +180 -0
- package/ai/skills/stacks-collections/SKILL.md +1 -1
- package/ai/skills/stacks-commerce/SKILL.md +141 -35
- package/ai/skills/stacks-composables/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-configuration/SKILL.md +1 -1
- package/ai/skills/stacks-cron/SKILL.md +1 -1
- package/ai/skills/stacks-crosswind/SKILL.md +1 -1
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-datetime/SKILL.md +1 -1
- package/ai/skills/stacks-dependencies/SKILL.md +1 -1
- package/ai/skills/stacks-deploy/SKILL.md +1 -1
- package/ai/skills/stacks-desktop/SKILL.md +1 -1
- package/ai/skills/stacks-development/SKILL.md +1 -1
- package/ai/skills/stacks-dns/SKILL.md +1 -1
- package/ai/skills/stacks-docs/SKILL.md +1 -1
- package/ai/skills/stacks-domain-modeling/FORMATS.md +123 -0
- package/ai/skills/stacks-domain-modeling/SKILL.md +109 -0
- package/ai/skills/stacks-enums/SKILL.md +1 -1
- package/ai/skills/stacks-error-handling/SKILL.md +1 -1
- package/ai/skills/stacks-events/SKILL.md +1 -1
- package/ai/skills/stacks-faker/SKILL.md +1 -1
- package/ai/skills/stacks-flow/PHASE-BOUNDARIES.md +91 -0
- package/ai/skills/stacks-flow/SKILL.md +117 -0
- package/ai/skills/stacks-git/SKILL.md +37 -9
- package/ai/skills/stacks-grilling/SKILL.md +85 -0
- package/ai/skills/stacks-guard/SKILL.md +86 -11
- package/ai/skills/stacks-guard/scripts/block-destructive.sh +67 -0
- package/ai/skills/stacks-handoff/SKILL.md +70 -0
- package/ai/skills/stacks-health/SKILL.md +1 -1
- package/ai/skills/stacks-http/SKILL.md +1 -1
- package/ai/skills/stacks-i18n/SKILL.md +1 -1
- package/ai/skills/stacks-investigate/SKILL.md +234 -106
- package/ai/skills/stacks-investigate/scripts/hitl-loop.template.sh +47 -0
- package/ai/skills/stacks-jobs/SKILL.md +1 -1
- package/ai/skills/stacks-listeners/SKILL.md +1 -1
- package/ai/skills/stacks-logging/SKILL.md +1 -1
- package/ai/skills/stacks-mail/SKILL.md +1 -1
- package/ai/skills/stacks-middleware/SKILL.md +1 -1
- package/ai/skills/stacks-migrations/SKILL.md +1 -1
- package/ai/skills/stacks-models/SKILL.md +1 -1
- package/ai/skills/stacks-new-feature/SKILL.md +65 -5
- package/ai/skills/stacks-notifications/SKILL.md +1 -1
- package/ai/skills/stacks-objects/SKILL.md +1 -1
- package/ai/skills/stacks-office-hours/SKILL.md +16 -2
- package/ai/skills/stacks-orm/SKILL.md +1 -1
- package/ai/skills/stacks-path/SKILL.md +1 -1
- package/ai/skills/stacks-payments/SKILL.md +35 -1
- package/ai/skills/stacks-plan-review/SKILL.md +27 -6
- package/ai/skills/stacks-plugins/SKILL.md +1 -1
- package/ai/skills/stacks-prototype/LOGIC.md +103 -0
- package/ai/skills/stacks-prototype/SKILL.md +66 -0
- package/ai/skills/stacks-prototype/UI.md +112 -0
- package/ai/skills/stacks-push/SKILL.md +1 -1
- package/ai/skills/stacks-query-builder/SKILL.md +1 -1
- package/ai/skills/stacks-queue/SKILL.md +1 -1
- package/ai/skills/stacks-realtime/SKILL.md +1 -1
- package/ai/skills/stacks-registry/SKILL.md +1 -1
- package/ai/skills/stacks-repl/SKILL.md +1 -1
- package/ai/skills/stacks-retro/SKILL.md +121 -75
- package/ai/skills/stacks-review/SKILL.md +182 -74
- package/ai/skills/stacks-router/SKILL.md +1 -1
- package/ai/skills/stacks-routes/SKILL.md +1 -1
- package/ai/skills/stacks-scaffolding/SKILL.md +1 -1
- package/ai/skills/stacks-scheduler/SKILL.md +1 -1
- package/ai/skills/stacks-search-engine/SKILL.md +1 -1
- package/ai/skills/stacks-security/SKILL.md +1 -1
- package/ai/skills/stacks-security-audit/SKILL.md +1 -1
- package/ai/skills/stacks-server/SKILL.md +1 -1
- package/ai/skills/stacks-shell/SKILL.md +1 -1
- package/ai/skills/stacks-slug/SKILL.md +1 -1
- package/ai/skills/stacks-sms/SKILL.md +1 -1
- package/ai/skills/stacks-socials/SKILL.md +1 -1
- package/ai/skills/stacks-storage/SKILL.md +1 -1
- package/ai/skills/stacks-strings/SKILL.md +1 -1
- package/ai/skills/stacks-stx/SKILL.md +1 -1
- package/ai/skills/stacks-tdd/EXAMPLES.md +136 -0
- package/ai/skills/stacks-tdd/SKILL.md +125 -0
- package/ai/skills/stacks-testing/SKILL.md +13 -3
- package/ai/skills/stacks-tunnel/SKILL.md +1 -1
- package/ai/skills/stacks-types/SKILL.md +1 -1
- package/ai/skills/stacks-ui/SKILL.md +1 -1
- package/ai/skills/stacks-utils/SKILL.md +1 -1
- package/ai/skills/stacks-validation/SKILL.md +1 -1
- package/ai/skills/stacks-whois/SKILL.md +1 -1
- package/ai/skills/stacks-wizard/SKILL.md +127 -0
- package/ai/skills/stacks-wizard/scripts/template.sh +208 -0
- package/ai/skills/stacks-writing-for-agents/MECHANICS.md +125 -0
- package/ai/skills/stacks-writing-for-agents/SKILL.md +218 -0
- package/app/Actions/Auth/GenerateTwoFactorSecretAction.ts +12 -2
- package/app/Actions/Commerce/Shipping/{DriverDestroyAction.ts → CourierDestroyAction.ts} +6 -6
- package/app/Actions/Commerce/Shipping/{DriverIndexAction.ts → CourierIndexAction.ts} +3 -3
- package/app/Actions/Commerce/Shipping/CourierPingStoreAction.ts +61 -0
- package/app/Actions/Commerce/Shipping/{DriverShowAction.ts → CourierShowAction.ts} +5 -5
- package/app/Actions/Commerce/Shipping/{DriverStoreAction.ts → CourierStoreAction.ts} +4 -4
- package/app/Actions/Commerce/Shipping/{DriverUpdateAction.ts → CourierUpdateAction.ts} +6 -6
- package/app/Actions/Commerce/Shipping/DeliveryRouteStartAction.ts +38 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopCompleteAction.ts +40 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopFailAction.ts +44 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopStartAction.ts +39 -0
- package/app/Actions/Commerce/Shipping/courier-session.ts +75 -0
- package/app/Actions/Commerce/commerce-action.test.ts +5 -5
- package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +5 -5
- package/app/Actions/Dashboard/Commerce/CourierIndexAction.ts +24 -0
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +7 -7
- package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +11 -11
- package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +29 -29
- package/app/Actions/Dashboard/Commerce/{driver-records.test.ts → courier-records.test.ts} +9 -9
- package/app/Actions/Dashboard/Commerce/{driver-records.ts → courier-records.ts} +14 -14
- package/app/Actions/Dashboard/Commerce/delivery-route-records.test.ts +13 -13
- package/app/Actions/Dashboard/Commerce/delivery-route-records.ts +25 -25
- package/app/Models/User.ts +1 -1
- package/app/Models/commerce/{Driver.ts → Courier.ts} +7 -7
- package/app/Models/commerce/{DriverPing.ts → CourierPing.ts} +7 -7
- package/app/Models/commerce/DeliveryRoute.ts +6 -6
- package/app/Models/commerce/DeliveryStop.ts +31 -8
- package/bootstrap.ts +7 -0
- package/functions/commerce/shippings/couriers.ts +19 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -3
- package/resources/components/Dashboard/Commerce/Delivery/{DriverDeleteDialog.stx → CourierDeleteDialog.stx} +5 -5
- package/resources/components/Dashboard/Commerce/Delivery/{DriverDialog.stx → CourierDialog.stx} +10 -10
- package/resources/components/Dashboard/Commerce/Delivery/{DriversDashboard.stx → CouriersDashboard.stx} +43 -43
- package/resources/components/Dashboard/Commerce/Delivery/{DriversTable.stx → CouriersTable.stx} +21 -21
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +18 -18
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +22 -22
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +24 -24
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +8 -8
- package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +1 -1
- package/resources/functions/dashboard/data.ts +1 -1
- package/resources/functions/dashboard/sidebar.ts +2 -2
- package/routes/dashboard-api.ts +6 -6
- package/routes/dashboard.ts +7 -7
- package/routes/delivery.ts +24 -0
- package/types/defaults.ts +3 -3
- package/views/dashboard/.discovered-models.json +18 -18
- package/views/dashboard/AUDIT.md +1 -1
- package/views/dashboard/commerce/delivery/{drivers.stx → couriers.stx} +2 -2
- package/views/dashboard/composables/useChart.ts +16 -2
- package/views/dashboard/layouts/default.stx +1 -1
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +0 -24
- package/functions/commerce/shippings/drivers.ts +0 -19
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
-
interface
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
75
|
-
const id = Number(
|
|
76
|
-
return
|
|
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
|
|
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 (!
|
|
86
|
-
emit('invalid', 'Select a persisted
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
vehicle:
|
|
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
|
|
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">
|
|
131
|
-
<select x-model="
|
|
132
|
-
<option value="">Select a
|
|
133
|
-
<template :for="
|
|
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="
|
|
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">{{
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
51
|
-
return id ?
|
|
50
|
+
function courierFor(id?: number | null): CourierRecord | undefined {
|
|
51
|
+
return id ? couriers().find(courier => courier.id === id) : undefined
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
54
|
+
function withCourier(record: DeliveryRouteRecord): DeliveryRouteRecord {
|
|
55
55
|
return {
|
|
56
56
|
...record,
|
|
57
|
-
|
|
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.
|
|
74
|
-
record.
|
|
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.
|
|
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,
|
|
104
|
+
const [routeData, courierData] = await Promise.all([
|
|
105
105
|
dashboardApi<DeliveryRouteRecord[]>('/api/dashboard/commerce/delivery-routes'),
|
|
106
|
-
dashboardApi<
|
|
106
|
+
dashboardApi<CourierRecord[]>('/api/dashboard/commerce/couriers'),
|
|
107
107
|
])
|
|
108
|
-
if (!Array.isArray(routeData) || !Array.isArray(
|
|
108
|
+
if (!Array.isArray(routeData) || !Array.isArray(courierData))
|
|
109
109
|
throw new TypeError('Delivery route resources were not returned in the expected shape.')
|
|
110
|
-
|
|
111
|
-
records.set(routeData.map(
|
|
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 =
|
|
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
|
|
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() ||
|
|
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() &&
|
|
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()" :
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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">
|
|
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.
|
|
61
|
-
<span class="block font-normal text-stone-400 text-xs dark:text-neutral-500">{{ route.
|
|
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.
|
|
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.
|
|
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: '
|
|
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
|
-
|
|
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', '
|
|
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/
|
|
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' },
|
package/routes/dashboard-api.ts
CHANGED
|
@@ -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
|
|
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/
|
|
387
|
-
guard(route.get('/commerce/
|
|
388
|
-
guard(route.post('/commerce/
|
|
389
|
-
guard(route.patch('/commerce/
|
|
390
|
-
guard(route.delete('/commerce/
|
|
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'))
|
package/routes/dashboard.ts
CHANGED
|
@@ -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/
|
|
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,
|
|
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('/
|
|
472
|
-
route.get('/
|
|
473
|
-
route.post('/
|
|
474
|
-
route.patch('/
|
|
475
|
-
route.delete('/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/views/dashboard/AUDIT.md
CHANGED
|
@@ -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:
|
|
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
|
|
4
|
+
@title('Dashboard - Delivery Couriers')
|
|
5
5
|
|
|
6
6
|
<div class="p-4 lg:p-8 min-h-[100dvh] dark:bg-neutral-900">
|
|
7
|
-
<
|
|
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
|
|
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
|
|
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 })
|
|
@@ -644,7 +644,7 @@ onDestroy(() => {
|
|
|
644
644
|
<!-- Chrome strip. The traffic lights are the platform's, so this draws
|
|
645
645
|
none and reserves whatever room the host says they need. Craft
|
|
646
646
|
measures its own window: the far edge of the real buttons where they
|
|
647
|
-
float over this sidebar, and nothing where they do not
|
|
647
|
+
float over this sidebar, and nothing where they do not: a window
|
|
648
648
|
that keeps them in a titlebar, a fullscreen one whose titlebar has
|
|
649
649
|
slid away, or a browser, which has no window buttons at all. It is also the window drag
|
|
650
650
|
region, and in a browser it is the same empty strip macOS sidebars
|
|
@@ -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
|
-
}
|