@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,94 +1,123 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import type { ReviewRecord } from '../../../../app/Actions/Dashboard/Commerce/review-records'
|
|
5
|
+
import { parseDashboardDate } from '../../../../functions/dashboard-dates'
|
|
3
6
|
|
|
4
7
|
const reviews = useReactiveProp('reviews', [] as ReviewRecord[])
|
|
5
8
|
const loading = useReactiveProp('loading', false)
|
|
6
9
|
const busyId = useReactiveProp('busyId', '')
|
|
10
|
+
/** The parent's sort select: a change hands the order back from a header sort. */
|
|
11
|
+
const sortResetKey = useReactiveProp<unknown>('sortResetKey', '')
|
|
12
|
+
/** The parent's search and filters: a change returns to the first page. */
|
|
13
|
+
const pageResetKey = useReactiveProp<unknown>('pageResetKey', '')
|
|
7
14
|
const emit = defineEmits()
|
|
8
15
|
|
|
16
|
+
/** Epoch seconds or milliseconds as well as stored timestamps, as the API sends either. */
|
|
9
17
|
function formatDate(value: string): string {
|
|
10
18
|
if (!value)
|
|
11
19
|
return 'Not recorded'
|
|
12
|
-
const
|
|
13
|
-
? Number(value) * (value.length === 10 ? 1000 : 1)
|
|
14
|
-
:
|
|
15
|
-
const normalized = /^\d{4}-\d{2}-\d{2} \d/.test(value)
|
|
16
|
-
? `${value.replace(' ', 'T')}Z`
|
|
17
|
-
: value
|
|
18
|
-
const date = new Date(Number.isFinite(numeric) ? numeric : normalized)
|
|
20
|
+
const date = /^\d{10,13}$/.test(value)
|
|
21
|
+
? new Date(Number(value) * (value.length === 10 ? 1000 : 1))
|
|
22
|
+
: parseDashboardDate(value)
|
|
19
23
|
return Number.isNaN(date.getTime())
|
|
20
24
|
? value
|
|
21
25
|
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
|
|
22
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Customer and review are slotted: the verified badge, the stars and the
|
|
30
|
+
* vote counts are layout a column option cannot express. Approve and feature
|
|
31
|
+
* each have two declared actions, one per state, so a row offers the one
|
|
32
|
+
* that applies.
|
|
33
|
+
*/
|
|
34
|
+
const COLUMNS = [
|
|
35
|
+
{ key: 'customerLabel', label: 'Customer', sortable: true, slotted: true },
|
|
36
|
+
{ key: 'productLabel', label: 'Product', sortable: true, class: 'text-gray-700 dark:text-neutral-200' },
|
|
37
|
+
{ key: 'rating', label: 'Review', sortable: true, slotted: true },
|
|
38
|
+
{
|
|
39
|
+
key: 'moderation',
|
|
40
|
+
label: 'State',
|
|
41
|
+
type: 'badge' as const,
|
|
42
|
+
sortable: true,
|
|
43
|
+
subtitleKey: 'featuredLabel',
|
|
44
|
+
badgeClasses: {
|
|
45
|
+
approved: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30',
|
|
46
|
+
pending: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30',
|
|
47
|
+
},
|
|
48
|
+
badgeLabels: { approved: 'Approved', pending: 'Pending' },
|
|
49
|
+
},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const ROW_ACTIONS = [
|
|
53
|
+
{ action: 'approve', label: 'Approve review', icon: 'i-hugeicons-checkmark-circle-02', class: 'text-green-500', when: { key: 'approved', equals: false } },
|
|
54
|
+
{ action: 'approve', label: 'Return review to pending', icon: 'i-hugeicons-cancel-circle', class: 'text-green-500', when: { key: 'approved', equals: true } },
|
|
55
|
+
{ action: 'feature', label: 'Feature review', icon: 'i-hugeicons-star', class: 'text-amber-500', when: { key: 'featured', equals: false } },
|
|
56
|
+
{ action: 'feature', label: 'Remove review from featured', icon: 'i-hugeicons-star-off', class: 'text-amber-500', when: { key: 'featured', equals: true } },
|
|
57
|
+
{ action: 'edit', label: 'Edit review', icon: 'i-hugeicons-edit-01' },
|
|
58
|
+
{ action: 'delete', label: 'Delete review', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const rows = derived(() => reviews().map(review => ({
|
|
62
|
+
...review,
|
|
63
|
+
customerLabel: review.customerName || 'Guest customer',
|
|
64
|
+
productLabel: review.productName || 'Unlinked product',
|
|
65
|
+
titleLabel: review.title || 'Untitled review',
|
|
66
|
+
contentLabel: review.content || 'No review content recorded.',
|
|
67
|
+
stars: `${'★'.repeat(review.rating)}${'☆'.repeat(5 - review.rating)}`,
|
|
68
|
+
starsLabel: `${review.rating} out of 5 stars`,
|
|
69
|
+
helpfulLabel: `${review.helpfulVotes} helpful`,
|
|
70
|
+
unhelpfulLabel: `${review.unhelpfulVotes} not helpful`,
|
|
71
|
+
dateLabel: formatDate(review.createdAt),
|
|
72
|
+
moderation: review.approved ? 'approved' : 'pending',
|
|
73
|
+
featuredLabel: review.featured ? 'Featured' : '',
|
|
74
|
+
})))
|
|
75
|
+
|
|
76
|
+
function handleRowAction(payload: { action: string, row: ReviewRecord }): void {
|
|
77
|
+
emit(payload.action, payload.row)
|
|
78
|
+
}
|
|
23
79
|
</script>
|
|
24
80
|
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + (review.approved ? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/30' : 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/30')">
|
|
69
|
-
{{ review.approved ? 'Approved' : 'Pending' }}
|
|
70
|
-
</span>
|
|
71
|
-
<span :if="review.featured" class="block mt-2 font-medium text-blue-700 text-xs dark:text-blue-300">Featured</span>
|
|
72
|
-
</td>
|
|
73
|
-
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
74
|
-
<div class="flex gap-1 justify-end">
|
|
75
|
-
<Button variant="ghost" size="sm" iconOnly :disabled="busyId() === review.id" :ariaLabel="review.approved ? 'Return review to pending' : 'Approve review'" @click="emit('approve', review)">
|
|
76
|
-
<span :class="'block h-4 w-4 text-green-500 ' + (review.approved ? 'i-hugeicons-cancel-circle' : 'i-hugeicons-checkmark-circle-02')"></span>
|
|
77
|
-
</Button>
|
|
78
|
-
<Button variant="ghost" size="sm" iconOnly :disabled="busyId() === review.id" :ariaLabel="review.featured ? 'Remove review from featured' : 'Feature review'" @click="emit('feature', review)">
|
|
79
|
-
<span :class="'block h-4 w-4 text-amber-500 ' + (review.featured ? 'i-hugeicons-star-off' : 'i-hugeicons-star')"></span>
|
|
80
|
-
</Button>
|
|
81
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit review ' + review.id" @click="emit('edit', review)">
|
|
82
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
83
|
-
</Button>
|
|
84
|
-
<Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete review ' + review.id" @click="emit('delete', review)">
|
|
85
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
86
|
-
</Button>
|
|
87
|
-
</div>
|
|
88
|
-
</td>
|
|
89
|
-
</tr>
|
|
90
|
-
</template>
|
|
91
|
-
</tbody>
|
|
92
|
-
</table>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
81
|
+
{{--
|
|
82
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box
|
|
83
|
+
and the filter selects. Table pages the whole filtered list the parent passes.
|
|
84
|
+
--}}
|
|
85
|
+
<Table
|
|
86
|
+
:sort-reset-key="sortResetKey()"
|
|
87
|
+
:page-reset-key="pageResetKey()"
|
|
88
|
+
:columns="COLUMNS"
|
|
89
|
+
:data="rows()"
|
|
90
|
+
:loading="loading()"
|
|
91
|
+
:row-actions="ROW_ACTIONS"
|
|
92
|
+
action-label-key="id"
|
|
93
|
+
busy-key="id"
|
|
94
|
+
:busy-value="busyId()"
|
|
95
|
+
paginated
|
|
96
|
+
:page-size="12"
|
|
97
|
+
empty-title="No reviews found"
|
|
98
|
+
empty-description="Adjust the current filters or wait for a customer review."
|
|
99
|
+
@row-action="handleRowAction"
|
|
100
|
+
>
|
|
101
|
+
<template #cell="{ row, column }">
|
|
102
|
+
<template :if="column.key === 'customerLabel'">
|
|
103
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.customerLabel"></p>
|
|
104
|
+
<p :if="row.customerEmail" class="mt-1 text-gray-500 text-xs dark:text-neutral-400" x-text="row.customerEmail"></p>
|
|
105
|
+
<span :if="row.verifiedPurchase" class="inline-flex items-center mt-2 font-medium text-green-700 text-xs dark:text-green-300">
|
|
106
|
+
<span class="mr-1 h-3.5 w-3.5 i-hugeicons-checkmark-badge-01"></span>
|
|
107
|
+
Verified purchase
|
|
108
|
+
</span>
|
|
109
|
+
</template>
|
|
110
|
+
<template :if="column.key === 'rating'">
|
|
111
|
+
<div class="max-w-lg min-w-72">
|
|
112
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white" x-text="row.titleLabel"></p>
|
|
113
|
+
<p class="mt-1 line-clamp-2 text-gray-500 text-sm dark:text-neutral-400" x-text="row.contentLabel"></p>
|
|
114
|
+
<div class="flex flex-wrap gap-x-3 gap-y-1 items-center mt-2 text-gray-400 text-xs dark:text-neutral-500">
|
|
115
|
+
<span :aria-label="row.starsLabel" class="text-amber-500 text-sm" x-text="row.stars"></span>
|
|
116
|
+
<span x-text="row.helpfulLabel"></span>
|
|
117
|
+
<span x-text="row.unhelpfulLabel"></span>
|
|
118
|
+
<span x-text="row.dateLabel"></span>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
122
|
+
</template>
|
|
123
|
+
</Table>
|
|
@@ -268,7 +268,7 @@ onMount(() => {
|
|
|
268
268
|
</label>
|
|
269
269
|
</div>
|
|
270
270
|
|
|
271
|
-
<CommerceTaxesTable :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
271
|
+
<CommerceTaxesTable :sort-reset-key="sort()" :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
272
272
|
</section>
|
|
273
273
|
</div>
|
|
274
274
|
|
|
@@ -5,6 +5,8 @@ import type { TaxRateRecord } from '../../../../app/Actions/Dashboard/Commerce/t
|
|
|
5
5
|
|
|
6
6
|
const records = useReactiveProp('records', [] as TaxRateRecord[])
|
|
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
|
/**
|
|
@@ -50,6 +52,7 @@ function handleRowAction(payload: { action: string, row: TaxRateRecord }): void
|
|
|
50
52
|
and the filter selects. Paging is what was missing, and Table brings it.
|
|
51
53
|
--}}
|
|
52
54
|
<Table
|
|
55
|
+
:sort-reset-key="sortResetKey()"
|
|
53
56
|
:columns="COLUMNS"
|
|
54
57
|
:data="rows()"
|
|
55
58
|
:loading="loading()"
|
|
@@ -265,7 +265,7 @@ onMount(() => {
|
|
|
265
265
|
</label>
|
|
266
266
|
</div>
|
|
267
267
|
|
|
268
|
-
<CommerceUnitsTable :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
268
|
+
<CommerceUnitsTable :sort-reset-key="sort()" :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
269
269
|
</section>
|
|
270
270
|
</div>
|
|
271
271
|
|
|
@@ -5,6 +5,8 @@ import type { ProductUnitRecord } from '../../../../app/Actions/Dashboard/Commer
|
|
|
5
5
|
|
|
6
6
|
const records = useReactiveProp('records', [] as ProductUnitRecord[])
|
|
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
|
/**
|
|
@@ -56,6 +58,7 @@ function handleRowAction(payload: { action: string, row: ProductUnitRecord }): v
|
|
|
56
58
|
duplicate them. Paging is what was missing, and Table brings it.
|
|
57
59
|
--}}
|
|
58
60
|
<Table
|
|
61
|
+
:sort-reset-key="sortResetKey()"
|
|
59
62
|
:columns="COLUMNS"
|
|
60
63
|
:data="rows()"
|
|
61
64
|
:loading="loading()"
|
|
@@ -226,7 +226,7 @@ onMount(() => {
|
|
|
226
226
|
<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="variant">Name</option><option value="options">Most options</option><option value="product">Product</option><option value="newest">Newest</option></select></label>
|
|
227
227
|
</div>
|
|
228
228
|
|
|
229
|
-
<CommerceVariantsTable :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
229
|
+
<CommerceVariantsTable :sort-reset-key="sort()" :records="visibleRecords()" :loading="loading()" @edit="openEdit" @delete="openDelete" />
|
|
230
230
|
</section>
|
|
231
231
|
</div>
|
|
232
232
|
|
|
@@ -5,6 +5,8 @@ import type { ProductVariantRecord } from '../../../../app/Actions/Dashboard/Com
|
|
|
5
5
|
|
|
6
6
|
const records = useReactiveProp('records', [] as ProductVariantRecord[])
|
|
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
|
/**
|
|
@@ -48,6 +50,7 @@ function handleRowAction(payload: { action: string, row: ProductVariantRecord })
|
|
|
48
50
|
</script>
|
|
49
51
|
|
|
50
52
|
<Table
|
|
53
|
+
:sort-reset-key="sortResetKey()"
|
|
51
54
|
:columns="COLUMNS"
|
|
52
55
|
:data="rows()"
|
|
53
56
|
:loading="loading()"
|
|
@@ -52,8 +52,6 @@ const {
|
|
|
52
52
|
loading,
|
|
53
53
|
loadError,
|
|
54
54
|
search,
|
|
55
|
-
page,
|
|
56
|
-
perPage,
|
|
57
55
|
editorOpen,
|
|
58
56
|
editorMode,
|
|
59
57
|
editing,
|
|
@@ -63,8 +61,6 @@ const {
|
|
|
63
61
|
deleteBusy,
|
|
64
62
|
deleteError,
|
|
65
63
|
filteredRecords,
|
|
66
|
-
paginatedRecords,
|
|
67
|
-
goToPage,
|
|
68
64
|
handleSearch,
|
|
69
65
|
openCreate,
|
|
70
66
|
openEdit,
|
|
@@ -73,7 +69,6 @@ const {
|
|
|
73
69
|
closeDelete,
|
|
74
70
|
upsert,
|
|
75
71
|
remove,
|
|
76
|
-
clampPage,
|
|
77
72
|
} = useRecordsDashboard<CourierRecord>({ filter: matchesQuery })
|
|
78
73
|
|
|
79
74
|
const users = state<UserOption[]>([])
|
|
@@ -114,7 +109,6 @@ async function loadCouriers(): Promise<void> {
|
|
|
114
109
|
throw new TypeError('Courier resources were not returned in the expected shape.')
|
|
115
110
|
users.set(userData.users)
|
|
116
111
|
records.set(courierData.map(withUser))
|
|
117
|
-
clampPage()
|
|
118
112
|
}
|
|
119
113
|
catch (error) {
|
|
120
114
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -203,11 +197,7 @@ onMount(() => {
|
|
|
203
197
|
<div><h2 class="font-semibold text-gray-900 dark:text-white">Courier roster</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
|
|
204
198
|
<SearchFilter placeholder="Search couriers" class="w-full sm:w-80" @search="handleSearch" />
|
|
205
199
|
</div>
|
|
206
|
-
<
|
|
207
|
-
<CouriersTable :if="!loading()" :couriers="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
|
|
208
|
-
<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">
|
|
209
|
-
<DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
|
|
210
|
-
</div>
|
|
200
|
+
<CouriersTable :couriers="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
|
|
211
201
|
</section>
|
|
212
202
|
</div>
|
|
213
203
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
type CourierStatus = 'active' | 'on_delivery' | 'on_break'
|
|
3
5
|
|
|
@@ -13,91 +15,62 @@ interface CourierRow {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const couriers = useReactiveProp('couriers', [] as CourierRow[])
|
|
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
|
-
|
|
24
|
-
|
|
23
|
+
const COLUMNS = [
|
|
24
|
+
{ key: 'name', label: 'Courier', sortable: true, class: 'font-medium text-stone-800 whitespace-nowrap dark:text-white' },
|
|
25
|
+
{ key: 'phone', label: 'Phone', sortable: true, class: 'whitespace-nowrap' },
|
|
26
|
+
{ key: 'vehicle_number', label: 'Vehicle', sortable: true, class: 'font-mono whitespace-nowrap' },
|
|
27
|
+
{ key: 'license', label: 'License', sortable: true, class: 'font-mono whitespace-nowrap' },
|
|
28
|
+
{ key: 'userLabel', label: 'User', sortable: true, class: 'whitespace-nowrap' },
|
|
29
|
+
{
|
|
30
|
+
key: 'status',
|
|
31
|
+
label: 'Status',
|
|
32
|
+
type: 'badge' as const,
|
|
33
|
+
sortable: true,
|
|
34
|
+
badgeClasses: {
|
|
35
|
+
active: 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
|
36
|
+
on_delivery: 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
|
37
|
+
on_break: 'bg-amber-50 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
|
38
|
+
},
|
|
39
|
+
badgeLabels: { active: 'Available', on_delivery: 'On delivery', on_break: 'On break' },
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const ROW_ACTIONS = [
|
|
44
|
+
{ action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
|
|
45
|
+
{ action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
|
|
46
|
+
]
|
|
25
47
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
const rows = derived(() => couriers().map(courier => ({
|
|
49
|
+
...courier,
|
|
50
|
+
userLabel: courier.user?.name || (courier.user_id ? `User #${courier.user_id}` : 'Not linked'),
|
|
51
|
+
actionName: `courier ${courier.name}, record ${courier.id}`,
|
|
52
|
+
})))
|
|
53
|
+
|
|
54
|
+
function handleRowAction(payload: { action: string, row: CourierRow }): void {
|
|
55
|
+
emit(payload.action, payload.row)
|
|
32
56
|
}
|
|
33
57
|
</script>
|
|
34
58
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<td class="pl-6 pr-3 py-3.5 font-medium text-sm text-stone-800 whitespace-nowrap dark:text-white">
|
|
54
|
-
{{ courier.name }}
|
|
55
|
-
</td>
|
|
56
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
57
|
-
{{ courier.phone }}
|
|
58
|
-
</td>
|
|
59
|
-
<td class="px-4 py-3.5 font-mono text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
60
|
-
{{ courier.vehicle_number }}
|
|
61
|
-
</td>
|
|
62
|
-
<td class="px-4 py-3.5 font-mono text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
63
|
-
{{ courier.license }}
|
|
64
|
-
</td>
|
|
65
|
-
<td class="px-4 py-3.5 text-sm text-stone-500 whitespace-nowrap dark:text-neutral-300">
|
|
66
|
-
{{ courier.user?.name || (courier.user_id ? 'User #' + courier.user_id : 'Not linked') }}
|
|
67
|
-
</td>
|
|
68
|
-
<td class="px-4 py-3.5 text-sm whitespace-nowrap">
|
|
69
|
-
<span :class="'inline-flex items-center px-2 py-1 font-medium text-xs ring-1 ring-inset rounded-md ' + statusClass(courier.status)">
|
|
70
|
-
{{ statusLabel(courier.status) }}
|
|
71
|
-
</span>
|
|
72
|
-
</td>
|
|
73
|
-
<td class="relative pl-3 pr-6 py-3.5 font-medium text-right text-sm whitespace-nowrap">
|
|
74
|
-
<Button
|
|
75
|
-
variant="ghost"
|
|
76
|
-
size="sm"
|
|
77
|
-
iconOnly
|
|
78
|
-
@click="emit('edit', courier)"
|
|
79
|
-
:ariaLabel="'Edit courier ' + courier.name + ', record ' + courier.id"
|
|
80
|
-
>
|
|
81
|
-
<span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
|
|
82
|
-
</Button>
|
|
83
|
-
<Button
|
|
84
|
-
variant="ghost"
|
|
85
|
-
size="sm"
|
|
86
|
-
iconOnly
|
|
87
|
-
@click="emit('delete', courier)"
|
|
88
|
-
:ariaLabel="'Delete courier ' + courier.name + ', record ' + courier.id"
|
|
89
|
-
>
|
|
90
|
-
<span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
|
|
91
|
-
</Button>
|
|
92
|
-
</td>
|
|
93
|
-
</tr>
|
|
94
|
-
</template>
|
|
95
|
-
|
|
96
|
-
<tr :if="couriers().length === 0">
|
|
97
|
-
<td colspan="7" class="px-6 py-12 text-center text-sm text-stone-500 dark:text-neutral-400">
|
|
98
|
-
No couriers found.
|
|
99
|
-
</td>
|
|
100
|
-
</tr>
|
|
101
|
-
</tbody>
|
|
102
|
-
</table>
|
|
103
|
-
</div>
|
|
59
|
+
{{--
|
|
60
|
+
No `searchable` or `filterable`: the parent dashboard owns the search box.
|
|
61
|
+
Table pages the whole filtered list the parent passes, and stays mounted
|
|
62
|
+
while it loads.
|
|
63
|
+
--}}
|
|
64
|
+
<Table
|
|
65
|
+
:page-reset-key="pageResetKey()"
|
|
66
|
+
:columns="COLUMNS"
|
|
67
|
+
:data="rows()"
|
|
68
|
+
:loading="loading()"
|
|
69
|
+
:row-actions="ROW_ACTIONS"
|
|
70
|
+
action-label-key="actionName"
|
|
71
|
+
paginated
|
|
72
|
+
:page-size="10"
|
|
73
|
+
empty-title="No couriers found"
|
|
74
|
+
empty-description="Add a courier, or adjust the search."
|
|
75
|
+
@row-action="handleRowAction"
|
|
76
|
+
/>
|
|
@@ -49,8 +49,6 @@ const {
|
|
|
49
49
|
loading,
|
|
50
50
|
loadError,
|
|
51
51
|
search,
|
|
52
|
-
page,
|
|
53
|
-
perPage,
|
|
54
52
|
editorOpen,
|
|
55
53
|
editorMode,
|
|
56
54
|
editing,
|
|
@@ -60,8 +58,6 @@ const {
|
|
|
60
58
|
deleteBusy,
|
|
61
59
|
deleteError,
|
|
62
60
|
filteredRecords,
|
|
63
|
-
paginatedRecords,
|
|
64
|
-
goToPage,
|
|
65
61
|
handleSearch,
|
|
66
62
|
openCreate,
|
|
67
63
|
openEdit,
|
|
@@ -70,7 +66,6 @@ const {
|
|
|
70
66
|
closeDelete,
|
|
71
67
|
upsert,
|
|
72
68
|
remove,
|
|
73
|
-
clampPage,
|
|
74
69
|
} = useRecordsDashboard<DeliveryRouteRecord>({ filter: matchesQuery })
|
|
75
70
|
|
|
76
71
|
const couriers = state<CourierRecord[]>([])
|
|
@@ -117,7 +112,6 @@ async function loadRoutes(): Promise<void> {
|
|
|
117
112
|
throw new TypeError('Delivery route resources were not returned in the expected shape.')
|
|
118
113
|
couriers.set(courierData)
|
|
119
114
|
records.set(routeData.map(withCourier))
|
|
120
|
-
clampPage()
|
|
121
115
|
}
|
|
122
116
|
catch (error) {
|
|
123
117
|
loadError.set(error instanceof Error ? error.message : String(error))
|
|
@@ -172,7 +166,6 @@ async function confirmDelete(): Promise<void> {
|
|
|
172
166
|
})
|
|
173
167
|
remove(current)
|
|
174
168
|
deleting.set(null)
|
|
175
|
-
clampPage()
|
|
176
169
|
pushToast('success', 'Delivery route deleted', { detail: `Route #${current.id}` })
|
|
177
170
|
}
|
|
178
171
|
catch (error) {
|
|
@@ -208,11 +201,7 @@ onMount(() => {
|
|
|
208
201
|
<div><h2 class="font-semibold text-gray-900 dark:text-white">Route plan</h2><p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ filteredRecords().length }} matching records</p></div>
|
|
209
202
|
<SearchFilter placeholder="Search delivery routes" class="w-full sm:w-80" @search="handleSearch" />
|
|
210
203
|
</div>
|
|
211
|
-
<
|
|
212
|
-
<DeliveryRoutesTable :if="!loading()" :routes="paginatedRecords()" @edit="openEdit" @delete="requestDelete" />
|
|
213
|
-
<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">
|
|
214
|
-
<DeliveryPagination :current-page="page()" :total-items="filteredRecords().length" :items-per-page="perPage()" @select="goToPage" />
|
|
215
|
-
</div>
|
|
204
|
+
<DeliveryRoutesTable :routes="filteredRecords()" :loading="loading()" :page-reset-key="search()" @edit="openEdit" @delete="requestDelete" />
|
|
216
205
|
</section>
|
|
217
206
|
</div>
|
|
218
207
|
|