@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 SerializedList = string | string[] | null | undefined
|
|
@@ -30,21 +31,47 @@ interface ShippingZonePayload {
|
|
|
30
31
|
status: 'active' | 'inactive' | 'draft'
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
function matchesQuery(records: ShippingZoneRecord[], query: string): ShippingZoneRecord[] {
|
|
35
|
+
return records.filter(record => [
|
|
36
|
+
record.name,
|
|
37
|
+
record.status,
|
|
38
|
+
record.shipping_method?.name,
|
|
39
|
+
...listTokens(record.countries),
|
|
40
|
+
...listTokens(record.regions),
|
|
41
|
+
...listTokens(record.postal_codes),
|
|
42
|
+
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
records,
|
|
47
|
+
loading,
|
|
48
|
+
loadError,
|
|
49
|
+
search,
|
|
50
|
+
page,
|
|
51
|
+
perPage,
|
|
52
|
+
editorOpen,
|
|
53
|
+
editorMode,
|
|
54
|
+
editing,
|
|
55
|
+
saving,
|
|
56
|
+
editorError,
|
|
57
|
+
deleting,
|
|
58
|
+
deleteBusy,
|
|
59
|
+
deleteError,
|
|
60
|
+
filteredRecords,
|
|
61
|
+
paginatedRecords,
|
|
62
|
+
goToPage,
|
|
63
|
+
handleSearch,
|
|
64
|
+
openCreate,
|
|
65
|
+
openEdit,
|
|
66
|
+
closeEditor,
|
|
67
|
+
requestDelete,
|
|
68
|
+
closeDelete,
|
|
69
|
+
upsert,
|
|
70
|
+
remove,
|
|
71
|
+
clampPage,
|
|
72
|
+
} = useRecordsDashboard<ShippingZoneRecord>({ filter: matchesQuery })
|
|
73
|
+
|
|
34
74
|
const methods = state<ShippingMethodOption[]>([])
|
|
35
|
-
const loading = state(true)
|
|
36
|
-
const loadError = state('')
|
|
37
|
-
const search = state('')
|
|
38
|
-
const page = state(1)
|
|
39
|
-
const perPage = state(10)
|
|
40
|
-
const editorOpen = state(false)
|
|
41
|
-
const editorMode = state<'create' | 'edit'>('create')
|
|
42
|
-
const editing = state<ShippingZoneRecord | null>(null)
|
|
43
|
-
const saving = state(false)
|
|
44
|
-
const editorError = state('')
|
|
45
|
-
const deleting = state<ShippingZoneRecord | null>(null)
|
|
46
|
-
const deleteBusy = state(false)
|
|
47
|
-
const deleteError = state('')
|
|
48
75
|
|
|
49
76
|
function listTokens(value: SerializedList): string[] {
|
|
50
77
|
if (Array.isArray(value))
|
|
@@ -73,25 +100,6 @@ function withMethod(record: ShippingZoneRecord): ShippingZoneRecord {
|
|
|
73
100
|
}
|
|
74
101
|
}
|
|
75
102
|
|
|
76
|
-
function filteredRecords(): ShippingZoneRecord[] {
|
|
77
|
-
const query = search().trim().toLowerCase()
|
|
78
|
-
if (!query)
|
|
79
|
-
return records()
|
|
80
|
-
return records().filter(record => [
|
|
81
|
-
record.name,
|
|
82
|
-
record.status,
|
|
83
|
-
record.shipping_method?.name,
|
|
84
|
-
...listTokens(record.countries),
|
|
85
|
-
...listTokens(record.regions),
|
|
86
|
-
...listTokens(record.postal_codes),
|
|
87
|
-
].some(value => String(value || '').toLowerCase().includes(query)))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function paginatedRecords(): ShippingZoneRecord[] {
|
|
91
|
-
const start = (page() - 1) * perPage()
|
|
92
|
-
return filteredRecords().slice(start, start + perPage())
|
|
93
|
-
}
|
|
94
|
-
|
|
95
103
|
function uniqueCount(field: 'countries' | 'regions'): number {
|
|
96
104
|
return new Set(records().flatMap(record => listTokens(record[field]))).size
|
|
97
105
|
}
|
|
@@ -122,9 +130,7 @@ async function loadZones(): Promise<void> {
|
|
|
122
130
|
throw new TypeError('Shipping zone resources were not returned as arrays.')
|
|
123
131
|
methods.set(methodData)
|
|
124
132
|
records.set(zoneData.map(withMethod))
|
|
125
|
-
|
|
126
|
-
if (page() > lastPage)
|
|
127
|
-
page.set(lastPage)
|
|
133
|
+
clampPage()
|
|
128
134
|
}
|
|
129
135
|
catch (error) {
|
|
130
136
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -135,33 +141,6 @@ async function loadZones(): Promise<void> {
|
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
|
|
138
|
-
function handleSearch(value: string): void {
|
|
139
|
-
search.set(value)
|
|
140
|
-
page.set(1)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function openCreate(): void {
|
|
144
|
-
editing.set(null)
|
|
145
|
-
editorMode.set('create')
|
|
146
|
-
editorError.set('')
|
|
147
|
-
editorOpen.set(true)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function openEdit(record: ShippingZoneRecord): void {
|
|
151
|
-
editing.set(record)
|
|
152
|
-
editorMode.set('edit')
|
|
153
|
-
editorError.set('')
|
|
154
|
-
editorOpen.set(true)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function closeEditor(): void {
|
|
158
|
-
if (saving())
|
|
159
|
-
return
|
|
160
|
-
editorOpen.set(false)
|
|
161
|
-
editing.set(null)
|
|
162
|
-
editorError.set('')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
144
|
function setEditorError(message: string): void {
|
|
166
145
|
editorError.set(message)
|
|
167
146
|
}
|
|
@@ -181,14 +160,7 @@ async function saveZone(payload: ShippingZonePayload): Promise<void> {
|
|
|
181
160
|
},
|
|
182
161
|
)
|
|
183
162
|
const hydrated = withMethod(updated)
|
|
184
|
-
|
|
185
|
-
records.set(records().map(record => record.id === current.id ? hydrated : record))
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
const nextRecords = [...records(), hydrated]
|
|
189
|
-
records.set(nextRecords)
|
|
190
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
191
|
-
}
|
|
163
|
+
upsert(hydrated, current)
|
|
192
164
|
editorOpen.set(false)
|
|
193
165
|
editing.set(null)
|
|
194
166
|
pushToast('success', current ? 'Shipping zone updated' : 'Shipping zone added', {
|
|
@@ -203,18 +175,6 @@ async function saveZone(payload: ShippingZonePayload): Promise<void> {
|
|
|
203
175
|
}
|
|
204
176
|
}
|
|
205
177
|
|
|
206
|
-
function requestDelete(record: ShippingZoneRecord): 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/shipping-zones/${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', 'Shipping zone deleted', { detail: current.name })
|
|
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 loadZones()
|
|
249
203
|
})
|
|
@@ -252,23 +206,17 @@ onMount(() => {
|
|
|
252
206
|
<div class="space-y-6">
|
|
253
207
|
<TabNavigation model-value="zones" />
|
|
254
208
|
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<Button :loading="loading()" variant="secondary" @click="loadZones()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
262
|
-
<Button :disabled="loading() || methods().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add zone</Button>
|
|
263
|
-
</div>
|
|
264
|
-
</header>
|
|
209
|
+
<PageHeader title="Shipping zones" description="Map methods to persisted country, region, and postal coverage.">
|
|
210
|
+
<div class="flex gap-2">
|
|
211
|
+
<Button :loading="loading()" variant="secondary" @click="loadZones()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
212
|
+
<Button :disabled="loading() || methods().length === 0" @click="openCreate()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add zone</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="loadZones()">Retry</Button></div>
|
|
267
217
|
<div :if="!loading() && methods().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 shipping method before creating a zone.</div>
|
|
268
218
|
|
|
269
|
-
<
|
|
270
|
-
<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>
|
|
271
|
-
</section>
|
|
219
|
+
<MetricCards label="Shipping zone metrics" columns="grid-cols-1 sm:grid-cols-2 xl:grid-cols-4" :items="metricCards()" />
|
|
272
220
|
|
|
273
221
|
<section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
274
222
|
<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,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formErrorMessages } from '../../../../functions/dashboard-form'
|
|
2
3
|
import type {
|
|
3
4
|
PrintDeviceRecord,
|
|
4
5
|
PrintDeviceStatus,
|
|
@@ -51,10 +52,7 @@ function submit(): void {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function errorMessages(): string[] {
|
|
54
|
-
return
|
|
55
|
-
const label = field.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ')
|
|
56
|
-
return `${label.charAt(0).toUpperCase()}${label.slice(1)}: ${message}`
|
|
57
|
-
})
|
|
55
|
+
return formErrorMessages(errors())
|
|
58
56
|
}
|
|
59
57
|
</script>
|
|
60
58
|
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '../../../../app/Actions/Dashboard/Commerce/print-device-records'
|
|
7
7
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
8
8
|
import { pushToast } from '../../../../functions/toasts'
|
|
9
|
+
import { pageControls } from '../../../../functions/dashboard-list'
|
|
9
10
|
|
|
10
11
|
interface PrintDevicesResponse {
|
|
11
12
|
records: PrintDeviceRecord[]
|
|
@@ -234,13 +235,7 @@ async function deletePrintDevice(): Promise<void> {
|
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
page.set(Math.max(1, page() - 1))
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function nextPage(): void {
|
|
242
|
-
page.set(Math.min(pageCount(), page() + 1))
|
|
243
|
-
}
|
|
238
|
+
const { previousPage, nextPage } = pageControls(page, pageCount)
|
|
244
239
|
|
|
245
240
|
onMount(() => {
|
|
246
241
|
void loadPrintDevices()
|
|
@@ -248,29 +243,17 @@ onMount(() => {
|
|
|
248
243
|
</script>
|
|
249
244
|
|
|
250
245
|
<div class="space-y-6">
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
</
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<Button tag="a" variant="secondary" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
|
|
259
|
-
<Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add printer</Button>
|
|
260
|
-
</div>
|
|
261
|
-
</header>
|
|
246
|
+
<PageHeader title="Receipt printers" description="Manage native PrintDevice records and inspect their persisted operational state.">
|
|
247
|
+
<div class="flex flex-wrap gap-2">
|
|
248
|
+
<Button variant="secondary" :loading="loading()" @click="loadPrintDevices"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
249
|
+
<Button tag="a" variant="secondary" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export filtered</Button>
|
|
250
|
+
<Button @click="openAdd()"><span class="h-4 w-4 i-hugeicons-add-01"></span>Add printer</Button>
|
|
251
|
+
</div>
|
|
252
|
+
</PageHeader>
|
|
262
253
|
|
|
263
254
|
<div :if="loadError()" class="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">{{ loadError() }}</div>
|
|
264
255
|
|
|
265
|
-
<
|
|
266
|
-
<template :for="item in metricCards()" :key="item.label">
|
|
267
|
-
<article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
268
|
-
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
|
|
269
|
-
<p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
|
|
270
|
-
<p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
|
|
271
|
-
</article>
|
|
272
|
-
</template>
|
|
273
|
-
</section>
|
|
256
|
+
<MetricCards label="Print device metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
|
|
274
257
|
|
|
275
258
|
<PrintDeviceBreakdown :records="records()" />
|
|
276
259
|
|
|
@@ -189,28 +189,16 @@ onMount(() => {
|
|
|
189
189
|
</script>
|
|
190
190
|
|
|
191
191
|
<div class="space-y-6">
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<Button variant="secondary" :loading="loading()" @click="loadPrintLogs"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
199
|
-
<Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current page</Button>
|
|
200
|
-
</div>
|
|
201
|
-
</header>
|
|
192
|
+
<PageHeader title="Print logs" description="Inspect native Receipt records from configured print devices.">
|
|
193
|
+
<div class="flex flex-wrap gap-2">
|
|
194
|
+
<Button variant="secondary" :loading="loading()" @click="loadPrintLogs"><span :if="!loading()" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
195
|
+
<Button tag="a" :href="exportHref()" :download="exportFilename()"><span class="h-4 w-4 i-hugeicons-download-04"></span>Export current page</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</PageHeader>
|
|
202
198
|
|
|
203
199
|
<div :if="loadError()" class="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">{{ loadError() }}</div>
|
|
204
200
|
|
|
205
|
-
<
|
|
206
|
-
<template :for="item in metricCards()" :key="item.label">
|
|
207
|
-
<article class="p-4 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
|
|
208
|
-
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ item.label }}</p>
|
|
209
|
-
<p class="mt-2 font-semibold text-2xl text-gray-900 dark:text-white">{{ item.value }}</p>
|
|
210
|
-
<p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ item.detail }}</p>
|
|
211
|
-
</article>
|
|
212
|
-
</template>
|
|
213
|
-
</section>
|
|
201
|
+
<MetricCards label="Print log metrics" columns="grid-cols-2 lg:grid-cols-4" :items="metricCards()" />
|
|
214
202
|
|
|
215
203
|
<PrintLogBreakdown :records="records()" />
|
|
216
204
|
|
|
@@ -150,22 +150,18 @@ onMount(() => {
|
|
|
150
150
|
</script>
|
|
151
151
|
|
|
152
152
|
<div class="space-y-6">
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Add entry
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
</header>
|
|
153
|
+
<PageHeader title="Product Waitlist" description="Native WaitlistProduct records without simulated customers or activity.">
|
|
154
|
+
<div class="flex gap-2">
|
|
155
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadRecords">
|
|
156
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
157
|
+
Refresh
|
|
158
|
+
</Button>
|
|
159
|
+
<Button :disabled="isLoading() || options().products.length === 0" @click="openAddDialog()">
|
|
160
|
+
<span class="h-4 w-4 i-hugeicons-add-01"></span>
|
|
161
|
+
Add entry
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</PageHeader>
|
|
169
165
|
|
|
170
166
|
<div :if="loadError()" class="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">{{ loadError() }}</div>
|
|
171
167
|
<div :if="!isLoading() && options().products.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">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { ProductWaitlistOptions, ProductWaitlistRecord, ProductWaitlistWritePayload } from '../../../../app/Actions/Dashboard/Commerce/product-waitlist-records'
|
|
3
|
+
import { formErrorMessages } from '../../../../functions/dashboard-form'
|
|
3
4
|
|
|
4
5
|
type DialogMode = 'add' | 'edit'
|
|
5
6
|
|
|
@@ -94,10 +95,7 @@ function submitForm(): void {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
function errorMessages(): string[] {
|
|
97
|
-
return
|
|
98
|
-
const label = field.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ')
|
|
99
|
-
return `${label.charAt(0).toUpperCase()}${label.slice(1)}: ${message}`
|
|
100
|
-
})
|
|
98
|
+
return formErrorMessages(errors())
|
|
101
99
|
}
|
|
102
100
|
</script>
|
|
103
101
|
|
|
@@ -207,22 +207,18 @@ onMount(() => {
|
|
|
207
207
|
</script>
|
|
208
208
|
|
|
209
209
|
<div class="space-y-6">
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Add party
|
|
223
|
-
</Button>
|
|
224
|
-
</div>
|
|
225
|
-
</header>
|
|
210
|
+
<PageHeader title="Restaurant Waitlist" description="Manage persisted WaitlistRestaurant records and seating states.">
|
|
211
|
+
<div class="flex gap-2">
|
|
212
|
+
<Button variant="secondary" :loading="isLoading()" @click="loadRecords">
|
|
213
|
+
<span :if="!isLoading()" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
214
|
+
Refresh
|
|
215
|
+
</Button>
|
|
216
|
+
<Button :disabled="isLoading()" @click="openAddDialog()">
|
|
217
|
+
<span class="h-4 w-4 i-hugeicons-add-01"></span>
|
|
218
|
+
Add party
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
</PageHeader>
|
|
226
222
|
|
|
227
223
|
<div :if="loadError()" class="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">{{ loadError() }}</div>
|
|
228
224
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { RestaurantWaitlistOptions, RestaurantWaitlistRecord, RestaurantWaitlistWritePayload } from '../../../../app/Actions/Dashboard/Commerce/restaurant-waitlist-records'
|
|
3
3
|
import { restaurantWaitlistDateTimeLocal, restaurantWaitlistTimestamp } from '../../../../app/Actions/Dashboard/Commerce/restaurant-waitlist-records'
|
|
4
|
+
import { formErrorMessages } from '../../../../functions/dashboard-form'
|
|
4
5
|
|
|
5
6
|
type DialogMode = 'add' | 'edit'
|
|
6
7
|
|
|
@@ -104,10 +105,7 @@ function submitForm(): void {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function errorMessages(): string[] {
|
|
107
|
-
return
|
|
108
|
-
const label = field.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ')
|
|
109
|
-
return `${label.charAt(0).toUpperCase()}${label.slice(1)}: ${message}`
|
|
110
|
-
})
|
|
108
|
+
return formErrorMessages(errors())
|
|
111
109
|
}
|
|
112
110
|
</script>
|
|
113
111
|
|
|
@@ -200,27 +200,23 @@ onMount(() => {
|
|
|
200
200
|
</script>
|
|
201
201
|
|
|
202
202
|
<div class="space-y-6">
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
</
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Refresh
|
|
221
|
-
</Button>
|
|
222
|
-
</div>
|
|
223
|
-
</header>
|
|
203
|
+
<PageHeader title="Deployments" description="Deployment model records through the guarded dashboard API.">
|
|
204
|
+
<div class="flex gap-2 items-center">
|
|
205
|
+
<Button :disabled="isStarting() || isPreviewing()" variant="outline" @click="openDeploymentDialog('preview')">
|
|
206
|
+
Preview
|
|
207
|
+
</Button>
|
|
208
|
+
<Button :disabled="isStarting() || isPreviewing() || isRollingBack()" variant="secondary" @click="openRollback">
|
|
209
|
+
Rollback
|
|
210
|
+
</Button>
|
|
211
|
+
<Button :disabled="isStarting() || isPreviewing()" @click="openDeploymentDialog('deploy')">
|
|
212
|
+
Deploy
|
|
213
|
+
</Button>
|
|
214
|
+
<Button :loading="isLoading()" variant="secondary" @click="loadDeployments">
|
|
215
|
+
<span :if="!isLoading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>
|
|
216
|
+
Refresh
|
|
217
|
+
</Button>
|
|
218
|
+
</div>
|
|
219
|
+
</PageHeader>
|
|
224
220
|
|
|
225
221
|
<div :if="loadError()" class="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">{{ loadError() }}</div>
|
|
226
222
|
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
9
9
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
10
10
|
import { pushToast } from '../../../../functions/toasts'
|
|
11
|
+
import { pageControls } from '../../../../functions/dashboard-list'
|
|
11
12
|
|
|
12
13
|
const emptySummary: AbandonedCartSummary = {
|
|
13
14
|
open: 0,
|
|
@@ -162,15 +163,7 @@ async function createCampaign(payload: Record<string, unknown>): Promise<void> {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
if (page() > 1)
|
|
167
|
-
page.set(page() - 1)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function nextPage(): void {
|
|
171
|
-
if (page() < totalPages())
|
|
172
|
-
page.set(page() + 1)
|
|
173
|
-
}
|
|
166
|
+
const { previousPage, nextPage } = pageControls(page, totalPages)
|
|
174
167
|
|
|
175
168
|
onMount(() => {
|
|
176
169
|
void loadCarts()
|
|
@@ -178,16 +171,12 @@ onMount(() => {
|
|
|
178
171
|
</script>
|
|
179
172
|
|
|
180
173
|
<div class="space-y-6">
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Button :loading="loading()" variant="secondary" @click="loadCarts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
188
|
-
<Button @click="openCompose()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-mail-send-01"></span>New recovery campaign</Button>
|
|
189
|
-
</div>
|
|
190
|
-
</header>
|
|
174
|
+
<PageHeader title="Abandoned Carts" description="The audience that already chose the products. Every other campaign starts by guessing.">
|
|
175
|
+
<div class="flex gap-2">
|
|
176
|
+
<Button :loading="loading()" variant="secondary" @click="loadCarts()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
177
|
+
<Button @click="openCompose()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-mail-send-01"></span>New recovery campaign</Button>
|
|
178
|
+
</div>
|
|
179
|
+
</PageHeader>
|
|
191
180
|
|
|
192
181
|
<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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadCarts()">Retry</Button></div>
|
|
193
182
|
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type { CampaignDeliveryOperation } from '../../../../app/Actions/Dashboard/Marketing/campaign-delivery'
|
|
8
8
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
9
9
|
import { pushToast } from '../../../../functions/toasts'
|
|
10
|
+
import { pageControls } from '../../../../functions/dashboard-list'
|
|
10
11
|
|
|
11
12
|
interface CampaignIndexResponse {
|
|
12
13
|
records: CampaignRecord[]
|
|
@@ -271,15 +272,7 @@ async function confirmDelivery(payload: { scheduledAt?: string }): Promise<void>
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
|
|
275
|
-
if (page() > 1)
|
|
276
|
-
page.set(page() - 1)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function nextPage(): void {
|
|
280
|
-
if (page() < totalPages())
|
|
281
|
-
page.set(page() + 1)
|
|
282
|
-
}
|
|
275
|
+
const { previousPage, nextPage } = pageControls(page, totalPages)
|
|
283
276
|
|
|
284
277
|
onMount(() => {
|
|
285
278
|
void loadCampaigns()
|
|
@@ -287,16 +280,12 @@ onMount(() => {
|
|
|
287
280
|
</script>
|
|
288
281
|
|
|
289
282
|
<div class="space-y-6">
|
|
290
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
<Button :loading="loading()" variant="secondary" @click="loadCampaigns()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
297
|
-
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add campaign</Button>
|
|
298
|
-
</div>
|
|
299
|
-
</header>
|
|
283
|
+
<PageHeader title="Campaigns" description="Manage Campaign records with real audience and delivery aggregates.">
|
|
284
|
+
<div class="flex gap-2">
|
|
285
|
+
<Button :loading="loading()" variant="secondary" @click="loadCampaigns()"><span :if="!loading()" aria-hidden="true" class="h-4 w-4 i-hugeicons-refresh"></span>Refresh</Button>
|
|
286
|
+
<Button @click="openAdd()"><span aria-hidden="true" class="h-4 w-4 i-hugeicons-add-01"></span>Add campaign</Button>
|
|
287
|
+
</div>
|
|
288
|
+
</PageHeader>
|
|
300
289
|
|
|
301
290
|
<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"><span>{{ loadError() }}</span><Button variant="secondary" size="sm" @click="loadCampaigns()">Retry</Button></div>
|
|
302
291
|
|