@stacksjs/defaults 0.75.277 → 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 +3 -3
- 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,66 +1,93 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { CommerceOrderRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-order-records'
|
|
3
|
-
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
4
5
|
|
|
5
6
|
const records = useReactiveProp('records', [] as CommerceOrderRecord[])
|
|
6
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', '')
|
|
7
12
|
const emit = defineEmits()
|
|
8
13
|
|
|
9
14
|
function label(value: string): string {
|
|
10
15
|
return value.replaceAll('_', ' ').toLowerCase().replace(/\b\w/g, letter => letter.toUpperCase())
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The pill's colour by tone rather than by status, so a status the table
|
|
20
|
+
* has not named still reads as pending, as the hand-rolled pill did.
|
|
21
|
+
*/
|
|
22
|
+
function statusTone(status: string): string {
|
|
23
|
+
const value = status.toUpperCase()
|
|
24
|
+
if (['DELIVERED', 'COMPLETED', 'PAID'].includes(value))
|
|
25
|
+
return 'done'
|
|
26
|
+
if (['CANCELED', 'CANCELLED', 'REFUNDED'].includes(value))
|
|
27
|
+
return 'stopped'
|
|
28
|
+
if (['PREPARING', 'PROCESSING', 'READY', 'SHIPPED'].includes(value))
|
|
29
|
+
return 'moving'
|
|
30
|
+
return 'pending'
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:
|
|
27
|
-
|
|
33
|
+
const COLUMNS = [
|
|
34
|
+
{ key: 'orderLabel', label: 'Order', sortable: true, subtitleKey: 'itemsLabel', class: 'font-medium text-gray-900 whitespace-nowrap dark:text-white' },
|
|
35
|
+
{ key: 'customerLabel', label: 'Customer', sortable: true, subtitleKey: 'customerEmail', class: 'text-gray-700 dark:text-neutral-200' },
|
|
36
|
+
// Integer minor units of the order's own currency.
|
|
37
|
+
{ key: 'totalAmount', label: 'Total', type: 'currency' as const, currencyKey: 'currency', sortable: true, class: 'font-medium' },
|
|
38
|
+
{ key: 'fulfillmentLabel', label: 'Fulfillment', sortable: true, subtitleKey: 'deliveryAddress', class: 'text-gray-700 dark:text-neutral-200' },
|
|
39
|
+
{
|
|
40
|
+
key: 'statusTone',
|
|
41
|
+
label: 'Status',
|
|
42
|
+
type: 'badge' as const,
|
|
43
|
+
sortable: true,
|
|
44
|
+
badgeLabelKey: 'statusLabel',
|
|
45
|
+
badgeClasses: {
|
|
46
|
+
done: 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300',
|
|
47
|
+
stopped: 'bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-300',
|
|
48
|
+
moving: 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300',
|
|
49
|
+
pending: 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const ROW_ACTIONS = [
|
|
56
|
+
{ action: 'view', label: 'View order', icon: 'i-hugeicons-view' },
|
|
57
|
+
{ action: 'edit', label: 'Edit order', icon: 'i-hugeicons-edit-02' },
|
|
58
|
+
{ action: 'delete', label: 'Delete order', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const rows = derived(() => records().map(record => ({
|
|
62
|
+
...record,
|
|
63
|
+
orderLabel: `ORD-${record.id.padStart(5, '0')}`,
|
|
64
|
+
itemsLabel: `${record.itemCount} ${record.itemCount === 1 ? 'item' : 'items'}`,
|
|
65
|
+
customerLabel: record.customerName || 'Unassigned customer',
|
|
66
|
+
fulfillmentLabel: label(record.orderType),
|
|
67
|
+
statusTone: statusTone(record.status),
|
|
68
|
+
statusLabel: label(record.status),
|
|
69
|
+
})))
|
|
28
70
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
if (['DELIVERED', 'COMPLETED', 'PAID'].includes(status))
|
|
32
|
-
return 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300'
|
|
33
|
-
if (['CANCELED', 'CANCELLED', 'REFUNDED'].includes(status))
|
|
34
|
-
return 'bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-300'
|
|
35
|
-
if (['PREPARING', 'PROCESSING', 'READY', 'SHIPPED'].includes(status))
|
|
36
|
-
return 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300'
|
|
37
|
-
return 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300'
|
|
71
|
+
function handleRowAction(payload: { action: string, row: CommerceOrderRecord }): void {
|
|
72
|
+
emit(payload.action, payload.row)
|
|
38
73
|
}
|
|
39
74
|
</script>
|
|
40
75
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<td class="px-4 py-3 text-gray-700 text-sm whitespace-nowrap dark:text-neutral-200">{{ formatDate(record.createdAt) }}</td>
|
|
60
|
-
<td class="px-4 py-3"><div class="flex gap-1 justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'View order ' + record.id" @click="emit('view', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-view"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit order ' + record.id" @click="emit('edit', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete order ' + record.id" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td>
|
|
61
|
-
</tr>
|
|
62
|
-
</template>
|
|
63
|
-
</tbody>
|
|
64
|
-
</table>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
76
|
+
{{--
|
|
77
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
78
|
+
and the filter selects. Table pages the whole filtered list the parent passes.
|
|
79
|
+
--}}
|
|
80
|
+
<Table
|
|
81
|
+
:sort-reset-key="sortResetKey()"
|
|
82
|
+
:page-reset-key="pageResetKey()"
|
|
83
|
+
:columns="COLUMNS"
|
|
84
|
+
:data="rows()"
|
|
85
|
+
:loading="loading()"
|
|
86
|
+
:row-actions="ROW_ACTIONS"
|
|
87
|
+
action-label-key="id"
|
|
88
|
+
paginated
|
|
89
|
+
:page-size="12"
|
|
90
|
+
empty-title="No orders found"
|
|
91
|
+
empty-description="Adjust the filters or add a native Order record."
|
|
92
|
+
@row-action="handleRowAction"
|
|
93
|
+
/>
|
|
@@ -200,7 +200,7 @@ onMount(() => {
|
|
|
200
200
|
<label class="flex gap-2 items-center text-gray-500 text-sm dark:text-neutral-400">Sort<select x-model="sort" class="px-3 py-1.5 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="newest">Newest</option><option value="oldest">Oldest</option><option value="amount">Highest amount</option><option value="status">Status</option></select></label>
|
|
201
201
|
</div>
|
|
202
202
|
|
|
203
|
-
<CommercePaymentsTable :records="visibleRecords()" :loading="loading()" @view="openDetails" />
|
|
203
|
+
<CommercePaymentsTable :sort-reset-key="sort()" :records="visibleRecords()" :loading="loading()" @view="openDetails" />
|
|
204
204
|
</section>
|
|
205
205
|
</div>
|
|
206
206
|
|
|
@@ -6,6 +6,8 @@ import { formatCurrency } from '@stacksjs/commerce/money'
|
|
|
6
6
|
|
|
7
7
|
const records = useReactiveProp('records', [] as PaymentRecord[])
|
|
8
8
|
const loading = useReactiveProp('loading', false)
|
|
9
|
+
/** The parent's sort select; a change hands the order back from a header sort. */
|
|
10
|
+
const sortResetKey = useReactiveProp<unknown>('sortResetKey', '')
|
|
9
11
|
const emit = defineEmits()
|
|
10
12
|
|
|
11
13
|
/** `partiallyRefunded` and `bank_transfer` as words, each capitalized. */
|
|
@@ -83,6 +85,7 @@ function handleRowAction(payload: { action: string, row: PaymentRecord }): void
|
|
|
83
85
|
</script>
|
|
84
86
|
|
|
85
87
|
<Table
|
|
88
|
+
:sort-reset-key="sortResetKey()"
|
|
86
89
|
:columns="COLUMNS"
|
|
87
90
|
:data="rows()"
|
|
88
91
|
:loading="loading()"
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
9
9
|
import { pushToast } from '../../../../functions/toasts'
|
|
10
10
|
import { minorToInput } from '@stacksjs/commerce/money'
|
|
11
|
-
import { pageControls } from '../../../../functions/dashboard-list'
|
|
12
11
|
|
|
13
12
|
interface ProductIndexResponse {
|
|
14
13
|
records: CommerceProductRecord[]
|
|
@@ -34,8 +33,6 @@ const search = state('')
|
|
|
34
33
|
const categoryFilter = state('all')
|
|
35
34
|
const availabilityFilter = state('all')
|
|
36
35
|
const sort = state('newest')
|
|
37
|
-
const page = state(1)
|
|
38
|
-
const pageSize = 10
|
|
39
36
|
const loading = state(true)
|
|
40
37
|
const loadError = state('')
|
|
41
38
|
const dialogOpen = state(false)
|
|
@@ -94,15 +91,6 @@ function filteredRecords(): CommerceProductRecord[] {
|
|
|
94
91
|
})
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
function pageCount(): number {
|
|
98
|
-
return Math.max(1, Math.ceil(filteredRecords().length / pageSize))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function visibleRecords(): CommerceProductRecord[] {
|
|
102
|
-
const selectedPage = Math.min(page(), pageCount())
|
|
103
|
-
return filteredRecords().slice((selectedPage - 1) * pageSize, selectedPage * pageSize)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
94
|
function matchingLabel(): string {
|
|
107
95
|
const count = filteredRecords().length
|
|
108
96
|
return `${count} matching ${count === 1 ? 'product' : 'products'}`
|
|
@@ -146,13 +134,13 @@ function exportHref(): string {
|
|
|
146
134
|
return `data:text/csv;charset=utf-8,${encodeURIComponent(`\uFEFF${csv}`)}`
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
137
|
+
/**
|
|
138
|
+
* What the table is showing: a change returns it to the first page, as this
|
|
139
|
+
* page's own pager did before the table took over paging.
|
|
140
|
+
*/
|
|
141
|
+
function viewKey(): string {
|
|
142
|
+
return JSON.stringify([search(), categoryFilter(), availabilityFilter(), sort()])
|
|
143
|
+
}
|
|
156
144
|
|
|
157
145
|
async function loadProducts(): Promise<void> {
|
|
158
146
|
loading.set(true)
|
|
@@ -164,7 +152,6 @@ async function loadProducts(): Promise<void> {
|
|
|
164
152
|
categories.set(Array.isArray(data.categories) ? data.categories : [])
|
|
165
153
|
manufacturers.set(Array.isArray(data.manufacturers) ? data.manufacturers : [])
|
|
166
154
|
defaultCurrency.set(data.defaultCurrency || 'USD')
|
|
167
|
-
page.set(Math.min(page(), Math.max(1, Math.ceil((data.records?.length || 0) / pageSize))))
|
|
168
155
|
}
|
|
169
156
|
catch (error) {
|
|
170
157
|
loadError.set('Product records could not be loaded.')
|
|
@@ -260,8 +247,6 @@ async function deleteProduct(): Promise<void> {
|
|
|
260
247
|
}
|
|
261
248
|
}
|
|
262
249
|
|
|
263
|
-
const { previousPage, nextPage } = pageControls(page, pageCount)
|
|
264
|
-
|
|
265
250
|
onMount(() => {
|
|
266
251
|
void loadProducts()
|
|
267
252
|
})
|
|
@@ -294,14 +279,9 @@ onMount(() => {
|
|
|
294
279
|
|
|
295
280
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
296
281
|
<p class="text-gray-500 text-sm dark:text-neutral-400">{{ matchingLabel() }}</p>
|
|
297
|
-
<div class="flex gap-2 items-center">
|
|
298
|
-
<Button variant="secondary" size="sm" :disabled="page() <= 1" @click="previousPage()">Previous</Button>
|
|
299
|
-
<span class="text-gray-500 text-sm dark:text-neutral-400">Page {{ Math.min(page(), pageCount()) }} of {{ pageCount() }}</span>
|
|
300
|
-
<Button variant="secondary" size="sm" :disabled="page() >= pageCount()" @click="nextPage()">Next</Button>
|
|
301
|
-
</div>
|
|
302
282
|
</div>
|
|
303
283
|
|
|
304
|
-
<CommerceProductsTable :records="
|
|
284
|
+
<CommerceProductsTable :sort-reset-key="sort()" :page-reset-key="viewKey()" :records="filteredRecords()" :loading="loading()" :currency="defaultCurrency()" @view="viewProduct" @edit="openEdit" @delete="openDelete" />
|
|
305
285
|
</section>
|
|
306
286
|
</div>
|
|
307
287
|
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { CommerceProductRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
3
|
-
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
4
5
|
|
|
5
6
|
const records = useReactiveProp('records', [] as CommerceProductRecord[])
|
|
6
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', '')
|
|
7
12
|
const currency = useReactiveProp('currency', 'USD')
|
|
8
13
|
const emit = defineEmits()
|
|
9
14
|
|
|
@@ -11,40 +16,82 @@ function initials(name: string): string {
|
|
|
11
16
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map(part => part[0]?.toUpperCase() || '').join('') || 'PR'
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The product cell is slotted for its initials, and inventory for its colour:
|
|
21
|
+
* none in stock reads red. Everything else is a column option.
|
|
22
|
+
*/
|
|
23
|
+
const COLUMNS = [
|
|
24
|
+
{ key: 'nameLabel', label: 'Product', sortable: true, slotted: true },
|
|
25
|
+
{ key: 'categoryName', label: 'Relationships', sortable: true, subtitleKey: 'manufacturerName', class: 'text-gray-700 dark:text-neutral-200' },
|
|
26
|
+
{ key: 'price', label: 'Price', type: 'currency' as const, currencyKey: 'currency', sortable: true, class: 'font-medium whitespace-nowrap' },
|
|
27
|
+
{ key: 'inventoryCount', label: 'Inventory', sortable: true, slotted: true },
|
|
28
|
+
{
|
|
29
|
+
key: 'availability',
|
|
30
|
+
label: 'Availability',
|
|
31
|
+
type: 'badge' as const,
|
|
32
|
+
sortable: true,
|
|
33
|
+
badgeClasses: { available: 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300' },
|
|
34
|
+
badgeLabels: { available: 'Available', unavailable: 'Unavailable' },
|
|
35
|
+
},
|
|
36
|
+
{ key: 'variantsLabel', label: 'Related', subtitleKey: 'reviewsLabel', searchable: false, class: 'text-gray-500 text-xs dark:text-neutral-400' },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const ROW_ACTIONS = [
|
|
40
|
+
{ action: 'view', label: 'View product', icon: 'i-hugeicons-view' },
|
|
41
|
+
{ action: 'edit', label: 'Edit product', icon: 'i-hugeicons-edit-02' },
|
|
42
|
+
{ action: 'delete', label: 'Delete product', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const rows = derived(() => records().map(record => ({
|
|
46
|
+
...record,
|
|
47
|
+
currency: currency(),
|
|
48
|
+
nameLabel: record.name || 'Unnamed product',
|
|
49
|
+
initials: initials(record.name),
|
|
50
|
+
descriptionLabel: record.description || 'No description',
|
|
51
|
+
outOfStock: record.inventoryCount === 0,
|
|
52
|
+
inventoryLabel: record.inventoryCount.toLocaleString(),
|
|
53
|
+
prepLabel: `${record.preparationTime} min prep`,
|
|
54
|
+
availability: record.isAvailable ? 'available' : 'unavailable',
|
|
55
|
+
variantsLabel: `${record.variantCount} variants, ${record.unitCount} units`,
|
|
56
|
+
reviewsLabel: `${record.reviewCount} reviews`,
|
|
57
|
+
})))
|
|
17
58
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
? 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300'
|
|
21
|
-
: 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300'
|
|
59
|
+
function handleRowAction(payload: { action: string, row: CommerceProductRecord }): void {
|
|
60
|
+
emit(payload.action, payload.row)
|
|
22
61
|
}
|
|
23
62
|
</script>
|
|
24
63
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</div>
|
|
64
|
+
{{--
|
|
65
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
66
|
+
and the filter selects. Table pages the whole filtered list the parent passes.
|
|
67
|
+
--}}
|
|
68
|
+
<Table
|
|
69
|
+
:sort-reset-key="sortResetKey()"
|
|
70
|
+
:page-reset-key="pageResetKey()"
|
|
71
|
+
:columns="COLUMNS"
|
|
72
|
+
:data="rows()"
|
|
73
|
+
:loading="loading()"
|
|
74
|
+
:row-actions="ROW_ACTIONS"
|
|
75
|
+
action-label-key="nameLabel"
|
|
76
|
+
paginated
|
|
77
|
+
:page-size="10"
|
|
78
|
+
empty-title="No products found"
|
|
79
|
+
empty-description="Adjust the filters or add a native Product record."
|
|
80
|
+
@row-action="handleRowAction"
|
|
81
|
+
>
|
|
82
|
+
<template #cell="{ row, column }">
|
|
83
|
+
<template :if="column.key === 'nameLabel'">
|
|
84
|
+
<div class="flex gap-3 items-center">
|
|
85
|
+
<div class="flex flex-none items-center justify-center h-9 w-9 font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md" x-text="row.initials"></div>
|
|
86
|
+
<div class="min-w-0">
|
|
87
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.nameLabel"></p>
|
|
88
|
+
<p class="mt-1 max-w-52 text-gray-400 text-xs truncate dark:text-neutral-500" x-text="row.descriptionLabel"></p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
<template :if="column.key === 'inventoryCount'">
|
|
93
|
+
<p :class="'font-medium text-sm ' + (row.outOfStock ? 'text-red-600 dark:text-red-400' : 'text-gray-700 dark:text-neutral-200')" x-text="row.inventoryLabel"></p>
|
|
94
|
+
<p class="mt-1 text-gray-400 text-xs dark:text-neutral-500" x-text="row.prepLabel"></p>
|
|
95
|
+
</template>
|
|
96
|
+
</template>
|
|
97
|
+
</Table>
|
|
@@ -43,8 +43,6 @@ const productFilter = state('all')
|
|
|
43
43
|
const ratingFilter = state('all')
|
|
44
44
|
const moderationFilter = state('all')
|
|
45
45
|
const sort = state('newest')
|
|
46
|
-
const page = state(1)
|
|
47
|
-
const pageSize = 12
|
|
48
46
|
const loading = state(true)
|
|
49
47
|
const loadError = state('')
|
|
50
48
|
const editingReview = state<ReviewRecord | null>(null)
|
|
@@ -109,24 +107,6 @@ function filteredRecords(): ReviewRecord[] {
|
|
|
109
107
|
})
|
|
110
108
|
}
|
|
111
109
|
|
|
112
|
-
function totalPages(): number {
|
|
113
|
-
return Math.max(1, Math.ceil(filteredRecords().length / pageSize))
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function visibleRecords(): ReviewRecord[] {
|
|
117
|
-
const safePage = Math.min(page(), totalPages())
|
|
118
|
-
const offset = (safePage - 1) * pageSize
|
|
119
|
-
return filteredRecords().slice(offset, offset + pageSize)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function rangeStart(): number {
|
|
123
|
-
return filteredRecords().length === 0 ? 0 : (Math.min(page(), totalPages()) - 1) * pageSize + 1
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function rangeEnd(): number {
|
|
127
|
-
return Math.min(Math.min(page(), totalPages()) * pageSize, filteredRecords().length)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
110
|
function metricCards(): Array<{ label: string, value: string }> {
|
|
131
111
|
return [
|
|
132
112
|
{ label: 'Recorded', value: String(summary().total) },
|
|
@@ -137,14 +117,13 @@ function metricCards(): Array<{ label: string, value: string }> {
|
|
|
137
117
|
]
|
|
138
118
|
}
|
|
139
119
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
sort()
|
|
146
|
-
|
|
147
|
-
})
|
|
120
|
+
/**
|
|
121
|
+
* What the table is showing: a change returns it to the first page, as this
|
|
122
|
+
* page's own pager did before the table took over paging.
|
|
123
|
+
*/
|
|
124
|
+
function viewKey(): string {
|
|
125
|
+
return JSON.stringify([search(), productFilter(), ratingFilter(), moderationFilter(), sort()])
|
|
126
|
+
}
|
|
148
127
|
|
|
149
128
|
async function loadReviews(): Promise<void> {
|
|
150
129
|
loading.set(true)
|
|
@@ -154,8 +133,6 @@ async function loadReviews(): Promise<void> {
|
|
|
154
133
|
records.set(Array.isArray(data.records) ? data.records : [])
|
|
155
134
|
summary.set(data.summary ?? { ...emptySummary })
|
|
156
135
|
products.set(Array.isArray(data.products) ? data.products : [])
|
|
157
|
-
if (page() > totalPages())
|
|
158
|
-
page.set(totalPages())
|
|
159
136
|
}
|
|
160
137
|
catch (error) {
|
|
161
138
|
loadError.set('Review records could not be loaded.')
|
|
@@ -335,7 +312,7 @@ onMount(() => {
|
|
|
335
312
|
|
|
336
313
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
337
314
|
<p class="text-gray-500 text-sm dark:text-neutral-400">
|
|
338
|
-
|
|
315
|
+
{{ filteredRecords().length }} matching {{ filteredRecords().length === 1 ? 'review' : 'reviews' }}
|
|
339
316
|
</p>
|
|
340
317
|
<label class="flex gap-2 items-center text-gray-500 text-sm dark:text-neutral-400">
|
|
341
318
|
Sort
|
|
@@ -350,7 +327,9 @@ onMount(() => {
|
|
|
350
327
|
</div>
|
|
351
328
|
|
|
352
329
|
<CommerceReviewsTable
|
|
353
|
-
:
|
|
330
|
+
:sort-reset-key="sort()"
|
|
331
|
+
:page-reset-key="viewKey()"
|
|
332
|
+
:reviews="filteredRecords()"
|
|
354
333
|
:loading="loading()"
|
|
355
334
|
:busy-id="busyId()"
|
|
356
335
|
@approve="toggleApproval"
|
|
@@ -359,11 +338,6 @@ onMount(() => {
|
|
|
359
338
|
@delete="openDelete"
|
|
360
339
|
/>
|
|
361
340
|
|
|
362
|
-
<nav :if="totalPages() > 1" aria-label="Review pages" class="flex items-center justify-between">
|
|
363
|
-
<Button variant="secondary" :disabled="page() <= 1" @click="page.update(value => value - 1)">Previous</Button>
|
|
364
|
-
<span class="text-gray-500 text-sm dark:text-neutral-400">Page {{ page() }} of {{ totalPages() }}</span>
|
|
365
|
-
<Button variant="secondary" :disabled="page() >= totalPages()" @click="page.update(value => value + 1)">Next</Button>
|
|
366
|
-
</nav>
|
|
367
341
|
</section>
|
|
368
342
|
</div>
|
|
369
343
|
|