@stacksjs/defaults 0.75.231 → 0.75.236
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/skills/stacks-dashboard/SKILL.md +2 -2
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-deploy/SKILL.md +3 -2
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-native/CATALOG.md +10 -10
- package/ai/skills/stacks-realtime/SKILL.md +4 -2
- package/ai/skills/stacks-storage/SKILL.md +43 -0
- package/ai/skills/stacks-testing/SKILL.md +38 -10
- package/app/Actions/Monitoring/error-provider.ts +129 -12
- package/app/Models/Error.ts +21 -0
- package/functions/dashboard-form.test.ts +41 -0
- package/functions/dashboard-form.ts +30 -0
- package/functions/dashboard-list.test.ts +228 -0
- package/functions/dashboard-list.ts +257 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/project/storage/framework/types/browser-composables.d.ts +56 -0
- package/resources/components/Dashboard/Auth/AuthCard.stx +29 -0
- package/resources/components/Dashboard/Auth/ForgotPassword.stx +65 -79
- package/resources/components/Dashboard/Auth/Login.stx +73 -82
- package/resources/components/Dashboard/Auth/Register.stx +121 -135
- package/resources/components/Dashboard/Auth/ResetPassword.stx +88 -102
- package/resources/components/Dashboard/Buddy/BuddyAssistant.stx +11 -15
- package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceOverviewDashboard.stx +17 -21
- package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +7 -19
- package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +4 -16
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +7 -18
- package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +13 -24
- package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +7 -18
- package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +52 -105
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +12 -16
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +53 -105
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +53 -105
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +7 -13
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +51 -103
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +52 -104
- package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +10 -27
- package/resources/components/Dashboard/Commerce/PrintLogsDashboard.stx +7 -19
- package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +12 -16
- package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +2 -4
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +12 -16
- package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +2 -4
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +17 -21
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +8 -19
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +8 -19
- package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +11 -22
- package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +8 -19
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +12 -16
- package/resources/components/Dashboard/Realtime/RealtimeDashboard.stx +6 -10
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +6 -10
- package/resources/components/Dashboard/UI/MetricCards.stx +32 -0
- package/resources/components/Dashboard/UI/PageHeader.stx +25 -66
- package/resources/components/Dashboard/UI/Table.stx +79 -6
- package/resources/plugins/preloader.ts +9 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import { dashboardApi } from '../../../../../functions/dashboard-api'
|
|
3
|
+
import { useRecordsDashboard } from '../../../../../functions/dashboard-list'
|
|
3
4
|
import { pushToast } from '../../../../../functions/toasts'
|
|
4
5
|
|
|
5
6
|
type CourierStatus = 'active' | 'on_delivery' | 'on_break'
|
|
@@ -34,21 +35,48 @@ interface CourierPayload {
|
|
|
34
35
|
userId: number | null
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
function matchesQuery(records: CourierRecord[], query: string): CourierRecord[] {
|
|
39
|
+
return records.filter(record => [
|
|
40
|
+
record.name,
|
|
41
|
+
record.phone,
|
|
42
|
+
record.vehicle_number,
|
|
43
|
+
record.license,
|
|
44
|
+
record.status,
|
|
45
|
+
record.user?.name,
|
|
46
|
+
record.user?.email,
|
|
47
|
+
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const {
|
|
51
|
+
records,
|
|
52
|
+
loading,
|
|
53
|
+
loadError,
|
|
54
|
+
search,
|
|
55
|
+
page,
|
|
56
|
+
perPage,
|
|
57
|
+
editorOpen,
|
|
58
|
+
editorMode,
|
|
59
|
+
editing,
|
|
60
|
+
saving,
|
|
61
|
+
editorError,
|
|
62
|
+
deleting,
|
|
63
|
+
deleteBusy,
|
|
64
|
+
deleteError,
|
|
65
|
+
filteredRecords,
|
|
66
|
+
paginatedRecords,
|
|
67
|
+
goToPage,
|
|
68
|
+
handleSearch,
|
|
69
|
+
openCreate,
|
|
70
|
+
openEdit,
|
|
71
|
+
closeEditor,
|
|
72
|
+
requestDelete,
|
|
73
|
+
closeDelete,
|
|
74
|
+
upsert,
|
|
75
|
+
remove,
|
|
76
|
+
clampPage,
|
|
77
|
+
} = useRecordsDashboard<CourierRecord>({ filter: matchesQuery })
|
|
78
|
+
|
|
38
79
|
const users = state<UserOption[]>([])
|
|
39
|
-
const loading = state(true)
|
|
40
|
-
const loadError = state('')
|
|
41
|
-
const search = state('')
|
|
42
|
-
const page = state(1)
|
|
43
|
-
const perPage = state(10)
|
|
44
|
-
const editorOpen = state(false)
|
|
45
|
-
const editorMode = state<'create' | 'edit'>('create')
|
|
46
|
-
const editing = state<CourierRecord | null>(null)
|
|
47
|
-
const saving = state(false)
|
|
48
|
-
const editorError = state('')
|
|
49
|
-
const deleting = state<CourierRecord | null>(null)
|
|
50
|
-
const deleteBusy = state(false)
|
|
51
|
-
const deleteError = state('')
|
|
52
80
|
|
|
53
81
|
function userFor(id?: number | null): UserOption | undefined {
|
|
54
82
|
return id ? users().find(user => user.id === id) : undefined
|
|
@@ -61,26 +89,6 @@ function withUser(record: CourierRecord): CourierRecord {
|
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
|
|
64
|
-
function filteredRecords(): CourierRecord[] {
|
|
65
|
-
const query = search().trim().toLowerCase()
|
|
66
|
-
if (!query)
|
|
67
|
-
return records()
|
|
68
|
-
return records().filter(record => [
|
|
69
|
-
record.name,
|
|
70
|
-
record.phone,
|
|
71
|
-
record.vehicle_number,
|
|
72
|
-
record.license,
|
|
73
|
-
record.status,
|
|
74
|
-
record.user?.name,
|
|
75
|
-
record.user?.email,
|
|
76
|
-
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function paginatedRecords(): CourierRecord[] {
|
|
80
|
-
const start = (page() - 1) * perPage()
|
|
81
|
-
return filteredRecords().slice(start, start + perPage())
|
|
82
|
-
}
|
|
83
|
-
|
|
84
92
|
function statusCount(status: CourierStatus): number {
|
|
85
93
|
return records().filter(record => record.status === status).length
|
|
86
94
|
}
|
|
@@ -106,9 +114,7 @@ async function loadCouriers(): Promise<void> {
|
|
|
106
114
|
throw new TypeError('Courier resources were not returned in the expected shape.')
|
|
107
115
|
users.set(userData.users)
|
|
108
116
|
records.set(courierData.map(withUser))
|
|
109
|
-
|
|
110
|
-
if (page() > lastPage)
|
|
111
|
-
page.set(lastPage)
|
|
117
|
+
clampPage()
|
|
112
118
|
}
|
|
113
119
|
catch (error) {
|
|
114
120
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -119,33 +125,6 @@ async function loadCouriers(): Promise<void> {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
function handleSearch(value: string): void {
|
|
123
|
-
search.set(value)
|
|
124
|
-
page.set(1)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function openCreate(): void {
|
|
128
|
-
editing.set(null)
|
|
129
|
-
editorMode.set('create')
|
|
130
|
-
editorError.set('')
|
|
131
|
-
editorOpen.set(true)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function openEdit(record: CourierRecord): void {
|
|
135
|
-
editing.set(record)
|
|
136
|
-
editorMode.set('edit')
|
|
137
|
-
editorError.set('')
|
|
138
|
-
editorOpen.set(true)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function closeEditor(): void {
|
|
142
|
-
if (saving())
|
|
143
|
-
return
|
|
144
|
-
editorOpen.set(false)
|
|
145
|
-
editing.set(null)
|
|
146
|
-
editorError.set('')
|
|
147
|
-
}
|
|
148
|
-
|
|
149
128
|
function setEditorError(message: string): void {
|
|
150
129
|
editorError.set(message)
|
|
151
130
|
}
|
|
@@ -165,14 +144,7 @@ async function saveCourier(payload: CourierPayload): Promise<void> {
|
|
|
165
144
|
},
|
|
166
145
|
)
|
|
167
146
|
const hydrated = withUser(updated)
|
|
168
|
-
|
|
169
|
-
records.set(records().map(record => record.id === current.id ? hydrated : record))
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
const nextRecords = [...records(), hydrated]
|
|
173
|
-
records.set(nextRecords)
|
|
174
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
175
|
-
}
|
|
147
|
+
upsert(hydrated, current)
|
|
176
148
|
editorOpen.set(false)
|
|
177
149
|
editing.set(null)
|
|
178
150
|
pushToast('success', current ? 'Courier updated' : 'Courier added', { detail: hydrated.name })
|
|
@@ -185,18 +157,6 @@ async function saveCourier(payload: CourierPayload): Promise<void> {
|
|
|
185
157
|
}
|
|
186
158
|
}
|
|
187
159
|
|
|
188
|
-
function requestDelete(record: CourierRecord): void {
|
|
189
|
-
deleting.set(record)
|
|
190
|
-
deleteError.set('')
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function closeDelete(): void {
|
|
194
|
-
if (deleteBusy())
|
|
195
|
-
return
|
|
196
|
-
deleting.set(null)
|
|
197
|
-
deleteError.set('')
|
|
198
|
-
}
|
|
199
|
-
|
|
200
160
|
async function confirmDelete(): Promise<void> {
|
|
201
161
|
const current = deleting()
|
|
202
162
|
if (!current)
|
|
@@ -207,11 +167,8 @@ async function confirmDelete(): Promise<void> {
|
|
|
207
167
|
await dashboardApi(`/api/dashboard/commerce/couriers/${current.id}`, {
|
|
208
168
|
method: 'DELETE',
|
|
209
169
|
})
|
|
210
|
-
|
|
170
|
+
remove(current)
|
|
211
171
|
deleting.set(null)
|
|
212
|
-
const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
|
|
213
|
-
if (page() > lastPage)
|
|
214
|
-
page.set(lastPage)
|
|
215
172
|
pushToast('success', 'Courier deleted', { detail: current.name })
|
|
216
173
|
}
|
|
217
174
|
catch (error) {
|
|
@@ -222,10 +179,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
222
179
|
}
|
|
223
180
|
}
|
|
224
181
|
|
|
225
|
-
function goToPage(value: number): void {
|
|
226
|
-
page.set(value)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
182
|
onMount(() => {
|
|
230
183
|
void loadCouriers()
|
|
231
184
|
})
|
|
@@ -234,22 +187,16 @@ onMount(() => {
|
|
|
234
187
|
<div class="space-y-6">
|
|
235
188
|
<TabNavigation model-value="couriers" />
|
|
236
189
|
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<Button :loading="loading()" variant="secondary" @click="loadCouriers()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
244
|
-
<Button :disabled="loading()" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add courier</Button>
|
|
245
|
-
</div>
|
|
246
|
-
</header>
|
|
190
|
+
<PageHeader title="Delivery couriers" description="Manage courier availability, vehicles, licenses, and optional user accounts.">
|
|
191
|
+
<div class="flex gap-2">
|
|
192
|
+
<Button :loading="loading()" variant="secondary" @click="loadCouriers()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
193
|
+
<Button :disabled="loading()" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add courier</Button>
|
|
194
|
+
</div>
|
|
195
|
+
</PageHeader>
|
|
247
196
|
|
|
248
197
|
<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="loadCouriers()">Retry</Button></div>
|
|
249
198
|
|
|
250
|
-
<
|
|
251
|
-
<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>
|
|
252
|
-
</section>
|
|
199
|
+
<MetricCards label="Courier metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
253
200
|
|
|
254
201
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
255
202
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-4 border-b border-gray-200 dark:border-neutral-700">
|
|
@@ -142,22 +142,18 @@ onMount(() => {
|
|
|
142
142
|
<div class="space-y-6">
|
|
143
143
|
<TabNavigation model-value="overview" />
|
|
144
144
|
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Manage routes
|
|
158
|
-
</Button>
|
|
159
|
-
</div>
|
|
160
|
-
</header>
|
|
145
|
+
<PageHeader title="Delivery" description="Monitor active routes, courier availability, shipping methods, and coverage.">
|
|
146
|
+
<div class="flex gap-2">
|
|
147
|
+
<Button :loading="loading()" variant="secondary" @click="loadOverview()">
|
|
148
|
+
<span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
149
|
+
Refresh
|
|
150
|
+
</Button>
|
|
151
|
+
<Button href="/commerce/delivery/delivery-routes">
|
|
152
|
+
<span aria-hidden="true" class="h-4 w-4 i-hugeicons-route-02"></span>
|
|
153
|
+
Manage routes
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</PageHeader>
|
|
161
157
|
|
|
162
158
|
<div :if="loadError()" role="alert" 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">
|
|
163
159
|
<span>{{ loadError() }}</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import { dashboardApi } from '../../../../../functions/dashboard-api'
|
|
3
|
+
import { useRecordsDashboard } from '../../../../../functions/dashboard-list'
|
|
3
4
|
import { pushToast } from '../../../../../functions/toasts'
|
|
4
5
|
|
|
5
6
|
interface CourierRecord {
|
|
@@ -31,21 +32,48 @@ interface DeliveryRoutePayload {
|
|
|
31
32
|
lastActive: number
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
function matchesQuery(records: DeliveryRouteRecord[], query: string): DeliveryRouteRecord[] {
|
|
36
|
+
return records.filter(record => [
|
|
37
|
+
record.id,
|
|
38
|
+
record.courier,
|
|
39
|
+
record.courier_record?.name,
|
|
40
|
+
record.vehicle,
|
|
41
|
+
record.stops,
|
|
42
|
+
record.delivery_time,
|
|
43
|
+
record.total_distance,
|
|
44
|
+
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
records,
|
|
49
|
+
loading,
|
|
50
|
+
loadError,
|
|
51
|
+
search,
|
|
52
|
+
page,
|
|
53
|
+
perPage,
|
|
54
|
+
editorOpen,
|
|
55
|
+
editorMode,
|
|
56
|
+
editing,
|
|
57
|
+
saving,
|
|
58
|
+
editorError,
|
|
59
|
+
deleting,
|
|
60
|
+
deleteBusy,
|
|
61
|
+
deleteError,
|
|
62
|
+
filteredRecords,
|
|
63
|
+
paginatedRecords,
|
|
64
|
+
goToPage,
|
|
65
|
+
handleSearch,
|
|
66
|
+
openCreate,
|
|
67
|
+
openEdit,
|
|
68
|
+
closeEditor,
|
|
69
|
+
requestDelete,
|
|
70
|
+
closeDelete,
|
|
71
|
+
upsert,
|
|
72
|
+
remove,
|
|
73
|
+
clampPage,
|
|
74
|
+
} = useRecordsDashboard<DeliveryRouteRecord>({ filter: matchesQuery })
|
|
75
|
+
|
|
35
76
|
const couriers = state<CourierRecord[]>([])
|
|
36
|
-
const loading = state(true)
|
|
37
|
-
const loadError = state('')
|
|
38
|
-
const search = state('')
|
|
39
|
-
const page = state(1)
|
|
40
|
-
const perPage = state(10)
|
|
41
|
-
const editorOpen = state(false)
|
|
42
|
-
const editorMode = state<'create' | 'edit'>('create')
|
|
43
|
-
const editing = state<DeliveryRouteRecord | null>(null)
|
|
44
|
-
const saving = state(false)
|
|
45
|
-
const editorError = state('')
|
|
46
|
-
const deleting = state<DeliveryRouteRecord | null>(null)
|
|
47
|
-
const deleteBusy = state(false)
|
|
48
|
-
const deleteError = state('')
|
|
49
77
|
|
|
50
78
|
function courierFor(id?: number | null): CourierRecord | undefined {
|
|
51
79
|
return id ? couriers().find(courier => courier.id === id) : undefined
|
|
@@ -64,26 +92,6 @@ function routeTime(value: string | number): number {
|
|
|
64
92
|
return new Date(String(value).replace(' ', 'T')).getTime()
|
|
65
93
|
}
|
|
66
94
|
|
|
67
|
-
function filteredRecords(): DeliveryRouteRecord[] {
|
|
68
|
-
const query = search().trim().toLowerCase()
|
|
69
|
-
if (!query)
|
|
70
|
-
return records()
|
|
71
|
-
return records().filter(record => [
|
|
72
|
-
record.id,
|
|
73
|
-
record.courier,
|
|
74
|
-
record.courier_record?.name,
|
|
75
|
-
record.vehicle,
|
|
76
|
-
record.stops,
|
|
77
|
-
record.delivery_time,
|
|
78
|
-
record.total_distance,
|
|
79
|
-
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function paginatedRecords(): DeliveryRouteRecord[] {
|
|
83
|
-
const start = (page() - 1) * perPage()
|
|
84
|
-
return filteredRecords().slice(start, start + perPage())
|
|
85
|
-
}
|
|
86
|
-
|
|
87
95
|
function metricCards(): Array<{ label: string, value: string, detail: string }> {
|
|
88
96
|
const totalStops = records().reduce((sum, record) => sum + record.stops, 0)
|
|
89
97
|
const totalDistance = records().reduce((sum, record) => sum + record.total_distance, 0)
|
|
@@ -109,9 +117,7 @@ async function loadRoutes(): Promise<void> {
|
|
|
109
117
|
throw new TypeError('Delivery route resources were not returned in the expected shape.')
|
|
110
118
|
couriers.set(courierData)
|
|
111
119
|
records.set(routeData.map(withCourier))
|
|
112
|
-
|
|
113
|
-
if (page() > lastPage)
|
|
114
|
-
page.set(lastPage)
|
|
120
|
+
clampPage()
|
|
115
121
|
}
|
|
116
122
|
catch (error) {
|
|
117
123
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -122,33 +128,6 @@ async function loadRoutes(): Promise<void> {
|
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
function handleSearch(value: string): void {
|
|
126
|
-
search.set(value)
|
|
127
|
-
page.set(1)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function openCreate(): void {
|
|
131
|
-
editing.set(null)
|
|
132
|
-
editorMode.set('create')
|
|
133
|
-
editorError.set('')
|
|
134
|
-
editorOpen.set(true)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function openEdit(record: DeliveryRouteRecord): void {
|
|
138
|
-
editing.set(record)
|
|
139
|
-
editorMode.set('edit')
|
|
140
|
-
editorError.set('')
|
|
141
|
-
editorOpen.set(true)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function closeEditor(): void {
|
|
145
|
-
if (saving())
|
|
146
|
-
return
|
|
147
|
-
editorOpen.set(false)
|
|
148
|
-
editing.set(null)
|
|
149
|
-
editorError.set('')
|
|
150
|
-
}
|
|
151
|
-
|
|
152
131
|
function setEditorError(message: string): void {
|
|
153
132
|
editorError.set(message)
|
|
154
133
|
}
|
|
@@ -168,14 +147,7 @@ async function saveRoute(payload: DeliveryRoutePayload): Promise<void> {
|
|
|
168
147
|
},
|
|
169
148
|
)
|
|
170
149
|
const hydrated = withCourier(updated)
|
|
171
|
-
|
|
172
|
-
records.set(records().map(record => record.id === current.id ? hydrated : record))
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
const nextRecords = [...records(), hydrated]
|
|
176
|
-
records.set(nextRecords)
|
|
177
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
178
|
-
}
|
|
150
|
+
upsert(hydrated, current)
|
|
179
151
|
editorOpen.set(false)
|
|
180
152
|
editing.set(null)
|
|
181
153
|
pushToast('success', current ? 'Delivery route updated' : 'Delivery route added', { detail: `Route #${hydrated.id}` })
|
|
@@ -188,18 +160,6 @@ async function saveRoute(payload: DeliveryRoutePayload): Promise<void> {
|
|
|
188
160
|
}
|
|
189
161
|
}
|
|
190
162
|
|
|
191
|
-
function requestDelete(record: DeliveryRouteRecord): void {
|
|
192
|
-
deleting.set(record)
|
|
193
|
-
deleteError.set('')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function closeDelete(): void {
|
|
197
|
-
if (deleteBusy())
|
|
198
|
-
return
|
|
199
|
-
deleting.set(null)
|
|
200
|
-
deleteError.set('')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
163
|
async function confirmDelete(): Promise<void> {
|
|
204
164
|
const current = deleting()
|
|
205
165
|
if (!current)
|
|
@@ -210,11 +170,9 @@ async function confirmDelete(): Promise<void> {
|
|
|
210
170
|
await dashboardApi(`/api/dashboard/commerce/delivery-routes/${current.id}`, {
|
|
211
171
|
method: 'DELETE',
|
|
212
172
|
})
|
|
213
|
-
|
|
173
|
+
remove(current)
|
|
214
174
|
deleting.set(null)
|
|
215
|
-
|
|
216
|
-
if (page() > lastPage)
|
|
217
|
-
page.set(lastPage)
|
|
175
|
+
clampPage()
|
|
218
176
|
pushToast('success', 'Delivery route deleted', { detail: `Route #${current.id}` })
|
|
219
177
|
}
|
|
220
178
|
catch (error) {
|
|
@@ -225,10 +183,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
225
183
|
}
|
|
226
184
|
}
|
|
227
185
|
|
|
228
|
-
function goToPage(value: number): void {
|
|
229
|
-
page.set(value)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
186
|
onMount(() => {
|
|
233
187
|
void loadRoutes()
|
|
234
188
|
})
|
|
@@ -237,23 +191,17 @@ onMount(() => {
|
|
|
237
191
|
<div class="space-y-6">
|
|
238
192
|
<TabNavigation model-value="routes" />
|
|
239
193
|
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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() || couriers().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add route</Button>
|
|
248
|
-
</div>
|
|
249
|
-
</header>
|
|
194
|
+
<PageHeader title="Delivery routes" description="Assign persisted couriers and track route workload, distance, and activity.">
|
|
195
|
+
<div class="flex gap-2">
|
|
196
|
+
<Button :loading="loading()" variant="secondary" @click="loadRoutes()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
197
|
+
<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>
|
|
198
|
+
</div>
|
|
199
|
+
</PageHeader>
|
|
250
200
|
|
|
251
201
|
<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
202
|
<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
203
|
|
|
254
|
-
<
|
|
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>
|
|
256
|
-
</section>
|
|
204
|
+
<MetricCards label="Delivery route metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
257
205
|
|
|
258
206
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
259
207
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-4 border-b border-gray-200 dark:border-neutral-700">
|
|
@@ -202,22 +202,16 @@ onMount(() => {
|
|
|
202
202
|
<div class="space-y-6">
|
|
203
203
|
<TabNavigation model-value="digital" />
|
|
204
204
|
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
<Button :loading="loading()" variant="secondary" @click="loadMethods()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
212
|
-
<Button :disabled="loading()" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add method</Button>
|
|
213
|
-
</div>
|
|
214
|
-
</header>
|
|
205
|
+
<PageHeader title="Digital delivery" description="Control file access, download limits, expiry, and automatic fulfillment.">
|
|
206
|
+
<div class="flex gap-2">
|
|
207
|
+
<Button :loading="loading()" variant="secondary" @click="loadMethods()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
208
|
+
<Button :disabled="loading()" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add method</Button>
|
|
209
|
+
</div>
|
|
210
|
+
</PageHeader>
|
|
215
211
|
|
|
216
212
|
<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="loadMethods()">Retry</Button></div>
|
|
217
213
|
|
|
218
|
-
<
|
|
219
|
-
<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>
|
|
220
|
-
</section>
|
|
214
|
+
<MetricCards label="Digital delivery metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
221
215
|
|
|
222
216
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
223
217
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between px-4 py-4 border-b border-gray-200 dark:border-neutral-700">
|