@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.
Files changed (57) hide show
  1. package/ide/vscode/package.json +1 -1
  2. package/package.json +3 -3
  3. package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +28 -30
  4. package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +8 -28
  5. package/resources/components/Dashboard/Commerce/CommerceCategoriesTable.stx +72 -29
  6. package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +8 -29
  7. package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +69 -49
  8. package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +8 -27
  9. package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +70 -48
  10. package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +8 -29
  11. package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +72 -82
  12. package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +1 -1
  13. package/resources/components/Dashboard/Commerce/CommerceManufacturersTable.stx +3 -0
  14. package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +8 -28
  15. package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +76 -49
  16. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
  17. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +3 -0
  18. package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +8 -28
  19. package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +81 -34
  20. package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +11 -37
  21. package/resources/components/Dashboard/Commerce/CommerceReviewsTable.stx +106 -77
  22. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +1 -1
  23. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +3 -0
  24. package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +1 -1
  25. package/resources/components/Dashboard/Commerce/CommerceUnitsTable.stx +3 -0
  26. package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +1 -1
  27. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +3 -0
  28. package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +1 -11
  29. package/resources/components/Dashboard/Commerce/Delivery/CouriersTable.stx +55 -82
  30. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +1 -12
  31. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +57 -86
  32. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +1 -24
  33. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryTable.stx +62 -80
  34. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +1 -12
  35. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysTable.stx +58 -99
  36. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +1 -23
  37. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +51 -86
  38. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +1 -12
  39. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +58 -71
  40. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +1 -12
  41. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesTable.stx +85 -110
  42. package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +8 -28
  43. package/resources/components/Dashboard/Commerce/PrintDevicesTable.stx +62 -57
  44. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +7 -24
  45. package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +63 -59
  46. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +7 -25
  47. package/resources/components/Dashboard/Marketing/CampaignsTable.stx +80 -57
  48. package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +7 -27
  49. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +66 -71
  50. package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +7 -25
  51. package/resources/components/Dashboard/Marketing/SocialPostsTable.stx +75 -62
  52. package/resources/components/Dashboard/Monitoring/ErrorDashboard.stx +9 -50
  53. package/resources/components/Dashboard/Monitoring/ErrorTable.stx +68 -97
  54. package/resources/components/Dashboard/Notifications/NotificationDeliveries.stx +22 -44
  55. package/resources/components/Dashboard/Notifications/NotificationDeliveryHistory.stx +13 -4
  56. package/resources/components/Dashboard/Notifications/NotificationDeliveryTable.stx +74 -86
  57. package/resources/components/Dashboard/UI/Table.stx +41 -2
