@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 LicenseKeyStatus = 'active' | 'inactive' | 'unassigned'
|
|
@@ -42,21 +43,48 @@ interface LicenseKeyPayload {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const emptyOptions: LicenseKeyOptions = { customers: [], products: [], orders: [] }
|
|
45
|
-
|
|
46
|
+
function matchesQuery(records: LicenseKeyRecord[], query: string): LicenseKeyRecord[] {
|
|
47
|
+
return records.filter(record => [
|
|
48
|
+
record.key,
|
|
49
|
+
record.template,
|
|
50
|
+
record.status,
|
|
51
|
+
record.customer?.label,
|
|
52
|
+
record.customer?.detail,
|
|
53
|
+
record.product?.label,
|
|
54
|
+
record.order?.label,
|
|
55
|
+
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
records,
|
|
60
|
+
loading,
|
|
61
|
+
loadError,
|
|
62
|
+
search,
|
|
63
|
+
page,
|
|
64
|
+
perPage,
|
|
65
|
+
editorOpen,
|
|
66
|
+
editorMode,
|
|
67
|
+
editing,
|
|
68
|
+
saving,
|
|
69
|
+
editorError,
|
|
70
|
+
deleting,
|
|
71
|
+
deleteBusy,
|
|
72
|
+
deleteError,
|
|
73
|
+
filteredRecords,
|
|
74
|
+
paginatedRecords,
|
|
75
|
+
goToPage,
|
|
76
|
+
handleSearch,
|
|
77
|
+
openCreate,
|
|
78
|
+
openEdit,
|
|
79
|
+
closeEditor,
|
|
80
|
+
requestDelete,
|
|
81
|
+
closeDelete,
|
|
82
|
+
upsert,
|
|
83
|
+
remove,
|
|
84
|
+
clampPage,
|
|
85
|
+
} = useRecordsDashboard<LicenseKeyRecord>({ filter: matchesQuery })
|
|
86
|
+
|
|
46
87
|
const options = state<LicenseKeyOptions>(emptyOptions)
|
|
47
|
-
const loading = state(true)
|
|
48
|
-
const loadError = state('')
|
|
49
|
-
const search = state('')
|
|
50
|
-
const page = state(1)
|
|
51
|
-
const perPage = state(10)
|
|
52
|
-
const editorOpen = state(false)
|
|
53
|
-
const editorMode = state<'create' | 'edit'>('create')
|
|
54
|
-
const editing = state<LicenseKeyRecord | null>(null)
|
|
55
|
-
const saving = state(false)
|
|
56
|
-
const editorError = state('')
|
|
57
|
-
const deleting = state<LicenseKeyRecord | null>(null)
|
|
58
|
-
const deleteBusy = state(false)
|
|
59
|
-
const deleteError = state('')
|
|
60
88
|
|
|
61
89
|
function optionFor(items: RelatedSummary[], id?: number | null): RelatedSummary | null {
|
|
62
90
|
return id ? items.find(item => item.id === id) || null : null
|
|
@@ -78,26 +106,6 @@ function dateTime(value: string): number {
|
|
|
78
106
|
return new Date(value.replace(' ', 'T')).getTime()
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
function filteredRecords(): LicenseKeyRecord[] {
|
|
82
|
-
const query = search().trim().toLowerCase()
|
|
83
|
-
if (!query)
|
|
84
|
-
return records()
|
|
85
|
-
return records().filter(record => [
|
|
86
|
-
record.key,
|
|
87
|
-
record.template,
|
|
88
|
-
record.status,
|
|
89
|
-
record.customer?.label,
|
|
90
|
-
record.customer?.detail,
|
|
91
|
-
record.product?.label,
|
|
92
|
-
record.order?.label,
|
|
93
|
-
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function paginatedRecords(): LicenseKeyRecord[] {
|
|
97
|
-
const start = (page() - 1) * perPage()
|
|
98
|
-
return filteredRecords().slice(start, start + perPage())
|
|
99
|
-
}
|
|
100
|
-
|
|
101
109
|
function metricCards(): Array<{ label: string, value: string, detail: string }> {
|
|
102
110
|
const expiryCutoff = Date.now() + (30 * 24 * 60 * 60 * 1000)
|
|
103
111
|
const expiring = records().filter((record) => {
|
|
@@ -124,9 +132,7 @@ async function loadKeys(): Promise<void> {
|
|
|
124
132
|
throw new TypeError('License key resources were not returned in the expected shape.')
|
|
125
133
|
options.set(optionData)
|
|
126
134
|
records.set(keyData.map(withRelationships))
|
|
127
|
-
|
|
128
|
-
if (page() > lastPage)
|
|
129
|
-
page.set(lastPage)
|
|
135
|
+
clampPage()
|
|
130
136
|
}
|
|
131
137
|
catch (error) {
|
|
132
138
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -137,33 +143,6 @@ async function loadKeys(): Promise<void> {
|
|
|
137
143
|
}
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
function handleSearch(value: string): void {
|
|
141
|
-
search.set(value)
|
|
142
|
-
page.set(1)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function openCreate(): void {
|
|
146
|
-
editing.set(null)
|
|
147
|
-
editorMode.set('create')
|
|
148
|
-
editorError.set('')
|
|
149
|
-
editorOpen.set(true)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function openEdit(record: LicenseKeyRecord): void {
|
|
153
|
-
editing.set(record)
|
|
154
|
-
editorMode.set('edit')
|
|
155
|
-
editorError.set('')
|
|
156
|
-
editorOpen.set(true)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function closeEditor(): void {
|
|
160
|
-
if (saving())
|
|
161
|
-
return
|
|
162
|
-
editorOpen.set(false)
|
|
163
|
-
editing.set(null)
|
|
164
|
-
editorError.set('')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
146
|
function setEditorError(message: string): void {
|
|
168
147
|
editorError.set(message)
|
|
169
148
|
}
|
|
@@ -183,14 +162,7 @@ async function saveKey(payload: LicenseKeyPayload): Promise<void> {
|
|
|
183
162
|
},
|
|
184
163
|
)
|
|
185
164
|
const hydrated = withRelationships(updated)
|
|
186
|
-
|
|
187
|
-
records.set(records().map(record => record.id === current.id ? hydrated : record))
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
const nextRecords = [...records(), hydrated]
|
|
191
|
-
records.set(nextRecords)
|
|
192
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
193
|
-
}
|
|
165
|
+
upsert(hydrated, current)
|
|
194
166
|
editorOpen.set(false)
|
|
195
167
|
editing.set(null)
|
|
196
168
|
pushToast('success', current ? 'License key updated' : 'License key added', { detail: hydrated.key })
|
|
@@ -203,18 +175,6 @@ async function saveKey(payload: LicenseKeyPayload): Promise<void> {
|
|
|
203
175
|
}
|
|
204
176
|
}
|
|
205
177
|
|
|
206
|
-
function requestDelete(record: LicenseKeyRecord): void {
|
|
207
|
-
deleting.set(record)
|
|
208
|
-
deleteError.set('')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function closeDelete(): void {
|
|
212
|
-
if (deleteBusy())
|
|
213
|
-
return
|
|
214
|
-
deleting.set(null)
|
|
215
|
-
deleteError.set('')
|
|
216
|
-
}
|
|
217
|
-
|
|
218
178
|
async function confirmDelete(): Promise<void> {
|
|
219
179
|
const current = deleting()
|
|
220
180
|
if (!current)
|
|
@@ -225,11 +185,9 @@ async function confirmDelete(): Promise<void> {
|
|
|
225
185
|
await dashboardApi(`/api/dashboard/commerce/license-keys/${current.id}`, {
|
|
226
186
|
method: 'DELETE',
|
|
227
187
|
})
|
|
228
|
-
|
|
188
|
+
remove(current)
|
|
229
189
|
deleting.set(null)
|
|
230
|
-
|
|
231
|
-
if (page() > lastPage)
|
|
232
|
-
page.set(lastPage)
|
|
190
|
+
clampPage()
|
|
233
191
|
pushToast('success', 'License key deleted', { detail: current.key })
|
|
234
192
|
}
|
|
235
193
|
catch (error) {
|
|
@@ -240,10 +198,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
240
198
|
}
|
|
241
199
|
}
|
|
242
200
|
|
|
243
|
-
function goToPage(value: number): void {
|
|
244
|
-
page.set(value)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
201
|
onMount(() => {
|
|
248
202
|
void loadKeys()
|
|
249
203
|
})
|
|
@@ -252,22 +206,16 @@ onMount(() => {
|
|
|
252
206
|
<div class="space-y-6">
|
|
253
207
|
<TabNavigation model-value="license" />
|
|
254
208
|
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<Button :loading="loading()" variant="secondary" @click="loadKeys()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
262
|
-
<Button :disabled="loading()" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add key</Button>
|
|
263
|
-
</div>
|
|
264
|
-
</header>
|
|
209
|
+
<PageHeader title="License keys" description="Generate unique software licenses and manage their commerce assignments.">
|
|
210
|
+
<div class="flex gap-2">
|
|
211
|
+
<Button :loading="loading()" variant="secondary" @click="loadKeys()"><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 key</Button>
|
|
213
|
+
</div>
|
|
214
|
+
</PageHeader>
|
|
265
215
|
|
|
266
216
|
<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="loadKeys()">Retry</Button></div>
|
|
267
217
|
|
|
268
|
-
<
|
|
269
|
-
<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>
|
|
270
|
-
</section>
|
|
218
|
+
<MetricCards label="License key metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
271
219
|
|
|
272
220
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
273
221
|
<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">
|
|
@@ -209,22 +209,16 @@ onMount(() => {
|
|
|
209
209
|
<div class="space-y-6">
|
|
210
210
|
<TabNavigation model-value="methods" />
|
|
211
211
|
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
<Button :loading="loading()" variant="secondary" @click="loadMethods()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
219
|
-
<Button @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add method</Button>
|
|
220
|
-
</div>
|
|
221
|
-
</header>
|
|
212
|
+
<PageHeader title="Shipping methods" description="Manage persisted rates, activation state, and shipping-zone coverage.">
|
|
213
|
+
<div class="flex gap-2">
|
|
214
|
+
<Button :loading="loading()" variant="secondary" @click="loadMethods()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
215
|
+
<Button @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add method</Button>
|
|
216
|
+
</div>
|
|
217
|
+
</PageHeader>
|
|
222
218
|
|
|
223
219
|
<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>
|
|
224
220
|
|
|
225
|
-
<
|
|
226
|
-
<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>
|
|
227
|
-
</section>
|
|
221
|
+
<MetricCards label="Shipping method metrics" columns="grid-cols-1 sm:grid-cols-3" :items="metricCards()" />
|
|
228
222
|
|
|
229
223
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
230
224
|
<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">
|
|
@@ -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
|
import { formatCurrency, minorToInput } from '@stacksjs/commerce/money'
|
|
5
6
|
|
|
@@ -28,22 +29,47 @@ interface ShippingRatePayload {
|
|
|
28
29
|
rate: number
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
function matchesQuery(records: ShippingRateRecord[], query: string): ShippingRateRecord[] {
|
|
33
|
+
return records.filter(record => [
|
|
34
|
+
record.shipping_method?.name,
|
|
35
|
+
record.shipping_zone?.name,
|
|
36
|
+
record.weight_from,
|
|
37
|
+
record.weight_to,
|
|
38
|
+
minorToInput(record.rate, 'USD'),
|
|
39
|
+
].some(value => String(value ?? '').toLowerCase().includes(query)))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
records,
|
|
44
|
+
loading,
|
|
45
|
+
loadError,
|
|
46
|
+
search,
|
|
47
|
+
page,
|
|
48
|
+
perPage,
|
|
49
|
+
editorOpen,
|
|
50
|
+
editorMode,
|
|
51
|
+
editing,
|
|
52
|
+
saving,
|
|
53
|
+
editorError,
|
|
54
|
+
deleting,
|
|
55
|
+
deleteBusy,
|
|
56
|
+
deleteError,
|
|
57
|
+
filteredRecords,
|
|
58
|
+
paginatedRecords,
|
|
59
|
+
goToPage,
|
|
60
|
+
handleSearch,
|
|
61
|
+
openCreate,
|
|
62
|
+
openEdit,
|
|
63
|
+
closeEditor,
|
|
64
|
+
requestDelete,
|
|
65
|
+
closeDelete,
|
|
66
|
+
upsert,
|
|
67
|
+
remove,
|
|
68
|
+
clampPage,
|
|
69
|
+
} = useRecordsDashboard<ShippingRateRecord>({ filter: matchesQuery })
|
|
70
|
+
|
|
32
71
|
const methods = state<ShippingResourceOption[]>([])
|
|
33
72
|
const zones = state<ShippingResourceOption[]>([])
|
|
34
|
-
const loading = state(true)
|
|
35
|
-
const loadError = state('')
|
|
36
|
-
const search = state('')
|
|
37
|
-
const page = state(1)
|
|
38
|
-
const perPage = state(10)
|
|
39
|
-
const editorOpen = state(false)
|
|
40
|
-
const editorMode = state<'create' | 'edit'>('create')
|
|
41
|
-
const editing = state<ShippingRateRecord | null>(null)
|
|
42
|
-
const saving = state(false)
|
|
43
|
-
const editorError = state('')
|
|
44
|
-
const deleting = state<ShippingRateRecord | null>(null)
|
|
45
|
-
const deleteBusy = state(false)
|
|
46
|
-
const deleteError = state('')
|
|
47
73
|
|
|
48
74
|
function methodFor(id: number): ShippingResourceOption | undefined {
|
|
49
75
|
return methods().find(method => method.id === id)
|
|
@@ -61,24 +87,6 @@ function withRelations(record: ShippingRateRecord): ShippingRateRecord {
|
|
|
61
87
|
}
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
function filteredRecords(): ShippingRateRecord[] {
|
|
65
|
-
const query = search().trim().toLowerCase()
|
|
66
|
-
if (!query)
|
|
67
|
-
return records()
|
|
68
|
-
return records().filter(record => [
|
|
69
|
-
record.shipping_method?.name,
|
|
70
|
-
record.shipping_zone?.name,
|
|
71
|
-
record.weight_from,
|
|
72
|
-
record.weight_to,
|
|
73
|
-
minorToInput(record.rate, 'USD'),
|
|
74
|
-
].some(value => String(value ?? '').toLowerCase().includes(query)))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function paginatedRecords(): ShippingRateRecord[] {
|
|
78
|
-
const start = (page() - 1) * perPage()
|
|
79
|
-
return filteredRecords().slice(start, start + perPage())
|
|
80
|
-
}
|
|
81
|
-
|
|
82
90
|
function uniqueZoneCount(): number {
|
|
83
91
|
return new Set(records().map(record => record.shipping_zone_id)).size
|
|
84
92
|
}
|
|
@@ -114,9 +122,7 @@ async function loadRates(): Promise<void> {
|
|
|
114
122
|
methods.set(methodData)
|
|
115
123
|
zones.set(zoneData)
|
|
116
124
|
records.set(rateData.map(withRelations))
|
|
117
|
-
|
|
118
|
-
if (page() > lastPage)
|
|
119
|
-
page.set(lastPage)
|
|
125
|
+
clampPage()
|
|
120
126
|
}
|
|
121
127
|
catch (error) {
|
|
122
128
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -127,33 +133,6 @@ async function loadRates(): Promise<void> {
|
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
function handleSearch(value: string): void {
|
|
131
|
-
search.set(value)
|
|
132
|
-
page.set(1)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function openCreate(): void {
|
|
136
|
-
editing.set(null)
|
|
137
|
-
editorMode.set('create')
|
|
138
|
-
editorError.set('')
|
|
139
|
-
editorOpen.set(true)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function openEdit(record: ShippingRateRecord): void {
|
|
143
|
-
editing.set(record)
|
|
144
|
-
editorMode.set('edit')
|
|
145
|
-
editorError.set('')
|
|
146
|
-
editorOpen.set(true)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function closeEditor(): void {
|
|
150
|
-
if (saving())
|
|
151
|
-
return
|
|
152
|
-
editorOpen.set(false)
|
|
153
|
-
editing.set(null)
|
|
154
|
-
editorError.set('')
|
|
155
|
-
}
|
|
156
|
-
|
|
157
136
|
function setEditorError(message: string): void {
|
|
158
137
|
editorError.set(message)
|
|
159
138
|
}
|
|
@@ -173,14 +152,7 @@ async function saveRate(payload: ShippingRatePayload): Promise<void> {
|
|
|
173
152
|
},
|
|
174
153
|
)
|
|
175
154
|
const hydrated = withRelations(updated)
|
|
176
|
-
|
|
177
|
-
records.set(records().map(record => record.id === current.id ? hydrated : record))
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
const nextRecords = [...records(), hydrated]
|
|
181
|
-
records.set(nextRecords)
|
|
182
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
183
|
-
}
|
|
155
|
+
upsert(hydrated, current)
|
|
184
156
|
editorOpen.set(false)
|
|
185
157
|
editing.set(null)
|
|
186
158
|
pushToast('success', current ? 'Shipping rate updated' : 'Shipping rate added', {
|
|
@@ -195,18 +167,6 @@ async function saveRate(payload: ShippingRatePayload): Promise<void> {
|
|
|
195
167
|
}
|
|
196
168
|
}
|
|
197
169
|
|
|
198
|
-
function requestDelete(record: ShippingRateRecord): void {
|
|
199
|
-
deleting.set(record)
|
|
200
|
-
deleteError.set('')
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function closeDelete(): void {
|
|
204
|
-
if (deleteBusy())
|
|
205
|
-
return
|
|
206
|
-
deleting.set(null)
|
|
207
|
-
deleteError.set('')
|
|
208
|
-
}
|
|
209
|
-
|
|
210
170
|
async function confirmDelete(): Promise<void> {
|
|
211
171
|
const current = deleting()
|
|
212
172
|
if (!current)
|
|
@@ -217,11 +177,9 @@ async function confirmDelete(): Promise<void> {
|
|
|
217
177
|
await dashboardApi(`/api/dashboard/commerce/shipping-rates/${current.id}`, {
|
|
218
178
|
method: 'DELETE',
|
|
219
179
|
})
|
|
220
|
-
|
|
180
|
+
remove(current)
|
|
221
181
|
deleting.set(null)
|
|
222
|
-
|
|
223
|
-
if (page() > lastPage)
|
|
224
|
-
page.set(lastPage)
|
|
182
|
+
clampPage()
|
|
225
183
|
pushToast('success', 'Shipping rate deleted', {
|
|
226
184
|
detail: `${current.shipping_method?.name || 'Method'} for ${current.shipping_zone?.name || 'zone'}`,
|
|
227
185
|
})
|
|
@@ -234,10 +192,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
234
192
|
}
|
|
235
193
|
}
|
|
236
194
|
|
|
237
|
-
function goToPage(value: number): void {
|
|
238
|
-
page.set(value)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
195
|
onMount(() => {
|
|
242
196
|
void loadRates()
|
|
243
197
|
})
|
|
@@ -246,23 +200,17 @@ onMount(() => {
|
|
|
246
200
|
<div class="space-y-6">
|
|
247
201
|
<TabNavigation model-value="rates" />
|
|
248
202
|
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
<Button :loading="loading()" variant="secondary" @click="loadRates()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
256
|
-
<Button :disabled="loading() || methods().length === 0 || zones().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add rate</Button>
|
|
257
|
-
</div>
|
|
258
|
-
</header>
|
|
203
|
+
<PageHeader title="Shipping rates" description="Price method and zone combinations across exact weight bands.">
|
|
204
|
+
<div class="flex gap-2">
|
|
205
|
+
<Button :loading="loading()" variant="secondary" @click="loadRates()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
206
|
+
<Button :disabled="loading() || methods().length === 0 || zones().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add rate</Button>
|
|
207
|
+
</div>
|
|
208
|
+
</PageHeader>
|
|
259
209
|
|
|
260
210
|
<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="loadRates()">Retry</Button></div>
|
|
261
211
|
<div :if="!loading() && (!methods().length || !zones().length)" 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 at least one shipping method and one shipping zone before creating a rate.</div>
|
|
262
212
|
|
|
263
|
-
<
|
|
264
|
-
<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>
|
|
265
|
-
</section>
|
|
213
|
+
<MetricCards label="Shipping rate metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
266
214
|
|
|
267
215
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
268
216
|
<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">
|