@stacksjs/defaults 0.75.279 → 0.75.280
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/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +28 -30
- package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +8 -28
- package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +72 -29
- package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +8 -29
- package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +69 -49
- package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +8 -27
- package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +70 -48
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +8 -29
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +72 -82
- package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +3 -0
- package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +8 -28
- package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +76 -49
- package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +3 -0
- package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +8 -28
- package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +81 -34
- package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +11 -37
- package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +106 -77
- package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +3 -0
- package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +3 -0
- package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +1 -1
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +3 -0
- package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +1 -11
- package/resources/components/Dashboard/Commerce/Delivery/CouriersTable.stx +55 -82
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +1 -12
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +57 -86
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +1 -24
- package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +62 -80
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +1 -12
- package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +58 -99
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +1 -23
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +51 -86
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +1 -12
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +58 -71
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +1 -12
- package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +85 -110
- package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +8 -28
- package/resources/components/Dashboard/Commerce/PrintDevicesTable.stx +62 -57
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +7 -24
- package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +63 -59
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +7 -25
- package/resources/components/Dashboard/Marketing/CampaignsTable.stx +80 -57
- package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +7 -27
- package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +66 -71
- package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +7 -25
- package/resources/components/Dashboard/Marketing/SocialPostsTable.stx +75 -62
- package/resources/components/Dashboard/Monitoring/ErrorDashboard.stx +9 -50
- package/resources/components/Dashboard/Monitoring/ErrorTable.stx +68 -97
- package/resources/components/Dashboard/Notifications/NotificationDeliveries.stx +22 -44
- package/resources/components/Dashboard/Notifications/NotificationDeliveryHistory.stx +13 -4
- package/resources/components/Dashboard/Notifications/NotificationDeliveryTable.stx +74 -86
- package/resources/components/Dashboard/UI/Table.stx +41 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
type SerializedList = string | string[] | null | undefined
|
|
3
5
|
|
|
@@ -13,6 +15,9 @@ interface ShippingZoneRow {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const zones = useReactiveProp('zones', [] as ShippingZoneRow[])
|
|
18
|
+
const loading = useReactiveProp('loading', false)
|
|
19
|
+
/** The parent's search: a change returns to the first page. */
|
|
20
|
+
const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
|
|
16
21
|
const emit = defineEmits()
|
|
17
22
|
|
|
18
23
|
function parseList(value: SerializedList): string[] {
|
|
@@ -33,119 +38,89 @@ function parseList(value: SerializedList): string[] {
|
|
|
33
38
|
return []
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
41
|
+
/** Countries and regions are slotted for their chips: the first three, then a count. */
|
|
42
|
+
const COLUMNS = [
|
|
43
|
+
{ key: 'name', label: 'Zone', sortable: true, class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
|
|
44
|
+
{ key: 'methodLabel', label: 'Method', sortable: true, class: 'whitespace-nowrap' },
|
|
45
|
+
{ key: 'countriesText', label: 'Countries', slotted: true },
|
|
46
|
+
{ key: 'regionsText', label: 'Regions', slotted: true },
|
|
47
|
+
{ key: 'postalLabel', label: 'Postal codes', class: 'max-w-xs truncate' },
|
|
48
|
+
{
|
|
49
|
+
key: 'status',
|
|
50
|
+
label: 'Status',
|
|
51
|
+
type: 'badge' as const,
|
|
52
|
+
sortable: true,
|
|
53
|
+
badgeClasses: {
|
|
54
|
+
active: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300',
|
|
55
|
+
draft: 'bg-amber-50 text-amber-700 dark:bg-amber-400/10 dark:text-amber-300',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
]
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function formatPostalCodes(value: SerializedList): string {
|
|
45
|
-
const postalCodes = parseList(value)
|
|
46
|
-
return postalCodes.length ? postalCodes.join(', ') : 'All postal codes'
|
|
47
|
-
}
|
|
60
|
+
const ROW_ACTIONS = [
|
|
61
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
62
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
63
|
+
]
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
const rows = derived(() => zones().map((zone) => {
|
|
66
|
+
const countries = parseList(zone.countries)
|
|
67
|
+
const regions = parseList(zone.regions)
|
|
68
|
+
const postal = parseList(zone.postal_codes)
|
|
69
|
+
return {
|
|
70
|
+
...zone,
|
|
71
|
+
methodLabel: zone.shipping_method?.name || `Method #${zone.shipping_method_id}`,
|
|
72
|
+
countryChips: countries.slice(0, 3),
|
|
73
|
+
countriesMore: countries.length > 3 ? `+${countries.length - 3}` : '',
|
|
74
|
+
countriesText: countries.join(', '),
|
|
75
|
+
regionChips: regions.slice(0, 3),
|
|
76
|
+
regionsMore: regions.length > 3 ? `+${regions.length - 3}` : '',
|
|
77
|
+
regionsText: regions.join(', '),
|
|
78
|
+
postalLabel: postal.length ? postal.join(', ') : 'All postal codes',
|
|
79
|
+
actionName: `${zone.name}, record ${zone.id}`,
|
|
80
|
+
}
|
|
81
|
+
}))
|
|
57
82
|
|
|
58
|
-
function
|
|
59
|
-
|
|
83
|
+
function handleRowAction(payload: { action: string, row: ShippingZoneRow }): void {
|
|
84
|
+
emit(payload.action, payload.row)
|
|
60
85
|
}
|
|
61
86
|
</script>
|
|
62
87
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
<template :for="region in visibleItems(zone.regions)">
|
|
103
|
-
<span class="inline-flex px-2 py-1 font-medium text-emerald-700 text-xs dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-400/10 rounded-full">
|
|
104
|
-
{{ region }}
|
|
105
|
-
</span>
|
|
106
|
-
</template>
|
|
107
|
-
<span :if="remainingItems(zone.regions) > 0" class="inline-flex px-2 py-1 font-medium text-stone-600 text-xs dark:text-neutral-300 bg-stone-100 dark:bg-white/5 rounded-full">
|
|
108
|
-
+{{ remainingItems(zone.regions) }}
|
|
109
|
-
</span>
|
|
110
|
-
<span :if="parseList(zone.regions).length === 0">All regions</span>
|
|
111
|
-
</div>
|
|
112
|
-
</td>
|
|
113
|
-
<td class="px-4 py-3.5 max-w-xs text-sm text-stone-500 truncate dark:text-neutral-300">
|
|
114
|
-
{{ formatPostalCodes(zone.postal_codes) }}
|
|
115
|
-
</td>
|
|
116
|
-
<td class="px-4 py-3.5 text-sm whitespace-nowrap">
|
|
117
|
-
<span :class="'inline-flex px-2.5 py-1 font-medium text-xs ring-1 ring-inset rounded-full ' + statusClass(zone.status)">
|
|
118
|
-
{{ formatStatus(zone.status) }}
|
|
119
|
-
</span>
|
|
120
|
-
</td>
|
|
121
|
-
<td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
|
|
122
|
-
<Button
|
|
123
|
-
variant="ghost"
|
|
124
|
-
size="sm"
|
|
125
|
-
iconOnly
|
|
126
|
-
@click="emit('edit', zone)"
|
|
127
|
-
:ariaLabel="'Edit ' + zone.name + ', record ' + zone.id"
|
|
128
|
-
>
|
|
129
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
130
|
-
</Button>
|
|
131
|
-
<Button
|
|
132
|
-
variant="ghost"
|
|
133
|
-
size="sm"
|
|
134
|
-
iconOnly
|
|
135
|
-
@click="emit('delete', zone)"
|
|
136
|
-
:ariaLabel="'Delete ' + zone.name + ', record ' + zone.id"
|
|
137
|
-
>
|
|
138
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
139
|
-
</Button>
|
|
140
|
-
</td>
|
|
141
|
-
</tr>
|
|
142
|
-
</template>
|
|
143
|
-
|
|
144
|
-
<tr :if="zones().length === 0">
|
|
145
|
-
<td colspan="7" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
|
|
146
|
-
No shipping zones found.
|
|
147
|
-
</td>
|
|
148
|
-
</tr>
|
|
149
|
-
</tbody>
|
|
150
|
-
</table>
|
|
151
|
-
</div>
|
|
88
|
+
{{--
|
|
89
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box.
|
|
90
|
+
Table pages the whole filtered list the parent passes, and stays mounted
|
|
91
|
+
while it loads.
|
|
92
|
+
--}}
|
|
93
|
+
<Table
|
|
94
|
+
:page-reset-key="pageResetKey()"
|
|
95
|
+
:columns="COLUMNS"
|
|
96
|
+
:data="rows()"
|
|
97
|
+
:loading="loading()"
|
|
98
|
+
:row-actions="ROW_ACTIONS"
|
|
99
|
+
action-label-key="actionName"
|
|
100
|
+
paginated
|
|
101
|
+
:page-size="10"
|
|
102
|
+
empty-title="No shipping zones found"
|
|
103
|
+
empty-description="Add a zone, or adjust the search."
|
|
104
|
+
@row-action="handleRowAction"
|
|
105
|
+
>
|
|
106
|
+
<template #cell="{ row, column }">
|
|
107
|
+
<template :if="column.key === 'countriesText'">
|
|
108
|
+
<div class="flex flex-wrap gap-1">
|
|
109
|
+
<template :for="country in row.countryChips">
|
|
110
|
+
<span class="inline-flex px-2 py-1 font-medium text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-400/10 rounded-full" x-text="country"></span>
|
|
111
|
+
</template>
|
|
112
|
+
<span :if="row.countriesMore" class="inline-flex px-2 py-1 font-medium text-stone-600 text-xs dark:text-neutral-300 bg-stone-100 dark:bg-white/5 rounded-full" x-text="row.countriesMore"></span>
|
|
113
|
+
<span :if="row.countryChips.length === 0">All countries</span>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
<template :if="column.key === 'regionsText'">
|
|
117
|
+
<div class="flex flex-wrap gap-1">
|
|
118
|
+
<template :for="region in row.regionChips">
|
|
119
|
+
<span class="inline-flex px-2 py-1 font-medium text-emerald-700 text-xs dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-400/10 rounded-full" x-text="region"></span>
|
|
120
|
+
</template>
|
|
121
|
+
<span :if="row.regionsMore" class="inline-flex px-2 py-1 font-medium text-stone-600 text-xs dark:text-neutral-300 bg-stone-100 dark:bg-white/5 rounded-full" x-text="row.regionsMore"></span>
|
|
122
|
+
<span :if="row.regionChips.length === 0">All regions</span>
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|
|
125
|
+
</template>
|
|
126
|
+
</Table>
|
|
@@ -6,7 +6,6 @@ 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'
|
|
10
9
|
|
|
11
10
|
interface PrintDevicesResponse {
|
|
12
11
|
records: PrintDeviceRecord[]
|
|
@@ -35,8 +34,6 @@ const search = state('')
|
|
|
35
34
|
const statusFilter = state('all')
|
|
36
35
|
const locationFilter = state('all')
|
|
37
36
|
const sort = state('name')
|
|
38
|
-
const page = state(1)
|
|
39
|
-
const pageSize = 20
|
|
40
37
|
const loading = state(true)
|
|
41
38
|
const loadError = state('')
|
|
42
39
|
const dialogOpen = state(false)
|
|
@@ -76,15 +73,6 @@ function filteredRecords(): PrintDeviceRecord[] {
|
|
|
76
73
|
})
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
function pageCount(): number {
|
|
80
|
-
return Math.max(1, Math.ceil(filteredRecords().length / pageSize))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function visibleRecords(): PrintDeviceRecord[] {
|
|
84
|
-
const selectedPage = Math.min(page(), pageCount())
|
|
85
|
-
return filteredRecords().slice((selectedPage - 1) * pageSize, selectedPage * pageSize)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
76
|
function metricCards(): Array<{ label: string, value: string, detail: string }> {
|
|
89
77
|
const current = summary()
|
|
90
78
|
const health = current.total ? Math.round((current.online / current.total) * 100) : 0
|
|
@@ -118,13 +106,13 @@ function exportFilename(): string {
|
|
|
118
106
|
return `print-devices-${new Date().toISOString().slice(0, 10)}.csv`
|
|
119
107
|
}
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
109
|
+
/**
|
|
110
|
+
* What the table is showing: a change returns it to the first page, as this
|
|
111
|
+
* page's own pager did before the table took over paging.
|
|
112
|
+
*/
|
|
113
|
+
function viewKey(): string {
|
|
114
|
+
return JSON.stringify([search(), statusFilter(), locationFilter(), sort()])
|
|
115
|
+
}
|
|
128
116
|
|
|
129
117
|
async function loadPrintDevices(): Promise<void> {
|
|
130
118
|
loading.set(true)
|
|
@@ -133,7 +121,6 @@ async function loadPrintDevices(): Promise<void> {
|
|
|
133
121
|
const data = await dashboardApi<PrintDevicesResponse>('/api/dashboard/commerce/print-devices')
|
|
134
122
|
records.set(Array.isArray(data.records) ? data.records : [])
|
|
135
123
|
summary.set(data.summary ?? { ...emptySummary })
|
|
136
|
-
page.set(Math.min(page(), Math.max(1, Math.ceil((data.records?.length || 0) / pageSize))))
|
|
137
124
|
}
|
|
138
125
|
catch (error) {
|
|
139
126
|
loadError.set('Print devices could not be loaded.')
|
|
@@ -235,8 +222,6 @@ async function deletePrintDevice(): Promise<void> {
|
|
|
235
222
|
}
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
const { previousPage, nextPage } = pageControls(page, pageCount)
|
|
239
|
-
|
|
240
225
|
onMount(() => {
|
|
241
226
|
void loadPrintDevices()
|
|
242
227
|
})
|
|
@@ -271,14 +256,9 @@ onMount(() => {
|
|
|
271
256
|
|
|
272
257
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
273
258
|
<p class="text-gray-500 text-sm dark:text-neutral-400">{{ filteredRecords().length }} matching print devices</p>
|
|
274
|
-
<div class="flex gap-2 items-center">
|
|
275
|
-
<Button variant="secondary" size="sm" :disabled="page() <= 1" @click="previousPage()">Previous</Button>
|
|
276
|
-
<span class="text-gray-500 text-sm dark:text-neutral-400">Page {{ Math.min(page(), pageCount()) }} of {{ pageCount() }}</span>
|
|
277
|
-
<Button variant="secondary" size="sm" :disabled="page() >= pageCount()" @click="nextPage()">Next</Button>
|
|
278
|
-
</div>
|
|
279
259
|
</div>
|
|
280
260
|
|
|
281
|
-
<PrintDevicesTable :records="
|
|
261
|
+
<PrintDevicesTable :sort-reset-key="sort()" :page-reset-key="viewKey()" :records="filteredRecords()" :loading="loading()" @view="openDetails" @edit="openEdit" @delete="openDelete" />
|
|
282
262
|
</section>
|
|
283
263
|
</div>
|
|
284
264
|
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { PrintDeviceRecord } from '../../../../app/Actions/Dashboard/Commerce/print-device-records'
|
|
3
5
|
|
|
4
6
|
const records = useReactiveProp('records', [] as PrintDeviceRecord[])
|
|
5
7
|
const loading = useReactiveProp('loading', false)
|
|
8
|
+
/** The parent's sort select: a change hands the order back from a header sort. */
|
|
9
|
+
const sortResetKey = useReactiveProp<unknown>('sortResetKey', '')
|
|
10
|
+
/** The parent's search and filters: a change returns to the first page. */
|
|
11
|
+
const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
|
|
6
12
|
const emit = defineEmits()
|
|
7
13
|
|
|
8
|
-
function statusClass(status: string): string {
|
|
9
|
-
if (status === 'online')
|
|
10
|
-
return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300'
|
|
11
|
-
if (status === 'warning')
|
|
12
|
-
return 'bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300'
|
|
13
|
-
return 'bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-300'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function statusLabel(status: string): string {
|
|
17
|
-
return status.charAt(0).toUpperCase() + status.slice(1)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
14
|
function formatHeartbeat(value: number): string {
|
|
21
15
|
if (!value)
|
|
22
16
|
return 'Not recorded'
|
|
@@ -34,50 +28,61 @@ function formatHeartbeat(value: number): string {
|
|
|
34
28
|
return `${Math.abs(hours)} hr ${hours > 0 ? 'ago' : 'from now'}`
|
|
35
29
|
return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
36
30
|
}
|
|
31
|
+
|
|
32
|
+
/** The heartbeat reads relative to now, so it is text rather than a date column. */
|
|
33
|
+
const COLUMNS = [
|
|
34
|
+
{ key: 'nameLabel', label: 'Printer', sortable: true, subtitleKey: 'macLabel', class: 'font-medium text-gray-900 dark:text-white' },
|
|
35
|
+
{ key: 'location', label: 'Location', sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
36
|
+
{ key: 'terminal', label: 'Terminal', sortable: true, emptyText: 'Not recorded', class: 'font-mono whitespace-nowrap' },
|
|
37
|
+
{
|
|
38
|
+
key: 'status',
|
|
39
|
+
label: 'Status',
|
|
40
|
+
type: 'badge' as const,
|
|
41
|
+
sortable: true,
|
|
42
|
+
badgeClasses: {
|
|
43
|
+
online: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/50 dark:text-emerald-300',
|
|
44
|
+
warning: 'bg-amber-100 text-amber-800 dark:bg-amber-950/50 dark:text-amber-300',
|
|
45
|
+
offline: 'bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-300',
|
|
46
|
+
},
|
|
47
|
+
badgeLabels: { online: 'Online', warning: 'Warning', offline: 'Offline' },
|
|
48
|
+
},
|
|
49
|
+
{ key: 'heartbeatLabel', label: 'Last heartbeat', searchable: false, class: 'whitespace-nowrap' },
|
|
50
|
+
{ key: 'printCount', label: 'Prints', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const ROW_ACTIONS = [
|
|
54
|
+
{ action: 'view', label: 'View', icon: 'i-hugeicons-view' },
|
|
55
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
56
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const rows = derived(() => records().map(record => ({
|
|
60
|
+
...record,
|
|
61
|
+
nameLabel: record.name || 'Unnamed printer',
|
|
62
|
+
macLabel: record.macAddress || 'No MAC address',
|
|
63
|
+
heartbeatLabel: formatHeartbeat(record.lastPing),
|
|
64
|
+
})))
|
|
65
|
+
|
|
66
|
+
function handleRowAction(payload: { action: string, row: PrintDeviceRecord }): void {
|
|
67
|
+
emit(payload.action, payload.row)
|
|
68
|
+
}
|
|
37
69
|
</script>
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-right text-xs uppercase dark:text-neutral-400">Prints</th>
|
|
58
|
-
<th scope="col" class="relative px-4 py-3"><span class="sr-only">Actions</span></th>
|
|
59
|
-
</tr>
|
|
60
|
-
</thead>
|
|
61
|
-
<tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
|
|
62
|
-
<template :for="record in records()" :key="record.id">
|
|
63
|
-
<tr class="hover:bg-gray-50/60 dark:hover:bg-neutral-700/40">
|
|
64
|
-
<td class="px-4 py-4 min-w-56">
|
|
65
|
-
<Button variant="ghost" size="xs" @click="emit('view', record)">{{ record.name || 'Unnamed printer' }}</Button>
|
|
66
|
-
<p class="mt-1 font-mono text-gray-400 text-xs dark:text-neutral-500">{{ record.macAddress || 'No MAC address' }}</p>
|
|
67
|
-
</td>
|
|
68
|
-
<td class="px-4 py-4 text-gray-700 text-sm whitespace-nowrap dark:text-neutral-200">{{ record.location || 'Not recorded' }}</td>
|
|
69
|
-
<td class="px-4 py-4 font-mono text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ record.terminal || 'Not recorded' }}</td>
|
|
70
|
-
<td class="px-4 py-4 whitespace-nowrap"><span :class="'inline-flex px-2 py-1 font-semibold text-xs rounded-full ' + statusClass(record.status)">{{ statusLabel(record.status) }}</span></td>
|
|
71
|
-
<td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatHeartbeat(record.lastPing) }}</td>
|
|
72
|
-
<td class="px-4 py-4 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ record.printCount.toLocaleString() }}</td>
|
|
73
|
-
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
74
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'View ' + record.name" @click="emit('view', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-view"></span></Button>
|
|
75
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span></Button>
|
|
76
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span></Button>
|
|
77
|
-
</td>
|
|
78
|
-
</tr>
|
|
79
|
-
</template>
|
|
80
|
-
</tbody>
|
|
81
|
-
</table>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
71
|
+
{{--
|
|
72
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
73
|
+
and the filter selects. Table pages the whole filtered list the parent passes.
|
|
74
|
+
--}}
|
|
75
|
+
<Table
|
|
76
|
+
:sort-reset-key="sortResetKey()"
|
|
77
|
+
:page-reset-key="pageResetKey()"
|
|
78
|
+
:columns="COLUMNS"
|
|
79
|
+
:data="rows()"
|
|
80
|
+
:loading="loading()"
|
|
81
|
+
:row-actions="ROW_ACTIONS"
|
|
82
|
+
action-label-key="nameLabel"
|
|
83
|
+
paginated
|
|
84
|
+
:page-size="20"
|
|
85
|
+
empty-title="No print devices found"
|
|
86
|
+
empty-description="Add a printer or adjust the current filters."
|
|
87
|
+
@row-action="handleRowAction"
|
|
88
|
+
/>
|
|
@@ -8,7 +8,6 @@ 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'
|
|
12
11
|
|
|
13
12
|
const emptySummary: AbandonedCartSummary = {
|
|
14
13
|
open: 0,
|
|
@@ -39,8 +38,6 @@ const search = state('')
|
|
|
39
38
|
const stateFilter = state('open')
|
|
40
39
|
const idleFilter = state('all')
|
|
41
40
|
const sort = state('value')
|
|
42
|
-
const page = state(1)
|
|
43
|
-
const perPage = 10
|
|
44
41
|
const dialogOpen = state(false)
|
|
45
42
|
const saving = state(false)
|
|
46
43
|
const saveError = state('')
|
|
@@ -73,23 +70,14 @@ function filteredRecords(): AbandonedCartRecord[] {
|
|
|
73
70
|
})
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return Math.max(1, Math.ceil(filteredRecords().length / perPage))
|
|
73
|
+
/**
|
|
74
|
+
* What the table is showing: a change returns it to the first page, as this
|
|
75
|
+
* page's own pager did before the table took over paging.
|
|
76
|
+
*/
|
|
77
|
+
function viewKey(): string {
|
|
78
|
+
return JSON.stringify([search(), stateFilter(), idleFilter(), sort()])
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
effect(() => {
|
|
86
|
-
search()
|
|
87
|
-
stateFilter()
|
|
88
|
-
idleFilter()
|
|
89
|
-
sort()
|
|
90
|
-
page.set(1)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
81
|
/** Cart totals and campaign thresholds are integer minor units (stacksjs/stacks#2851). */
|
|
94
82
|
function money(value: number): string {
|
|
95
83
|
return formatCurrency(value, summary().currency || defaultCurrency())
|
|
@@ -122,8 +110,6 @@ async function loadCarts(): Promise<void> {
|
|
|
122
110
|
campaigns.set(data.campaigns)
|
|
123
111
|
defaultCurrency.set(data.defaultCurrency || 'USD')
|
|
124
112
|
defaultIdleHours.set(data.defaultIdleHours || 4)
|
|
125
|
-
if (page() > totalPages())
|
|
126
|
-
page.set(totalPages())
|
|
127
113
|
}
|
|
128
114
|
catch (error) {
|
|
129
115
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -163,8 +149,6 @@ async function createCampaign(payload: Record<string, unknown>): Promise<void> {
|
|
|
163
149
|
}
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
const { previousPage, nextPage } = pageControls(page, totalPages)
|
|
167
|
-
|
|
168
152
|
onMount(() => {
|
|
169
153
|
void loadCarts()
|
|
170
154
|
})
|
|
@@ -214,8 +198,7 @@ onMount(() => {
|
|
|
214
198
|
<label><span class="sr-only">Filter by how long the cart has been idle</span><select x-model="idleFilter" class="px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-800 border border-gray-300 rounded-md dark:border-neutral-700"><option value="all">Any age</option><option value="4">Idle 4h or more</option><option value="24">Idle a day or more</option><option value="72">Idle 3 days or more</option></select></label>
|
|
215
199
|
<label><span class="sr-only">Sort carts</span><select x-model="sort" class="px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-800 border border-gray-300 rounded-md dark:border-neutral-700"><option value="value">Highest value</option><option value="idle">Longest idle</option><option value="customer">Customer</option></select></label>
|
|
216
200
|
</div>
|
|
217
|
-
<AbandonedCartsTable :records="
|
|
218
|
-
<div :if="filteredRecords().length > perPage" class="flex items-center justify-between"><p class="text-gray-500 text-sm dark:text-neutral-400">Page {{ page() }} of {{ totalPages() }}</p><div class="flex gap-2"><Button :disabled="page() === 1" variant="secondary" @click="previousPage()">Previous</Button><Button :disabled="page() === totalPages()" variant="secondary" @click="nextPage()">Next</Button></div></div>
|
|
201
|
+
<AbandonedCartsTable :sort-reset-key="sort()" :page-reset-key="viewKey()" :records="filteredRecords()" :loading="loading()" />
|
|
219
202
|
</section>
|
|
220
203
|
</div>
|
|
221
204
|
|