@@ -1,61 +1,83 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { CommerceCustomerRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-customer-records'
3
- import { formatCurrency } from '@stacksjs/commerce/money'
4
5
 
5
6
  const records = useReactiveProp('records', [] as CommerceCustomerRecord[])
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
 
10
- function formatMoney(value: number): string {
11
- return formatCurrency(value, currency())
12
- }
15
+ /** Only the customer cell is slotted, for its initial. */
16
+ const COLUMNS = [
17
+ { key: 'nameLabel', label: 'Customer', sortable: true, slotted: true },
18
+ { key: 'emailLabel', label: 'Contact', sortable: true, subtitleKey: 'phoneLabel', class: 'text-gray-700 dark:text-neutral-200' },
19
+ // Money is integer minor units; the row carries the store currency so the
20
+ // column formats it through formatCurrency, not as a bare number.
21
+ { key: 'totalSpent', label: 'Recorded spend', type: 'currency' as const, currencyKey: 'currency', sortable: true, class: 'font-medium' },
22
+ { key: 'lastOrder', label: 'Last order', type: 'date' as const, sortable: true, emptyText: 'No order recorded', class: 'whitespace-nowrap' },
23
+ {
24
+ key: 'status',
25
+ label: 'Status',
26
+ type: 'badge' as const,
27
+ sortable: true,
28
+ badgeClasses: {
29
+ Active: 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300',
30
+ Inactive: 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300',
31
+ },
32
+ },
33
+ ]
13
34
 
14
- function formatDate(value: string): string {
15
- if (!value)
16
- return 'No order recorded'
17
- const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value)
18
- ? `${value}T00:00:00`
19
- : /^\d{4}-\d{2}-\d{2} \d/.test(value)
20
- ? `${value.replace(' ', 'T')}Z`
21
- : value
22
- const date = new Date(normalized)
23
- return Number.isNaN(date.getTime())
24
- ? value
25
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
26
- }
35
+ const ROW_ACTIONS = [
36
+ { action: 'view', label: 'View', icon: 'i-hugeicons-view' },
37
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-02' },
38
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
39
+ ]
40
+
41
+ const rows = derived(() => records().map(record => ({
42
+ ...record,
43
+ currency: currency(),
44
+ nameLabel: record.name || 'Unnamed customer',
45
+ initial: (record.name || '?').slice(0, 1).toUpperCase(),
46
+ idLabel: `ID ${record.id || 'not recorded'}`,
47
+ emailLabel: record.email || 'No email recorded',
48
+ phoneLabel: record.phone || 'No phone recorded',
49
+ })))
27
50
 
28
- function statusClass(record: CommerceCustomerRecord): string {
29
- if (record.status === 'Active')
30
- return 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300'
31
- if (record.status === 'Inactive')
32
- return 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300'
33
- return 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300'
51
+ function handleRowAction(payload: { action: string, row: CommerceCustomerRecord }): void {
52
+ emit(payload.action, payload.row)
34
53
  }
35
54
  </script>
36
55
 
37
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
38
- <div :if="loading()" class="flex items-center justify-center py-16 text-gray-500 text-sm dark:text-neutral-400"><span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading customers</div>
39
- <div :if="!loading() && records().length === 0" class="py-16 text-center">
40
- <div class="mb-3 mx-auto h-8 w-8 text-gray-400 i-hugeicons-user-group"></div>
41
- <p class="font-medium text-gray-900 text-sm dark:text-white">No customers found</p>
42
- <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Adjust the filters or add a native Customer record.</p>
43
- </div>
44
- <div :if="!loading() && records().length > 0" class="overflow-x-auto">
45
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
46
- <thead class="bg-gray-50 dark:bg-neutral-900/60"><tr><th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Customer</th><th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Contact</th><th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Recorded spend</th><th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Last order</th><th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Status</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead>
47
- <tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
48
- <template :for="record in records()" :key="record.id">
49
- <tr class="hover:bg-gray-50/70 dark:hover:bg-neutral-900/40">
50
- <td class="px-4 py-3"><div class="flex gap-3 items-center"><span class="flex items-center justify-center shrink-0 h-9 w-9 font-semibold text-blue-700 text-sm dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md">{{ (record.name || '?').slice(0, 1).toUpperCase() }}</span><div class="min-w-0"><Button variant="ghost" size="xs" @click="emit('view', record)">{{ record.name || 'Unnamed customer' }}</Button><p class="mt-1 font-mono text-gray-400 text-xs dark:text-neutral-500">ID {{ record.id || 'not recorded' }}</p></div></div></td>
51
- <td class="px-4 py-3"><p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.email || 'No email recorded' }}</p><p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.phone || 'No phone recorded' }}</p></td>
52
- <td class="px-4 py-3 font-medium text-gray-700 text-sm dark:text-neutral-200">{{ formatMoney(record.totalSpent) }}</td>
53
- <td class="px-4 py-3 text-gray-700 text-sm dark:text-neutral-200">{{ formatDate(record.lastOrder) }}</td>
54
- <td class="px-4 py-3"><span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + statusClass(record)">{{ record.status }}</span></td>
55
- <td class="px-4 py-3"><div class="flex gap-1 justify-end"><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><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-02"></span></Button><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-delete-02"></span></Button></div></td>
56
- </tr>
57
- </template>
58
- </tbody>
59
- </table>
60
- </div>
61
- </div>
56
+ {{--
57
+ No `searchable` or `filterable`: the parent dashboard owns the search box
58
+ and the filter selects. Table pages the whole filtered list the parent passes.
59
+ --}}
60
+ <Table
61
+ :sort-reset-key="sortResetKey()"
62
+ :page-reset-key="pageResetKey()"
63
+ :columns="COLUMNS"
64
+ :data="rows()"
65
+ :loading="loading()"
66
+ :row-actions="ROW_ACTIONS"
67
+ action-label-key="nameLabel"
68
+ paginated
69
+ :page-size="15"
70
+ empty-title="No customers found"
71
+ empty-description="Adjust the filters or add a native Customer record."
72
+ @row-action="handleRowAction"
73
+ >
74
+ <template #cell="{ row }">
75
+ <div class="flex gap-3 items-center">
76
+ <span class="flex items-center justify-center shrink-0 h-9 w-9 font-semibold text-blue-700 text-sm dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 rounded-md" x-text="row.initial"></span>
77
+ <div class="min-w-0">
78
+ <p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.nameLabel"></p>
79
+ <p class="mt-1 font-mono text-gray-400 text-xs dark:text-neutral-500" x-text="row.idLabel"></p>
80
+ </div>
81
+ </div>
82
+ </template>
83
+ </Table>
@@ -7,7 +7,6 @@ import type {
7
7
  import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
8
8
  import { pushToast } from '../../../../functions/toasts'
9
9
  import { minorToInput } from '@stacksjs/commerce/money'
10
- import { pageControls } from '../../../../functions/dashboard-list'
11
10
 
12
11
  interface GiftCardIndexResponse {
13
12
  records: GiftCardRecord[]
@@ -49,8 +48,6 @@ const statusFilter = state('all')
49
48
  const enabledFilter = state('all')
50
49
  const deliveryFilter = state('all')
51
50
  const sort = state('newest')
52
- const page = state(1)
53
- const pageSize = 20
54
51
  const loading = state(true)
55
52
  const loadError = state('')
56
53
  const dialogOpen = state(false)
@@ -98,15 +95,6 @@ function filteredRecords(): GiftCardRecord[] {
98
95
  })
99
96
  }
