@stacksjs/defaults 0.75.279 → 0.75.281
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-database/SKILL.md +1 -3
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +3 -3
- package/project/bun-version +1 -1
- 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
|
interface DeliveryRouteRow {
|
|
3
5
|
id: number
|
|
@@ -12,97 +14,66 @@ interface DeliveryRouteRow {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const routes = useReactiveProp('routes', [] as DeliveryRouteRow[])
|
|
17
|
+
const loading = useReactiveProp('loading', false)
|
|
18
|
+
/** The parent's search: a change returns to the first page. */
|
|
19
|
+
const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
|
|
15
20
|
const emit = defineEmits()
|
|
16
21
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
function formatNumber(value: number): string {
|
|
23
|
+
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(value)
|
|
24
|
+
}
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/** Time and distance carry their units, so they read as text and sort by the number. */
|
|
27
|
+
const COLUMNS = [
|
|
28
|
+
{ key: 'routeLabel', label: 'Route', sortable: true, class: 'font-medium tabular-nums text-stone-800 whitespace-nowrap dark:text-white' },
|
|
29
|
+
{ key: 'courierLabel', label: 'Courier', sortable: true, subtitleKey: 'courierNote', class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
|
|
30
|
+
{ key: 'vehicle', label: 'Vehicle', sortable: true, class: 'whitespace-nowrap' },
|
|
31
|
+
{ key: 'stops', label: 'Stops', type: 'number' as const, sortable: true, class: 'tabular-nums' },
|
|
32
|
+
{ key: 'delivery_time', label: 'Delivery time', sortable: true, slotted: true },
|
|
33
|
+
{ key: 'total_distance', label: 'Distance', sortable: true, slotted: true },
|
|
34
|
+
{ key: 'last_active', label: 'Last active', type: 'datetime' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
35
|
+
]
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
37
|
+
const ROW_ACTIONS = [
|
|
38
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
39
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
40
|
+
]
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const rows = derived(() => routes().map(route => ({
|
|
43
|
+
...route,
|
|
44
|
+
routeLabel: `#${route.id}`,
|
|
45
|
+
courierLabel: route.courier_record?.name || route.courier,
|
|
46
|
+
courierNote: route.courier_id ? `Linked courier #${route.courier_id}` : 'Legacy snapshot',
|
|
47
|
+
timeLabel: `${formatNumber(route.delivery_time)} min`,
|
|
48
|
+
distanceLabel: `${formatNumber(route.total_distance)} mi`,
|
|
49
|
+
actionName: `delivery route ${route.id} for ${route.courier}`,
|
|
50
|
+
})))
|
|
51
|
+
|
|
52
|
+
function handleRowAction(payload: { action: string, row: DeliveryRouteRow }): void {
|
|
53
|
+
emit(payload.action, payload.row)
|
|
34
54
|
}
|
|
35
55
|
</script>
|
|
36
56
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<span class="block">{{ route.courier_record?.name || route.courier }}</span>
|
|
61
|
-
<span class="block font-normal text-stone-400 text-xs dark:text-neutral-500">{{ route.courier_id ? 'Linked courier #' + route.courier_id : 'Legacy snapshot' }}</span>
|
|
62
|
-
</td>
|
|
63
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
64
|
-
{{ route.vehicle }}
|
|
65
|
-
</td>
|
|
66
|
-
<td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
67
|
-
{{ route.stops }}
|
|
68
|
-
</td>
|
|
69
|
-
<td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
70
|
-
{{ formatNumber(route.delivery_time) }} min
|
|
71
|
-
</td>
|
|
72
|
-
<td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
73
|
-
{{ formatNumber(route.total_distance) }} mi
|
|
74
|
-
</td>
|
|
75
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
76
|
-
{{ formatDateTime(route.last_active) }}
|
|
77
|
-
</td>
|
|
78
|
-
<td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
|
|
79
|
-
<Button
|
|
80
|
-
variant="ghost"
|
|
81
|
-
size="sm"
|
|
82
|
-
iconOnly
|
|
83
|
-
@click="emit('edit', route)"
|
|
84
|
-
:ariaLabel="'Edit delivery route ' + route.id + ' for ' + route.courier"
|
|
85
|
-
>
|
|
86
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
87
|
-
</Button>
|
|
88
|
-
<Button
|
|
89
|
-
variant="ghost"
|
|
90
|
-
size="sm"
|
|
91
|
-
iconOnly
|
|
92
|
-
@click="emit('delete', route)"
|
|
93
|
-
:ariaLabel="'Delete delivery route ' + route.id + ' for ' + route.courier"
|
|
94
|
-
>
|
|
95
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
96
|
-
</Button>
|
|
97
|
-
</td>
|
|
98
|
-
</tr>
|
|
99
|
-
</template>
|
|
100
|
-
|
|
101
|
-
<tr :if="routes().length === 0">
|
|
102
|
-
<td colspan="8" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
|
|
103
|
-
No delivery routes found.
|
|
104
|
-
</td>
|
|
105
|
-
</tr>
|
|
106
|
-
</tbody>
|
|
107
|
-
</table>
|
|
108
|
-
</div>
|
|
57
|
+
{{--
|
|
58
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box.
|
|
59
|
+
Table pages the whole filtered list the parent passes, and stays mounted
|
|
60
|
+
while it loads.
|
|
61
|
+
--}}
|
|
62
|
+
<Table
|
|
63
|
+
:page-reset-key="pageResetKey()"
|
|
64
|
+
:columns="COLUMNS"
|
|
65
|
+
:data="rows()"
|
|
66
|
+
:loading="loading()"
|
|
67
|
+
:row-actions="ROW_ACTIONS"
|
|
68
|
+
action-label-key="actionName"
|
|
69
|
+
paginated
|
|
70
|
+
:page-size="10"
|
|
71
|
+
empty-title="No delivery routes found"
|
|
72
|
+
empty-description="Add a route, or adjust the search."
|
|
73
|
+
@row-action="handleRowAction"
|
|
74
|
+
>
|
|
75
|
+
<template #cell="{ row, column }">
|
|
76
|
+
<span :if="column.key === 'delivery_time'" class="tabular-nums whitespace-nowrap" x-text="row.timeLabel"></span>
|
|
77
|
+
<span :if="column.key === 'total_distance'" class="tabular-nums whitespace-nowrap" x-text="row.distanceLabel"></span>
|
|
78
|
+
</template>
|
|
79
|
+
</Table>
|
|
@@ -29,8 +29,6 @@ const records = state<DigitalDeliveryRecord[]>([])
|
|
|
29
29
|
const loading = state(true)
|
|
30
30
|
const loadError = state('')
|
|
31
31
|
const search = state('')
|
|
32
|
-
const page = state(1)
|
|
33
|
-
const perPage = state(10)
|
|
34
32
|
const editorOpen = state(false)
|
|
35
33
|
const editorMode = state<'create' | 'edit'>('create')
|
|
36
34
|
const editing = state<DigitalDeliveryRecord | null>(null)
|
|
@@ -53,11 +51,6 @@ function filteredRecords(): DigitalDeliveryRecord[] {
|
|
|
53
51
|
].some(value => String(value).toLowerCase().includes(query)))
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
function paginatedRecords(): DigitalDeliveryRecord[] {
|
|
57
|
-
const start = (page() - 1) * perPage()
|
|
58
|
-
return filteredRecords().slice(start, start + perPage())
|
|
59
|
-
}
|
|
60
|
-
|
|
61
54
|
function metricCards(): Array<{ label: string, value: string, detail: string }> {
|
|
62
55
|
return [
|
|
63
56
|
{ label: 'Methods', value: records().length.toLocaleString(), detail: 'Persisted digital delivery records' },
|
|
@@ -75,9 +68,6 @@ async function loadMethods(): Promise<void> {
|
|
|
75
68
|
if (!Array.isArray(data))
|
|
76
69
|
throw new TypeError('Digital delivery records were not returned in the expected shape.')
|
|
77
70
|
records.set(data)
|
|
78
|
-
const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
|
|
79
|
-
if (page() > lastPage)
|
|
80
|
-
page.set(lastPage)
|
|
81
71
|
}
|
|
82
72
|
catch (error) {
|
|
83
73
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -90,7 +80,6 @@ async function loadMethods(): Promise<void> {
|
|
|
90
80
|
|
|
91
81
|
function handleSearch(value: string): void {
|
|
92
82
|
search.set(value)
|
|
93
|
-
page.set(1)
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
function openCreate(): void {
|
|
@@ -139,7 +128,6 @@ async function saveMethod(payload: DigitalDeliveryPayload): Promise<void> {
|
|
|
139
128
|
else {
|
|
140
129
|
const nextRecords = [...records(), updated]
|
|
141
130
|
records.set(nextRecords)
|
|
142
|
-
page.set(Math.max(1, Math.ceil(nextRecords.length / perPage())))
|
|
143
131
|
}
|
|
144
132
|
editorOpen.set(false)
|
|
145
133
|
editing.set(null)
|
|
@@ -177,9 +165,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
177
165
|
})
|
|
178
166
|
records.set(records().filter(record => record.id !== current.id))
|
|
179
167
|
deleting.set(null)
|
|
180
|
-
const lastPage = Math.max(1, Math.ceil(filteredRecords().length / perPage()))
|
|
181
|
-
if (page() > lastPage)
|
|
182
|
-
page.set(lastPage)
|
|
183
168
|
pushToast('success', 'Digital delivery deleted', { detail: current.name })
|
|
184
169
|
}
|
|
185
170
|
catch (error) {
|
|
@@ -190,10 +175,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
190
175
|
}
|
|
191
176
|
}
|
|
192
177
|
|
|
193
|
-
function goToPage(value: number): void {
|
|
194
|
-
page.set(value)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
178
|
onMount(() => {
|
|
198
179
|
void loadMethods()
|
|
199
180
|
})
|
|
@@ -218,11 +199,7 @@ onMount(() => {
|
|
|
218
199
|
<div><h2 class="font-semibold text-gray-900 dark:text-white">Delivery methods</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
|
|
219
200
|
<SearchFilter placeholder="Search digital delivery" class="w-full sm:w-80" @search="handleSearch" />
|
|
220
201
|
</div>
|
|
221
|
-
<
|
|
222
|
-
<DigitalDeliveryTable :if="!loading()" :methods="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
|
|
223
|
-
<div :if="!loading()" class="px-4 py-3 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
|
|
224
|
-
<DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
|
|
225
|
-
</div>
|
|
202
|
+
<DigitalDeliveryTable :methods="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
|
|
226
203
|
</section>
|
|
227
204
|
</div>
|
|
228
205
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
type DigitalDeliveryStatus = 'active' | 'inactive'
|
|
3
5
|
|
|
@@ -13,89 +15,69 @@ interface DigitalDeliveryRow {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const methods = useReactiveProp('methods', [] as DigitalDeliveryRow[])
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
+
/** Expiry keeps its unit and sorts by the number of days. */
|
|
24
|
+
const COLUMNS = [
|
|
25
|
+
{ key: 'name', label: 'Method', sortable: true, class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
|
|
26
|
+
{ key: 'description', label: 'Description', class: 'max-w-xs truncate' },
|
|
27
|
+
{ key: 'download_limit', label: 'Downloads', type: 'number' as const, sortable: true, emptyText: 'Unlimited' },
|
|
28
|
+
{ key: 'expiry_days', label: 'Expiry', sortable: true, slotted: true },
|
|
29
|
+
{ key: 'requiresLogin', label: 'Login', type: 'boolean' as const, sortable: true },
|
|
30
|
+
{ key: 'automatic', label: 'Automatic', type: 'boolean' as const, sortable: true },
|
|
31
|
+
{
|
|
32
|
+
key: 'status',
|
|
33
|
+
label: 'Status',
|
|
34
|
+
type: 'badge' as const,
|
|
35
|
+
sortable: true,
|
|
36
|
+
badgeClasses: {
|
|
37
|
+
active: 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
|
38
|
+
inactive: 'bg-stone-100 text-stone-600 dark:bg-neutral-700 dark:text-neutral-300',
|
|
39
|
+
},
|
|
40
|
+
badgeLabels: { active: 'Active', inactive: 'Inactive' },
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const ROW_ACTIONS = [
|
|
45
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
46
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
47
|
+
]
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
const rows = derived(() => methods().map(method => ({
|
|
50
|
+
...method,
|
|
51
|
+
expiryLabel: `${method.expiry_days} days`,
|
|
52
|
+
requiresLogin: Boolean(method.requires_login),
|
|
53
|
+
automatic: Boolean(method.automatic_delivery),
|
|
54
|
+
actionName: `digital delivery method ${method.name}, record ${method.id}`,
|
|
55
|
+
})))
|
|
56
|
+
|
|
57
|
+
function handleRowAction(payload: { action: string, row: DigitalDeliveryRow }): void {
|
|
58
|
+
emit(payload.action, payload.row)
|
|
26
59
|
}
|
|
27
60
|
</script>
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<td class="px-4 py-3.5 max-w-72 text-sm text-stone-500 dark:text-neutral-300">
|
|
52
|
-
<span class="block truncate" :title="method.description">{{ method.description }}</span>
|
|
53
|
-
</td>
|
|
54
|
-
<td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
55
|
-
{{ method.download_limit ?? 'Unlimited' }}
|
|
56
|
-
</td>
|
|
57
|
-
<td class="px-4 py-3.5 tabular-nums text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
58
|
-
{{ method.expiry_days }} days
|
|
59
|
-
</td>
|
|
60
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
61
|
-
{{ yesNo(method.requires_login) }}
|
|
62
|
-
</td>
|
|
63
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
64
|
-
{{ yesNo(method.automatic_delivery) }}
|
|
65
|
-
</td>
|
|
66
|
-
<td class="px-4 py-3.5 text-sm whitespace-nowrap">
|
|
67
|
-
<span :class="'inline-flex items-center px-2 py-1 font-medium text-xs ring-1 ring-inset rounded-md ' + statusClass(method.status)">
|
|
68
|
-
{{ method.status === 'active' ? 'Active' : 'Inactive' }}
|
|
69
|
-
</span>
|
|
70
|
-
</td>
|
|
71
|
-
<td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
|
|
72
|
-
<Button
|
|
73
|
-
variant="ghost"
|
|
74
|
-
size="sm"
|
|
75
|
-
iconOnly
|
|
76
|
-
@click="emit('edit', method)"
|
|
77
|
-
:ariaLabel="'Edit digital delivery method ' + method.name + ', record ' + method.id"
|
|
78
|
-
>
|
|
79
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
80
|
-
</Button>
|
|
81
|
-
<Button
|
|
82
|
-
variant="ghost"
|
|
83
|
-
size="sm"
|
|
84
|
-
iconOnly
|
|
85
|
-
@click="emit('delete', method)"
|
|
86
|
-
:ariaLabel="'Delete digital delivery method ' + method.name + ', record ' + method.id"
|
|
87
|
-
>
|
|
88
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
89
|
-
</Button>
|
|
90
|
-
</td>
|
|
91
|
-
</tr>
|
|
92
|
-
</template>
|
|
93
|
-
|
|
94
|
-
<tr :if="methods().length === 0">
|
|
95
|
-
<td colspan="8" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
|
|
96
|
-
No digital delivery methods found.
|
|
97
|
-
</td>
|
|
98
|
-
</tr>
|
|
99
|
-
</tbody>
|
|
100
|
-
</table>
|
|
101
|
-
</div>
|
|
62
|
+
{{--
|
|
63
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box.
|
|
64
|
+
Table pages the whole filtered list the parent passes, and stays mounted
|
|
65
|
+
while it loads.
|
|
66
|
+
--}}
|
|
67
|
+
<Table
|
|
68
|
+
:page-reset-key="pageResetKey()"
|
|
69
|
+
:columns="COLUMNS"
|
|
70
|
+
:data="rows()"
|
|
71
|
+
:loading="loading()"
|
|
72
|
+
:row-actions="ROW_ACTIONS"
|
|
73
|
+
action-label-key="actionName"
|
|
74
|
+
paginated
|
|
75
|
+
:page-size="10"
|
|
76
|
+
empty-title="No digital delivery methods found"
|
|
77
|
+
empty-description="Add a method, or adjust the search."
|
|
78
|
+
@row-action="handleRowAction"
|
|
79
|
+
>
|
|
80
|
+
<template #cell="{ row }">
|
|
81
|
+
<span class="whitespace-nowrap" x-text="row.expiryLabel"></span>
|
|
82
|
+
</template>
|
|
83
|
+
</Table>
|
|
@@ -60,8 +60,6 @@ const {
|
|
|
60
60
|
loading,
|
|
61
61
|
loadError,
|
|
62
62
|
search,
|
|
63
|
-
page,
|
|
64
|
-
perPage,
|
|
65
63
|
editorOpen,
|
|
66
64
|
editorMode,
|
|
67
65
|
editing,
|
|
@@ -71,8 +69,6 @@ const {
|
|
|
71
69
|
deleteBusy,
|
|
72
70
|
deleteError,
|
|
73
71
|
filteredRecords,
|
|
74
|
-
paginatedRecords,
|
|
75
|
-
goToPage,
|
|
76
72
|
handleSearch,
|
|
77
73
|
openCreate,
|
|
78
74
|
openEdit,
|
|
@@ -81,7 +77,6 @@ const {
|
|
|
81
77
|
closeDelete,
|
|
82
78
|
upsert,
|
|
83
79
|
remove,
|
|
84
|
-
clampPage,
|
|
85
80
|
} = useRecordsDashboard<LicenseKeyRecord>({ filter: matchesQuery })
|
|
86
81
|
|
|
87
82
|
const options = state<LicenseKeyOptions>(emptyOptions)
|
|
@@ -132,7 +127,6 @@ async function loadKeys(): Promise<void> {
|
|
|
132
127
|
throw new TypeError('License key resources were not returned in the expected shape.')
|
|
133
128
|
options.set(optionData)
|
|
134
129
|
records.set(keyData.map(withRelationships))
|
|
135
|
-
clampPage()
|
|
136
130
|
}
|
|
137
131
|
catch (error) {
|
|
138
132
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -187,7 +181,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
187
181
|
})
|
|
188
182
|
remove(current)
|
|
189
183
|
deleting.set(null)
|
|
190
|
-
clampPage()
|
|
191
184
|
pushToast('success', 'License key deleted', { detail: current.key })
|
|
192
185
|
}
|
|
193
186
|
catch (error) {
|
|
@@ -222,11 +215,7 @@ onMount(() => {
|
|
|
222
215
|
<div><h2 class="font-semibold text-gray-900 dark:text-white">Key inventory</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
|
|
223
216
|
<SearchFilter placeholder="Search license keys" class="w-full sm:w-80" @search="handleSearch" />
|
|
224
217
|
</div>
|
|
225
|
-
<
|
|
226
|
-
<LicenseKeysTable :if="!loading()" :license-keys="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
|
|
227
|
-
<div :if="!loading()" class="px-4 py-3 bg-gray-50 dark:bg-neutral-900/50 border-gray-200 border-t dark:border-neutral-700">
|
|
228
|
-
<DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
|
|
229
|
-
</div>
|
|
218
|
+
<LicenseKeysTable :license-keys="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
|
|
230
219
|
</section>
|
|
231
220
|
</div>
|
|
232
221
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
type LicenseKeyStatus = 'active' | 'inactive' | 'unassigned'
|
|
3
5
|
|
|
@@ -23,110 +25,67 @@ interface LicenseKeyRow {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const licenseKeys = useReactiveProp('license-keys', [] as LicenseKeyRow[])
|
|
28
|
+
const loading = useReactiveProp('loading', false)
|
|
29
|
+
/** The parent's search: a change returns to the first page. */
|
|
30
|
+
const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
|
|
26
31
|
const emit = defineEmits()
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const COLUMNS = [
|
|
34
|
+
{ key: 'key', label: 'Key', sortable: true, class: 'font-medium font-mono text-stone-800 whitespace-nowrap dark:text-white' },
|
|
35
|
+
{ key: 'template', label: 'Template', sortable: true, class: 'whitespace-nowrap' },
|
|
36
|
+
{ key: 'customerLabel', label: 'Customer', sortable: true, subtitleKey: 'customerDetail', class: 'whitespace-nowrap' },
|
|
37
|
+
{ key: 'productLabel', label: 'Product', sortable: true, class: 'whitespace-nowrap' },
|
|
38
|
+
{ key: 'orderLabel', label: 'Order', sortable: true, class: 'whitespace-nowrap' },
|
|
39
|
+
{ key: 'created_at', label: 'Created', type: 'date' as const, sortable: true, emptyText: 'Not set', class: 'whitespace-nowrap' },
|
|
40
|
+
{ key: 'expiry_date', label: 'Expires', type: 'date' as const, sortable: true, emptyText: 'Not set', class: 'whitespace-nowrap' },
|
|
41
|
+
{
|
|
42
|
+
key: 'status',
|
|
43
|
+
label: 'Status',
|
|
44
|
+
type: 'badge' as const,
|
|
45
|
+
sortable: true,
|
|
46
|
+
badgeClasses: {
|
|
47
|
+
active: 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
|
48
|
+
inactive: 'bg-stone-100 text-stone-600 dark:bg-neutral-700 dark:text-neutral-300',
|
|
49
|
+
unassigned: 'bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
|
50
|
+
},
|
|
51
|
+
badgeLabels: { active: 'Active', inactive: 'Inactive', unassigned: 'Unassigned' },
|
|
52
|
+
},
|
|
53
|
+
]
|
|
31
54
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
const ROW_ACTIONS = [
|
|
56
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
57
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
58
|
+
]
|
|
36
59
|
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
const rows = derived(() => licenseKeys().map(licenseKey => ({
|
|
61
|
+
...licenseKey,
|
|
62
|
+
customerLabel: licenseKey.customer?.label || 'Unassigned',
|
|
63
|
+
customerDetail: licenseKey.customer?.detail || '',
|
|
64
|
+
productLabel: licenseKey.product?.label || 'Unassigned',
|
|
65
|
+
orderLabel: licenseKey.order?.label || 'Unassigned',
|
|
66
|
+
actionName: `license key ${licenseKey.key}, record ${licenseKey.id}`,
|
|
67
|
+
})))
|
|
39
68
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function statusClass(status: LicenseKeyStatus): string {
|
|
45
|
-
if (status === 'active')
|
|
46
|
-
return 'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-900/30 dark:text-green-300'
|
|
47
|
-
if (status === 'inactive')
|
|
48
|
-
return 'bg-stone-100 text-stone-600 ring-stone-500/20 dark:bg-neutral-700 dark:text-neutral-300'
|
|
49
|
-
return 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-300'
|
|
69
|
+
function handleRowAction(payload: { action: string, row: LicenseKeyRow }): void {
|
|
70
|
+
emit(payload.action, payload.row)
|
|
50
71
|
}
|
|
51
72
|
</script>
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<template :for="licenseKey in licenseKeys" :key="licenseKey.id">
|
|
72
|
-
<tr class="hover:bg-stone-50/50 dark:hover:bg-neutral-700/50 transition-colors">
|
|
73
|
-
<td class="pl-6 pr-3 py-3.5 font-medium font-mono text-sm text-stone-800 whitespace-nowrap dark:text-white">
|
|
74
|
-
{{ licenseKey.key }}
|
|
75
|
-
</td>
|
|
76
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
77
|
-
{{ licenseKey.template }}
|
|
78
|
-
</td>
|
|
79
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
80
|
-
<span class="block">{{ licenseKey.customer?.label || 'Unassigned' }}</span>
|
|
81
|
-
<span :if="licenseKey.customer?.detail" class="block text-stone-400 text-xs dark:text-neutral-500">
|
|
82
|
-
{{ licenseKey.customer?.detail }}
|
|
83
|
-
</span>
|
|
84
|
-
</td>
|
|
85
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
86
|
-
{{ licenseKey.product?.label || 'Unassigned' }}
|
|
87
|
-
</td>
|
|
88
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
89
|
-
{{ licenseKey.order?.label || 'Unassigned' }}
|
|
90
|
-
</td>
|
|
91
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
92
|
-
{{ formatDate(licenseKey.created_at) }}
|
|
93
|
-
</td>
|
|
94
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
95
|
-
{{ formatDate(licenseKey.expiry_date) }}
|
|
96
|
-
</td>
|
|
97
|
-
<td class="px-4 py-3.5 text-sm whitespace-nowrap">
|
|
98
|
-
<span :class="'inline-flex items-center px-2 py-1 font-medium text-xs ring-1 ring-inset rounded-md ' + statusClass(licenseKey.status)">
|
|
99
|
-
{{ statusLabel(licenseKey.status) }}
|
|
100
|
-
</span>
|
|
101
|
-
</td>
|
|
102
|
-
<td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
|
|
103
|
-
<Button
|
|
104
|
-
variant="ghost"
|
|
105
|
-
size="sm"
|
|
106
|
-
iconOnly
|
|
107
|
-
@click="emit('edit', licenseKey)"
|
|
108
|
-
:ariaLabel="'Edit license key ' + licenseKey.key + ', record ' + licenseKey.id"
|
|
109
|
-
>
|
|
110
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
111
|
-
</Button>
|
|
112
|
-
<Button
|
|
113
|
-
variant="ghost"
|
|
114
|
-
size="sm"
|
|
115
|
-
iconOnly
|
|
116
|
-
@click="emit('delete', licenseKey)"
|
|
117
|
-
:ariaLabel="'Delete license key ' + licenseKey.key + ', record ' + licenseKey.id"
|
|
118
|
-
>
|
|
119
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
120
|
-
</Button>
|
|
121
|
-
</td>
|
|
122
|
-
</tr>
|
|
123
|
-
</template>
|
|
124
|
-
|
|
125
|
-
<tr :if="licenseKeys().length === 0">
|
|
126
|
-
<td colspan="9" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
|
|
127
|
-
No license keys found.
|
|
128
|
-
</td>
|
|
129
|
-
</tr>
|
|
130
|
-
</tbody>
|
|
131
|
-
</table>
|
|
132
|
-
</div>
|
|
74
|
+
{{--
|
|
75
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box.
|
|
76
|
+
Table pages the whole filtered list the parent passes, and stays mounted
|
|
77
|
+
while it loads.
|
|
78
|
+
--}}
|
|
79
|
+
<Table
|
|
80
|
+
:page-reset-key="pageResetKey()"
|
|
81
|
+
:columns="COLUMNS"
|
|
82
|
+
:data="rows()"
|
|
83
|
+
:loading="loading()"
|
|
84
|
+
:row-actions="ROW_ACTIONS"
|
|
85
|
+
action-label-key="actionName"
|
|
86
|
+
paginated
|
|
87
|
+
:page-size="10"
|
|
88
|
+
empty-title="No license keys found"
|
|
89
|
+
empty-description="Add a license key, or adjust the search."
|
|
90
|
+
@row-action="handleRowAction"
|
|
91
|
+
/>
|