100
97
 
101
- function pageCount(): number {
102
- return Math.max(1, Math.ceil(filteredRecords().length / pageSize))
103
- }
104
-
105
- function visibleRecords(): GiftCardRecord[] {
106
- const selectedPage = Math.min(page(), pageCount())
107
- return filteredRecords().slice((selectedPage - 1) * pageSize, selectedPage * pageSize)
108
- }
109
-
110
98
  function metricCards(): Array<{ label: string, value: string, detail: string }> {
111
99
  const current = summary()
112
100
  return [
@@ -140,14 +128,13 @@ function exportHref(): string {
140
128
  return `data:text/csv;charset=utf-8,${encodeURIComponent(`\uFEFF${csv}`)}`
141
129
  }
142
130
 
143
- effect(() => {
144
- search()
145
- statusFilter()
146
- enabledFilter()
147
- deliveryFilter()
148
- sort()
149
- page.set(1)
150
- })
131
+ /**
132
+ * What the table is showing: a change returns it to the first page, as this
133
+ * page's own pager did before the table took over paging.
134
+ */
135
+ function viewKey(): string {
136
+ return JSON.stringify([search(), statusFilter(), enabledFilter(), deliveryFilter(), sort()])
137
+ }
151
138
 
152
139
  async function loadGiftCards(): Promise<void> {
153
140
  loading.set(true)
@@ -156,7 +143,6 @@ async function loadGiftCards(): Promise<void> {
156
143
  const data = await dashboardApi<GiftCardIndexResponse>('/api/dashboard/commerce/gift-cards')
157
144
  records.set(Array.isArray(data.records) ? data.records : [])
158
145
  summary.set(data.summary ?? { ...emptySummary })
159
- page.set(Math.min(page(), Math.max(1, Math.ceil((data.records?.length || 0) / pageSize))))
160
146
  }
161
147
  catch (error) {
162
148
  loadError.set('Gift cards could not be loaded.')
@@ -258,8 +244,6 @@ async function deleteGiftCard(): Promise<void> {
258
244
  }
259
245
  }
260
246
 
261
- const { previousPage, nextPage } = pageControls(page, pageCount)
262
-
263
247
  onMount(() => {
264
248
  void loadGiftCards()
265
249
  })
@@ -295,14 +279,9 @@ onMount(() => {
295
279
 
296
280
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
297
281
  <p class="text-gray-500 text-sm dark:text-neutral-400">{{ filteredRecords().length }} matching gift cards</p>
298
- <div class="flex gap-2 items-center">
299
- <Button variant="secondary" size="sm" :disabled="page() <= 1" @click="previousPage()">Previous</Button>
300
- <span class="text-gray-500 text-sm dark:text-neutral-400">Page {{ Math.min(page(), pageCount()) }} of {{ pageCount() }}</span>
301
- <Button variant="secondary" size="sm" :disabled="page() >= pageCount()" @click="nextPage()">Next</Button>
302
- </div>
303
282
  </div>
304
283
 
305
- <CommerceGiftCardsTable :records="visibleRecords()" :loading="loading()" @view="openDetails" @edit="openEdit" @delete="openDelete" />
284
+ <CommerceGiftCardsTable :sort-reset-key="sort()" :page-reset-key="viewKey()" :records="filteredRecords()" :loading="loading()" @view="openDetails" @edit="openEdit" @delete="openDelete" />
306
285
  </section>
307
286
  </div>
308
287
 
@@ -1,94 +1,84 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { GiftCardRecord, GiftCardStatus } from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
3
5
  import { formatCurrency } from '@stacksjs/commerce/money'
4
6
 
5
- const { records = [], loading = false } = defineProps<{ records: GiftCardRecord[], loading?: boolean }>()
6
- const emit = defineEmits<{
7
- view: [record: GiftCardRecord]
8
- edit: [record: GiftCardRecord]
9
- delete: [record: GiftCardRecord]
10
- }>()
11
-
12
- function money(value: number, currency: string): string {
13
- return formatCurrency(value, currency)
14
- }
15
-
16
- function date(value: string): string {
17
- if (!value)
18
- return 'No expiry'
19
- const parsed = new Date(value)
20
- return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()
21
- }
7
+ const records = useReactiveProp('records', [] as GiftCardRecord[])
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', '')
11
+ /** The parent's search and filters: a change returns to the first page. */
12
+ const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
13
+ const emit = defineEmits()
22
14
 
23
15
  function statusLabel(status: GiftCardStatus): string {
24
16
  return status.charAt(0) + status.slice(1).toLowerCase()
25
17
  }
26
18
 
27
- function statusClass(status: GiftCardStatus): string {
28
- if (status === 'ACTIVE')
29
- return 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300'
30
- if (status === 'USED')
31
- return 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300'
32
- if (status === 'EXPIRED')
33
- return 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300'
34
- return 'bg-gray-100 text-gray-600 dark:bg-neutral-700 dark:text-neutral-300'
19
+ /**
20
+ * The balance is a currency column keyed on each card's own currency; the
21
+ * initial balance beneath it is formatted here with the same helper, since
22
+ * a subtitle is text.
23
+ */
24
+ const COLUMNS = [
25
+ { key: 'code', label: 'Gift card', sortable: true, subtitleKey: 'currency', class: 'font-medium text-gray-900 dark:text-white' },
26
+ { key: 'recipientLabel', label: 'Recipient', sortable: true, subtitleKey: 'recipientEmailLabel', class: 'text-gray-900 dark:text-white' },
27
+ { key: 'currentBalance', label: 'Balance', type: 'currency' as const, currencyKey: 'currency', sortable: true, subtitleKey: 'initialLabel', class: 'font-medium text-gray-900 dark:text-white' },
28
+ {
29
+ key: 'status',
30
+ label: 'Status',
31
+ type: 'badge' as const,
32
+ sortable: true,
33
+ badgeLabelKey: 'statusLabel',
34
+ subtitleKey: 'enabledLabel',
35
+ badgeClasses: {
36
+ ACTIVE: 'bg-green-50 text-green-700 dark:bg-green-950/40 dark:text-green-300',
37
+ USED: 'bg-blue-50 text-blue-700 dark:bg-blue-950/40 dark:text-blue-300',
38
+ EXPIRED: 'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300',
39
+ },
40
+ },
41
+ { key: 'deliveryLabel', label: 'Delivery', sortable: true, subtitleKey: 'reloadLabel', class: 'text-gray-700 dark:text-neutral-200' },
42
+ { key: 'expiryDate', label: 'Expiry', type: 'date' as const, sortable: true, emptyText: 'No expiry', class: 'whitespace-nowrap' },
43
+ ]
44
+
45
+ const ROW_ACTIONS = [
46
+ { action: 'view', label: 'View', icon: 'i-hugeicons-view' },
47
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-02' },
48
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
49
+ ]
50
+
51
+ const rows = derived(() => records().map(record => ({
52
+ ...record,
53
+ recipientLabel: record.recipientName || 'Unassigned',
54
+ recipientEmailLabel: record.recipientEmail || 'No recipient email',
55
+ initialLabel: `of ${formatCurrency(record.initialBalance, record.currency)}`,
56
+ statusLabel: statusLabel(record.status),
57
+ enabledLabel: record.isActive ? 'Enabled' : 'Disabled',
58
+ deliveryLabel: record.isDigital ? 'Digital' : 'Physical',
59
+ reloadLabel: record.isReloadable ? 'Reloadable' : 'Single use',
60
+ })))
61
+
62
+ function handleRowAction(payload: { action: string, row: GiftCardRecord }): void {
63
+ emit(payload.action, payload.row)
35
64
  }
36
65
  </script>
37
66
 
38
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
39
- <div :if="loading" class="flex items-center justify-center py-16 text-gray-500 text-sm dark:text-neutral-400"><span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading gift cards</div>
40
- <div :if="!loading && records.length === 0" class="py-16 text-center">
41
- <div class="mb-3 mx-auto h-8 w-8 text-gray-400 i-hugeicons-gift"></div>
42
- <p class="font-medium text-gray-900 text-sm dark:text-white">No gift cards found</p>
43
- <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">Adjust the filters or add a native GiftCard record.</p>
44
- </div>
45
- <div :if="!loading && records.length > 0" class="overflow-x-auto">
46
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
47
- <thead class="bg-gray-50 dark:bg-neutral-900/60">
48
- <tr>
49
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Gift card</th>
50
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Recipient</th>
51
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Balance</th>
52
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Status</th>
53
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Delivery</th>
54
- <th class="px-4 py-3 font-semibold text-gray-500 text-left text-xs tracking-wide uppercase dark:text-neutral-400">Expiry</th>
55
- <th class="px-4 py-3"><span class="sr-only">Actions</span></th>
56
- </tr>
57
- </thead>
58
- <tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
59
- <template :for="record in records" :key="record.id">
60
- <tr class="hover:bg-gray-50/70 dark:hover:bg-neutral-900/40">
61
- <td class="px-4 py-3">
62
- <Button variant="ghost" size="xs" @click="emit('view', record)">{{ record.code }}</Button>
63
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.currency }}</p>
64
- </td>
65
- <td class="px-4 py-3">
66
- <p class="text-gray-900 text-sm dark:text-white">{{ record.recipientName || 'Unassigned' }}</p>
67
- <p class="mt-1 max-w-52 text-gray-400 text-xs truncate dark:text-neutral-500">{{ record.recipientEmail || 'No recipient email' }}</p>
68
- </td>
69
- <td class="px-4 py-3">
70
- <p class="font-medium text-gray-900 text-sm dark:text-white">{{ money(record.currentBalance, record.currency) }}</p>
71
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">of {{ money(record.initialBalance, record.currency) }}</p>
72
- </td>
73
- <td class="px-4 py-3">
74
- <span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + statusClass(record.status)">{{ statusLabel(record.status) }}</span>
75
- <p :class="'mt-1 text-xs ' + (record.isActive ? 'text-green-600 dark:text-green-400' : 'text-gray-400 dark:text-neutral-500')">{{ record.isActive ? 'Enabled' : 'Disabled' }}</p>
76
- </td>
77
- <td class="px-4 py-3">
78
- <p class="text-gray-700 text-sm dark:text-neutral-200">{{ record.isDigital ? 'Digital' : 'Physical' }}</p>
79
- <p class="mt-1 text-gray-400 text-xs dark:text-neutral-500">{{ record.isReloadable ? 'Reloadable' : 'Single use' }}</p>
80
- </td>
81
- <td class="px-4 py-3 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ date(record.expiryDate) }}</td>
82
- <td class="px-4 py-3">
83
- <div class="flex gap-1 justify-end">
84
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'View ' + record.code" @click="emit('view', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-view"></span></Button>
85
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.code" @click="emit('edit', record)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button>
86
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.code" @click="emit('delete', record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button>
87
- </div>
88
- </td>
89
- </tr>
90
- </template>
91
- </tbody>
92
- </table>
93
- </div>
94
- </div>
67
+ {{--
68
+ No `searchable` or `filterable`: the parent dashboard owns the search box
69
+ and the filter selects. Table pages the whole filtered list the parent passes.
70
+ --}}
71
+ <Table
72
+ :sort-reset-key="sortResetKey()"
73
+ :page-reset-key="pageResetKey()"
74
+ :columns="COLUMNS"
75
+ :data="rows()"
76
+ :loading="loading()"
77
+ :row-actions="ROW_ACTIONS"
78
+ action-label-key="code"
79
+ paginated
80
+ :page-size="20"
81
+ empty-title="No gift cards found"
82
+ empty-description="Adjust the filters or add a native GiftCard record."
83
+ @row-action="handleRowAction"
84
+ />
@@ -233,7 +233,7 @@ onMount(() => {
233
233
  </label>
234
234
  </div>
235
235
 
236
- <CommerceManufacturersTable :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
236
+ <CommerceManufacturersTable :sort-reset-key="sort()" :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
237
237
  </section>
238
238
  </div>
239
239
 
@@ -5,6 +5,8 @@ import type { ManufacturerRecord } from '../../../../app/Actions/Dashboard/Comme
5
5
 
6
6
  const records = useReactiveProp('records', [] as ManufacturerRecord[])
7
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', '')
8
10
  const emit = defineEmits()
9
11
 
10
12
  /**
@@ -55,6 +57,7 @@ function handleRowAction(payload: { action: string, row: ManufacturerRecord }):
55
57
  duplicate them. Paging is what was missing, and Table brings it.
56
58
  --}}
57
59
  <Table
60
+ :sort-reset-key="sortResetKey()"
58
61
  :columns="COLUMNS"
59
62
  :data="rows()"
60
63
  :loading="loading()"
@@ -7,7 +7,6 @@ import type {
7
7
  import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
8
8
  import { pushToast } from '../../../../functions/toasts'
9
9
  import { formatCurrency, minorToInput } from '@stacksjs/commerce/money'
10
- import { pageControls } from '../../../../functions/dashboard-list'
11
10
 
12
11
  interface OrderIndexResponse {
13
12
  records: CommerceOrderRecord[]
@@ -53,8 +52,6 @@ const search = state('')
53
52
  const statusFilter = state('all')
54
53
  const typeFilter = state('all')
55
54
  const sort = state('newest')
56
- const page = state(1)
57
- const pageSize = 12
58
55
  const loading = state(true)
59
56
  const loadError = state('')
60
57
  const dialogOpen = state(false)
@@ -114,15 +111,6 @@ function filteredRecords(): CommerceOrderRecord[] {
114
111
  })
115
112
  }
116
113
 
117
- function pageCount(): number {
118
- return Math.max(1, Math.ceil(filteredRecords().length / pageSize))
119
- }
120
-
121
- function visibleRecords(): CommerceOrderRecord[] {
122
- const selectedPage = Math.min(page(), pageCount())
123
- return filteredRecords().slice((selectedPage - 1) * pageSize, selectedPage * pageSize)
124
- }
125
-
126
114
  function matchingLabel(): string {
127
115
  const count = filteredRecords().length
128
116
  return `${count} matching ${count === 1 ? 'order' : 'orders'}`
@@ -164,13 +152,13 @@ function exportHref(): string {
164
152
  return `data:text/csv;charset=utf-8,${encodeURIComponent(`\uFEFF${csv}`)}`
165
153
  }
166
154
 
167
- effect(() => {
168
- search()
169
- statusFilter()
170
- typeFilter()
171
- sort()
172
- page.set(1)
173
- })
155
+ /**
156
+ * What the table is showing: a change returns it to the first page, as this
157
+ * page's own pager did before the table took over paging.
158
+ */
159
+ function viewKey(): string {
160
+ return JSON.stringify([search(), statusFilter(), typeFilter(), sort()])
161
+ }
174
162
 
175
163
  async function loadOrders(): Promise<void> {
176
164
  loading.set(true)
@@ -183,7 +171,6 @@ async function loadOrders(): Promise<void> {
183
171
  statuses.set(Array.isArray(data.statuses) ? data.statuses : [])
184
172
  orderTypes.set(Array.isArray(data.orderTypes) ? data.orderTypes : [])
185
173
  defaultCurrency.set(data.defaultCurrency || 'USD')
186
- page.set(Math.min(page(), Math.max(1, Math.ceil((data.records?.length || 0) / pageSize))))
187
174
  }
188
175
  catch (error) {
189
176
  loadError.set('Order records could not be loaded.')
@@ -285,8 +272,6 @@ async function deleteOrder(): Promise<void> {
285
272
  }
286
273
  }
287
274
 
288
- const { previousPage, nextPage } = pageControls(page, pageCount)
289
-
290
275
  onMount(() => {
291
276
  void loadOrders()
292
277
  })
@@ -324,14 +309,9 @@ onMount(() => {
324
309
 
325
310
  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
326
311
  <p class="text-gray-500 text-sm dark:text-neutral-400">{{ matchingLabel() }}</p>
327
- <div class="flex gap-2 items-center">
328
- <Button variant="secondary" size="sm" :disabled="page() <= 1" @click="previousPage()">Previous</Button>
329
- <span class="text-gray-500 text-sm dark:text-neutral-400">Page {{ Math.min(page(), pageCount()) }} of {{ pageCount() }}</span>
330
- <Button variant="secondary" size="sm" :disabled="page() >= pageCount()" @click="nextPage()">Next</Button>
331
- </div>
332
312
  </div>
333
313
 
334
- <CommerceOrdersTable :records="visibleRecords()" :loading="loading()" @view="openDetails" @edit="openEdit" @delete="openDelete" />
314
+ <CommerceOrdersTable :sort-reset-key="sort()" :page-reset-key="viewKey()" :records="filteredRecords()" :loading="loading()" @view="openDetails" @edit="openEdit" @delete="openDelete" />
335
315
  </section>
336
316
  </div>
337
317