@stacksjs/defaults 0.75.38 → 0.75.40
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-ai/SKILL.md +24 -1
- package/ai/skills/stacks-commerce/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-dependencies/SKILL.md +35 -3
- package/ai/skills/stacks-env/SKILL.md +4 -1
- package/ai/skills/stacks-migrations/SKILL.md +2 -2
- package/ai/skills/stacks-models/SKILL.md +2 -2
- package/app/Actions/Commerce/Product/ProductStoreAction.ts +9 -1
- package/app/Actions/Commerce/Product/ProductUpdateAction.ts +4 -1
- package/app/Actions/Commerce/commerce-action.test.ts +30 -1
- package/app/Actions/Commerce/commerce-action.ts +44 -0
- package/app/Actions/Dashboard/Analytics/sales-analytics.test.ts +19 -0
- package/app/Actions/Dashboard/Analytics/sales-analytics.ts +2 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +3 -2
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.test.ts +29 -9
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +5 -12
- package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +15 -0
- package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +8 -10
- package/app/Actions/Dashboard/Commerce/commerce-pos.test.ts +26 -6
- package/app/Actions/Dashboard/Commerce/commerce-pos.ts +8 -3
- package/app/Actions/Dashboard/Commerce/commerce-product-records.ts +69 -0
- package/app/Actions/Dashboard/Marketing/abandoned-cart-records.test.ts +39 -0
- package/app/Actions/Dashboard/Marketing/abandoned-cart-records.ts +9 -3
- package/app/Actions/Dashboard/Marketing/campaign-records.ts +17 -3
- package/app/Actions/Storefront/AddToCartAction.ts +4 -1
- package/app/Actions/Storefront/GetCartAction.ts +25 -10
- package/app/Actions/Storefront/PlaceOrderAction.ts +6 -1
- package/app/Actions/Storefront/_shipping.ts +63 -37
- package/app/Broadcasts/OrderShipped.ts +1 -1
- package/app/Mail/OrderConfirmation.ts +14 -7
- package/app/Models/Campaign.ts +9 -4
- package/app/Models/commerce/Cart.ts +5 -4
- package/app/Models/commerce/CartItem.ts +6 -4
- package/app/Models/commerce/Coupon.ts +9 -3
- package/app/Models/commerce/Customer.ts +2 -1
- package/app/Models/commerce/GiftCard.ts +5 -2
- package/app/Models/commerce/Order.ts +6 -5
- package/app/Models/commerce/OrderItem.ts +2 -1
- package/app/Models/commerce/ProductVariant.ts +71 -4
- package/app/Models/commerce/Transaction.ts +3 -2
- package/app/Storefront/StorefrontMoney.ts +54 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Analytics/CampaignAnalytics.stx +8 -8
- package/resources/components/Dashboard/Analytics/SalesAnalytics.stx +4 -3
- package/resources/components/Dashboard/Billing/BillingPlanSummary.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +34 -10
- package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +3 -1
- package/resources/components/Dashboard/Commerce/CommerceCouponsTable.stx +4 -1
- package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +6 -6
- package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +3 -11
- package/resources/components/Dashboard/Commerce/CommerceCustomersTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardBreakdown.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +20 -17
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +3 -2
- package/resources/components/Dashboard/Commerce/CommerceGiftCardsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +28 -20
- package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +7 -15
- package/resources/components/Dashboard/Commerce/CommerceOrdersTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePaymentsBreakdown.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCart.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCatalog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +5 -7
- package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +2 -8
- package/resources/components/Dashboard/Commerce/CommerceProductDetailDashboard.stx +3 -20
- package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +27 -19
- package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +4 -15
- package/resources/components/Dashboard/Commerce/CommerceProductsTable.stx +2 -6
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +12 -10
- package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsTable.stx +3 -4
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +7 -5
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +3 -2
- package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesTable.stx +3 -4
- package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +2 -6
- package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +12 -10
- package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +3 -10
- package/resources/components/Dashboard/Marketing/AbandonedCartsTable.stx +3 -8
- package/resources/components/Dashboard/Marketing/CampaignDialog.stx +26 -8
- package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +4 -2
- package/resources/components/Dashboard/Marketing/RecoveryCampaignDialog.stx +25 -13
- package/resources/components/Dashboard/Transaction/index.stx +6 -3
- package/resources/components/Storefront/CartDrawer.stx +11 -7
- package/resources/views/cart.stx +19 -23
- package/resources/views/checkout/contact.stx +12 -14
- package/resources/views/checkout/payment.stx +13 -13
- package/resources/views/checkout/shipping.stx +13 -13
- package/resources/views/cms/blocks/form.stx +50 -1
- package/resources/views/orders/[id].stx +13 -4
- package/vcs/github/workflows/buddy-bot.yml +5 -5
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
|
@@ -3,11 +3,13 @@ import type {
|
|
|
3
3
|
CommerceProductRecord,
|
|
4
4
|
ProductOption,
|
|
5
5
|
ProductReviewSummary,
|
|
6
|
+
ProductWritePayload,
|
|
6
7
|
ProductUnitDetail,
|
|
7
8
|
ProductVariantDetail,
|
|
8
9
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
9
10
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
10
11
|
import { pushToast } from '../../../../functions/toasts'
|
|
12
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
11
13
|
|
|
12
14
|
interface ProductDetailResponse {
|
|
13
15
|
record: CommerceProductRecord
|
|
@@ -19,20 +21,6 @@ interface ProductDetailResponse {
|
|
|
19
21
|
defaultCurrency: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
interface ProductWritePayload {
|
|
23
|
-
name: string
|
|
24
|
-
description: string
|
|
25
|
-
price: number
|
|
26
|
-
imageUrl: string
|
|
27
|
-
isAvailable: boolean
|
|
28
|
-
inventoryCount: number
|
|
29
|
-
preparationTime: number
|
|
30
|
-
allergens: string
|
|
31
|
-
nutritionalInfo: string
|
|
32
|
-
categoryId: number | null
|
|
33
|
-
manufacturerId: number | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
24
|
const route = useRoute()
|
|
37
25
|
const record = state<CommerceProductRecord | null>(null)
|
|
38
26
|
const variants = state<ProductVariantDetail[]>([])
|
|
@@ -55,12 +43,7 @@ function productId(): string {
|
|
|
55
43
|
}
|
|
56
44
|
|
|
57
45
|
function formatMoney(value: number): string {
|
|
58
|
-
|
|
59
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: defaultCurrency() }).format(value)
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return `${value.toFixed(2)} ${defaultCurrency()}`
|
|
63
|
-
}
|
|
46
|
+
return formatCurrency(value, defaultCurrency())
|
|
64
47
|
}
|
|
65
48
|
|
|
66
49
|
function formatDate(value: string): string {
|
|
@@ -3,6 +3,11 @@ import type {
|
|
|
3
3
|
CommerceProductRecord,
|
|
4
4
|
ProductOption,
|
|
5
5
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
6
|
+
import { minorToInput, moneyInputStep } from '@stacksjs/commerce/money'
|
|
7
|
+
import {
|
|
8
|
+
productPriceInputError,
|
|
9
|
+
productWritePayload,
|
|
10
|
+
} from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
6
11
|
|
|
7
12
|
const open = useReactiveProp('open', false)
|
|
8
13
|
const mode = useReactiveProp('mode', 'add')
|
|
@@ -15,7 +20,9 @@ const errors = useReactiveProp('errors', {} as Record<string, string>)
|
|
|
15
20
|
const emit = defineEmits()
|
|
16
21
|
const name = state('')
|
|
17
22
|
const description = state('')
|
|
18
|
-
|
|
23
|
+
// The price as typed, in major units ("19.99"). It is stored as integer
|
|
24
|
+
// minor units (1999), converted once in `productWritePayload`.
|
|
25
|
+
const price = state('')
|
|
19
26
|
const imageUrl = state('')
|
|
20
27
|
const isAvailable = state(true)
|
|
21
28
|
const inventoryCount = state('0')
|
|
@@ -38,7 +45,7 @@ effect(() => {
|
|
|
38
45
|
hydrationKey = key
|
|
39
46
|
name.set(current?.name || '')
|
|
40
47
|
description.set(current?.description || '')
|
|
41
|
-
price.set(
|
|
48
|
+
price.set(current ? minorToInput(current.price, currency()) : '')
|
|
42
49
|
imageUrl.set(current?.imageUrl || '')
|
|
43
50
|
isAvailable.set(current?.isAvailable ?? true)
|
|
44
51
|
inventoryCount.set(String(current?.inventoryCount ?? 0))
|
|
@@ -62,14 +69,16 @@ function nutritionValid(): boolean {
|
|
|
62
69
|
}
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
function priceError(): string {
|
|
73
|
+
return productPriceInputError(price(), currency())
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
function valid(): boolean {
|
|
66
|
-
const parsedPrice = Number(price())
|
|
67
77
|
const parsedInventory = Number(inventoryCount())
|
|
68
78
|
const parsedPreparation = Number(preparationTime())
|
|
69
79
|
return Boolean(
|
|
70
80
|
name().trim()
|
|
71
|
-
&&
|
|
72
|
-
&& parsedPrice >= 1
|
|
81
|
+
&& !priceError()
|
|
73
82
|
&& Number.isFinite(parsedInventory)
|
|
74
83
|
&& parsedInventory >= 0
|
|
75
84
|
&& Number.isInteger(parsedInventory)
|
|
@@ -83,20 +92,19 @@ function valid(): boolean {
|
|
|
83
92
|
function submit(): void {
|
|
84
93
|
if (!valid())
|
|
85
94
|
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
imageUrl: imageUrl().trim(),
|
|
95
|
+
emit('submit', productWritePayload({
|
|
96
|
+
name: name(),
|
|
97
|
+
description: description(),
|
|
98
|
+
price: price(),
|
|
99
|
+
imageUrl: imageUrl(),
|
|
92
100
|
isAvailable: isAvailable(),
|
|
93
|
-
inventoryCount:
|
|
94
|
-
preparationTime:
|
|
95
|
-
allergens:
|
|
96
|
-
nutritionalInfo: nutritionalInfo()
|
|
97
|
-
categoryId: categoryId()
|
|
98
|
-
manufacturerId: manufacturerId()
|
|
99
|
-
})
|
|
101
|
+
inventoryCount: inventoryCount(),
|
|
102
|
+
preparationTime: preparationTime(),
|
|
103
|
+
allergens: allergens(),
|
|
104
|
+
nutritionalInfo: nutritionalInfo(),
|
|
105
|
+
categoryId: categoryId(),
|
|
106
|
+
manufacturerId: manufacturerId(),
|
|
107
|
+
}, currency()))
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
function errorMessages(): string[] {
|
|
@@ -121,7 +129,7 @@ function errorMessages(): string[] {
|
|
|
121
129
|
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
|
122
130
|
<label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Name</span><input x-model="name" type="text" maxlength="100" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
|
|
123
131
|
<label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Description</span><textarea x-model="description" rows="3" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700"></textarea></label>
|
|
124
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Price ({{ currency() }})</span><input x-model="price" type="number" min="
|
|
132
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Price ({{ currency() }})</span><input x-model="price" type="number" min="0" :step="moneyInputStep(currency())" inputmode="decimal" required :aria-invalid="price() !== '' && priceError() !== ''" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /><span :if="price() !== '' && priceError()" class="block mt-1 text-red-600 text-xs dark:text-red-400">{{ priceError() }}</span></label>
|
|
125
133
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Inventory count</span><input x-model="inventoryCount" type="number" min="0" step="1" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
|
|
126
134
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Preparation time (minutes)</span><input x-model="preparationTime" type="number" min="1" step="1" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
|
|
127
135
|
<label class="flex gap-3 items-center self-end px-3 py-2 min-h-10 bg-gray-50 dark:bg-neutral-900/60 border border-gray-200 rounded-md dark:border-neutral-700"><input x-model="isAvailable" type="checkbox" class="h-4 w-4 text-blue-600 border-gray-300 rounded" /><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Available for commerce</span></label>
|
|
@@ -3,9 +3,11 @@ import type {
|
|
|
3
3
|
CommerceProductRecord,
|
|
4
4
|
CommerceProductSummary,
|
|
5
5
|
ProductOption,
|
|
6
|
+
ProductWritePayload,
|
|
6
7
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
7
8
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
8
9
|
import { pushToast } from '../../../../functions/toasts'
|
|
10
|
+
import { minorToInput } from '@stacksjs/commerce/money'
|
|
9
11
|
|
|
10
12
|
interface ProductIndexResponse {
|
|
11
13
|
records: CommerceProductRecord[]
|
|
@@ -15,20 +17,6 @@ interface ProductIndexResponse {
|
|
|
15
17
|
defaultCurrency: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
interface ProductWritePayload {
|
|
19
|
-
name: string
|
|
20
|
-
description: string
|
|
21
|
-
price: number
|
|
22
|
-
imageUrl: string
|
|
23
|
-
isAvailable: boolean
|
|
24
|
-
inventoryCount: number
|
|
25
|
-
preparationTime: number
|
|
26
|
-
allergens: string
|
|
27
|
-
nutritionalInfo: string
|
|
28
|
-
categoryId: number | null
|
|
29
|
-
manufacturerId: number | null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
const emptySummary: CommerceProductSummary = {
|
|
33
21
|
total: 0,
|
|
34
22
|
available: 0,
|
|
@@ -135,7 +123,8 @@ function exportHref(): string {
|
|
|
135
123
|
record.id,
|
|
136
124
|
record.name,
|
|
137
125
|
record.description,
|
|
138
|
-
|
|
126
|
+
// Stored as integer minor units; the CSV carries the decimal a person reads.
|
|
127
|
+
minorToInput(record.price, defaultCurrency()),
|
|
139
128
|
defaultCurrency(),
|
|
140
129
|
record.isAvailable ? 'Available' : 'Unavailable',
|
|
141
130
|
String(record.inventoryCount),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommerceProductRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as CommerceProductRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
@@ -11,12 +12,7 @@ function initials(name: string): string {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function formatMoney(value: number): string {
|
|
14
|
-
|
|
15
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
19
|
-
}
|
|
15
|
+
return formatCurrency(value, currency())
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function availabilityClass(record: CommerceProductRecord): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { minorToInput, moneyInputError, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
2
3
|
interface ShippingMethodRecord {
|
|
3
4
|
id: number
|
|
4
5
|
name: string
|
|
@@ -33,33 +34,34 @@ effect(() => {
|
|
|
33
34
|
hydrationKey = nextKey
|
|
34
35
|
name.set(current?.name || '')
|
|
35
36
|
description.set(current?.description || '')
|
|
36
|
-
baseRate.set(current ? (current.base_rate
|
|
37
|
+
baseRate.set(current ? minorToInput(current.base_rate, 'USD') : '')
|
|
37
38
|
freeShipping.set(current?.free_shipping === null || current?.free_shipping === undefined
|
|
38
39
|
? ''
|
|
39
|
-
: (current.free_shipping
|
|
40
|
+
: minorToInput(current.free_shipping, 'USD'))
|
|
40
41
|
status.set(current?.status || 'active')
|
|
41
42
|
})
|
|
42
43
|
|
|
44
|
+
// Amounts are typed in dollars and stored as integer cents.
|
|
43
45
|
function submit(): void {
|
|
44
|
-
const baseRateValue = Number(baseRate())
|
|
45
|
-
const freeShippingValue = freeShipping().trim() === '' ? null : Number(freeShipping())
|
|
46
46
|
if (!name().trim()) {
|
|
47
47
|
emit('invalid', 'Method name is required.')
|
|
48
48
|
return
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const baseRateError = moneyInputError(baseRate(), 'USD')
|
|
51
|
+
if (baseRateError) {
|
|
52
|
+
emit('invalid', `Base rate: ${baseRateError}`)
|
|
52
53
|
return
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
const freeShippingError = moneyInputError(freeShipping(), 'USD', { required: false })
|
|
56
|
+
if (freeShippingError) {
|
|
57
|
+
emit('invalid', `Free shipping threshold: ${freeShippingError}`)
|
|
56
58
|
return
|
|
57
59
|
}
|
|
58
60
|
emit('submit', {
|
|
59
61
|
name: name().trim(),
|
|
60
62
|
description: description().trim(),
|
|
61
|
-
baseRate:
|
|
62
|
-
freeShipping:
|
|
63
|
+
baseRate: parseMoneyInput(baseRate(), 'USD'),
|
|
64
|
+
freeShipping: parseMoneyInput(freeShipping(), 'USD', { required: false }),
|
|
63
65
|
status: status(),
|
|
64
66
|
})
|
|
65
67
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formatCurrency as formatMinorUnits } from '@stacksjs/commerce/money'
|
|
2
3
|
interface ShippingZoneSummary {
|
|
3
4
|
id: number
|
|
4
5
|
name: string
|
|
@@ -17,14 +18,12 @@ interface ShippingMethodRow {
|
|
|
17
18
|
const methods = useReactiveProp('methods', [] as ShippingMethodRow[])
|
|
18
19
|
const emit = defineEmits()
|
|
19
20
|
|
|
21
|
+
// Shipping amounts are stored as integer cents, in USD.
|
|
20
22
|
function formatCurrency(cents?: number | null): string {
|
|
21
23
|
if (cents === null || cents === undefined)
|
|
22
24
|
return 'Not available'
|
|
23
25
|
|
|
24
|
-
return
|
|
25
|
-
style: 'currency',
|
|
26
|
-
currency: 'USD',
|
|
27
|
-
}).format(cents / 100)
|
|
26
|
+
return formatMinorUnits(cents, 'USD', 'en-US')
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
function statusClass(status: string): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { minorToInput, moneyInputError, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
2
3
|
interface ShippingResourceOption {
|
|
3
4
|
id: number
|
|
4
5
|
name: string
|
|
@@ -42,7 +43,7 @@ effect(() => {
|
|
|
42
43
|
zoneId.set(current ? String(current.shipping_zone_id) : '')
|
|
43
44
|
weightFrom.set(current ? String(current.weight_from) : '')
|
|
44
45
|
weightTo.set(current ? String(current.weight_to) : '')
|
|
45
|
-
amount.set(current ? (current.rate
|
|
46
|
+
amount.set(current ? minorToInput(current.rate, 'USD') : '')
|
|
46
47
|
})
|
|
47
48
|
|
|
48
49
|
function submit(): void {
|
|
@@ -50,7 +51,6 @@ function submit(): void {
|
|
|
50
51
|
const selectedZoneId = Number(zoneId())
|
|
51
52
|
const from = Number(weightFrom())
|
|
52
53
|
const to = Number(weightTo())
|
|
53
|
-
const rate = Number(amount())
|
|
54
54
|
|
|
55
55
|
if (!Number.isInteger(selectedMethodId) || selectedMethodId < 1) {
|
|
56
56
|
emit('invalid', 'Select a shipping method.')
|
|
@@ -68,8 +68,10 @@ function submit(): void {
|
|
|
68
68
|
emit('invalid', 'Ending weight must be greater than or equal to the starting weight.')
|
|
69
69
|
return
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Typed in dollars, stored as integer cents.
|
|
72
|
+
const rateError = moneyInputError(amount(), 'USD')
|
|
73
|
+
if (rateError) {
|
|
74
|
+
emit('invalid', `Rate: ${rateError}`)
|
|
73
75
|
return
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -78,7 +80,7 @@ function submit(): void {
|
|
|
78
80
|
shippingZoneId: selectedZoneId,
|
|
79
81
|
weightFrom: from,
|
|
80
82
|
weightTo: to,
|
|
81
|
-
rate:
|
|
83
|
+
rate: parseMoneyInput(amount(), 'USD'),
|
|
82
84
|
})
|
|
83
85
|
}
|
|
84
86
|
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import { dashboardApi } from '../../../../../functions/dashboard-api'
|
|
3
3
|
import { pushToast } from '../../../../../functions/toasts'
|
|
4
|
+
import { formatCurrency, minorToInput } from '@stacksjs/commerce/money'
|
|
4
5
|
|
|
5
6
|
interface ShippingResourceOption {
|
|
6
7
|
id: number
|
|
@@ -69,7 +70,7 @@ function filteredRecords(): ShippingRateRecord[] {
|
|
|
69
70
|
record.shipping_zone?.name,
|
|
70
71
|
record.weight_from,
|
|
71
72
|
record.weight_to,
|
|
72
|
-
record.rate
|
|
73
|
+
minorToInput(record.rate, 'USD'),
|
|
73
74
|
].some(value => String(value ?? '').toLowerCase().includes(query)))
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -86,7 +87,7 @@ function averageRate(): string {
|
|
|
86
87
|
if (!records().length)
|
|
87
88
|
return '$0.00'
|
|
88
89
|
const average = records().reduce((sum, record) => sum + record.rate, 0) / records().length
|
|
89
|
-
return
|
|
90
|
+
return formatCurrency(average, 'USD', 'en-US')
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function metricCards(): Array<{ label: string, value: string, detail: string }> {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formatCurrency as formatMinorUnits } from '@stacksjs/commerce/money'
|
|
2
3
|
interface ResourceSummary {
|
|
3
4
|
id: number
|
|
4
5
|
name: string
|
|
@@ -18,11 +19,9 @@ interface ShippingRateRow {
|
|
|
18
19
|
const rates = useReactiveProp('rates', [] as ShippingRateRow[])
|
|
19
20
|
const emit = defineEmits()
|
|
20
21
|
|
|
22
|
+
// Shipping amounts are stored as integer cents, in USD.
|
|
21
23
|
function formatCurrency(cents: number): string {
|
|
22
|
-
return
|
|
23
|
-
style: 'currency',
|
|
24
|
-
currency: 'USD',
|
|
25
|
-
}).format(cents / 100)
|
|
24
|
+
return formatMinorUnits(cents, 'USD', 'en-US')
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
function formatWeight(weight: number): string {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { PaymentRecord } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
3
3
|
import { isRefundablePayment } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
4
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
4
5
|
|
|
5
6
|
const open = useReactiveProp('open', false)
|
|
6
7
|
const record = useReactiveProp('record', null as PaymentRecord | null)
|
|
@@ -11,12 +12,7 @@ function label(value: string): string {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function money(value: number, currency: string): string {
|
|
14
|
-
|
|
15
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value / 100)
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return `${currency} ${(value / 100).toFixed(2)}`
|
|
19
|
-
}
|
|
15
|
+
return formatCurrency(value, currency)
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function detailRows(): Array<{ label: string, value: string }> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { PaymentRecord } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
3
|
+
import { formatCurrency, minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const record = useReactiveProp('record', null as PaymentRecord | null)
|
|
@@ -18,7 +19,7 @@ effect(() => {
|
|
|
18
19
|
if (!payment || hydrationKey === key)
|
|
19
20
|
return
|
|
20
21
|
hydrationKey = key
|
|
21
|
-
amount.set((
|
|
22
|
+
amount.set(minorToInput(payment.amount - payment.refundAmount, payment.currency))
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
function remaining(): number {
|
|
@@ -26,9 +27,15 @@ function remaining(): number {
|
|
|
26
27
|
return payment ? Math.max(0, payment.amount - payment.refundAmount) : 0
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function paymentCurrency(): string {
|
|
31
|
+
return record()?.currency || 'USD'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// What was typed, in integer minor units, or 0 while it is not a valid amount.
|
|
29
35
|
function amountCents(): number {
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
if (moneyInputError(amount(), paymentCurrency(), { min: 1 }))
|
|
37
|
+
return 0
|
|
38
|
+
return parseMoneyInput(amount(), paymentCurrency(), { min: 1 })
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function valid(): boolean {
|
|
@@ -37,12 +44,7 @@ function valid(): boolean {
|
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
function money(value: number, currency: string): string {
|
|
40
|
-
|
|
41
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value / 100)
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return `${currency} ${(value / 100).toFixed(2)}`
|
|
45
|
-
}
|
|
47
|
+
return formatCurrency(value, currency)
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
function submit(): void {
|
|
@@ -59,7 +61,7 @@ function submit(): void {
|
|
|
59
61
|
@close="emit('close')"
|
|
60
62
|
>
|
|
61
63
|
<form id="payment-refund-form" @submit.prevent="submit">
|
|
62
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Refund amount</span><input x-model="amount" type="number" min="
|
|
64
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Refund amount</span><input x-model="amount" type="number" :min="minorToInput(1, paymentCurrency())" :max="minorToInput(remaining(), paymentCurrency())" :step="moneyInputStep(paymentCurrency())" required class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
|
|
63
65
|
<p class="mt-2 text-amber-700 text-xs dark:text-amber-300">This records an already confirmed processor refund. It does not initiate money movement.</p>
|
|
64
66
|
</form>
|
|
65
67
|
<template #footer>
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
AbandonedCartSummary,
|
|
6
6
|
RecoveryCampaignRecord,
|
|
7
7
|
} from '../../../../app/Actions/Dashboard/Marketing/abandoned-cart-records'
|
|
8
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
8
9
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
9
10
|
import { pushToast } from '../../../../functions/toasts'
|
|
10
11
|
|
|
@@ -88,17 +89,9 @@ effect(() => {
|
|
|
88
89
|
page.set(1)
|
|
89
90
|
})
|
|
90
91
|
|
|
92
|
+
/** Cart totals and campaign thresholds are integer minor units (stacksjs/stacks#2851). */
|
|
91
93
|
function money(value: number): string {
|
|
92
|
-
|
|
93
|
-
return new Intl.NumberFormat(undefined, {
|
|
94
|
-
style: 'currency',
|
|
95
|
-
currency: summary().currency || defaultCurrency() || 'USD',
|
|
96
|
-
maximumFractionDigits: 0,
|
|
97
|
-
}).format(value)
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return `${Math.round(value)} ${defaultCurrency()}`
|
|
101
|
-
}
|
|
94
|
+
return formatCurrency(value, summary().currency || defaultCurrency())
|
|
102
95
|
}
|
|
103
96
|
|
|
104
97
|
function percentage(value: number): string {
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { AbandonedCartRecord } from '../../../../app/Actions/Dashboard/Marketing/abandoned-cart-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as AbandonedCartRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
6
7
|
|
|
8
|
+
/** A cart's total is integer minor units, like every commerce amount (stacksjs/stacks#2851). */
|
|
7
9
|
function money(value: number, currency: string): string {
|
|
8
|
-
|
|
9
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency || 'USD' }).format(value)
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
// A currency the browser does not know is not a reason to render nothing
|
|
13
|
-
// where a number belongs.
|
|
14
|
-
return `${value.toFixed(2)} ${currency}`
|
|
15
|
-
}
|
|
10
|
+
return formatCurrency(value, currency)
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
/**
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CampaignStatus,
|
|
6
6
|
CampaignType,
|
|
7
7
|
} from '../../../../app/Actions/Dashboard/Marketing/campaign-records'
|
|
8
|
+
import { minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
8
9
|
|
|
9
10
|
const open = useReactiveProp('open', false)
|
|
10
11
|
const mode = useReactiveProp('mode', 'add')
|
|
@@ -25,6 +26,9 @@ const fromName = state('')
|
|
|
25
26
|
const fromAddress = state('')
|
|
26
27
|
const emailListId = state('')
|
|
27
28
|
const scheduledAt = state('')
|
|
29
|
+
// Budget and spend as typed, in major units of `currency`. The record and
|
|
30
|
+
// the API hold integer minor units (1999 is $19.99), converted at this
|
|
31
|
+
// boundary through @stacksjs/commerce/money.
|
|
28
32
|
const budget = state('')
|
|
29
33
|
const spent = state('0')
|
|
30
34
|
const currency = state('USD')
|
|
@@ -33,6 +37,8 @@ const endDate = state('')
|
|
|
33
37
|
const canSubmit = derived(() => {
|
|
34
38
|
if (!name().trim())
|
|
35
39
|
return false
|
|
40
|
+
if (budgetError() || spentError())
|
|
41
|
+
return false
|
|
36
42
|
return type() !== 'email'
|
|
37
43
|
|| Boolean(subject().trim() && template().trim() && emailListId())
|
|
38
44
|
})
|
|
@@ -67,13 +73,22 @@ effect(() => {
|
|
|
67
73
|
fromAddress.set(current?.fromAddress || '')
|
|
68
74
|
emailListId.set(current?.emailListId || '')
|
|
69
75
|
scheduledAt.set(localDateTime(current?.scheduledAt || ''))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const recordCurrency = current?.currency || defaultCurrency()
|
|
77
|
+
budget.set(current?.budget ? minorToInput(current.budget, recordCurrency) : '')
|
|
78
|
+
spent.set(minorToInput(current?.spent || 0, recordCurrency))
|
|
79
|
+
currency.set(recordCurrency)
|
|
73
80
|
startDate.set(localDateTime(current?.startDate || ''))
|
|
74
81
|
endDate.set(localDateTime(current?.endDate || ''))
|
|
75
82
|
})
|
|
76
83
|
|
|
84
|
+
function budgetError(): string {
|
|
85
|
+
return moneyInputError(budget(), currency(), { required: false })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function spentError(): string {
|
|
89
|
+
return moneyInputError(spent(), currency(), { required: false })
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
function normalizeType(event: Event): void {
|
|
78
93
|
const nextType = (event.target as HTMLSelectElement).value as CampaignType
|
|
79
94
|
if (nextType === 'email')
|
|
@@ -85,6 +100,7 @@ function normalizeType(event: Event): void {
|
|
|
85
100
|
function submit(): void {
|
|
86
101
|
if (!canSubmit())
|
|
87
102
|
return
|
|
103
|
+
const code = currency().trim().toUpperCase()
|
|
88
104
|
emit('submit', {
|
|
89
105
|
name: name().trim(),
|
|
90
106
|
description: description().trim(),
|
|
@@ -97,9 +113,9 @@ function submit(): void {
|
|
|
97
113
|
fromAddress: fromAddress().trim(),
|
|
98
114
|
emailListId: emailListId() || null,
|
|
99
115
|
scheduledAt: databaseDateTime(scheduledAt()) || null,
|
|
100
|
-
budget: budget(),
|
|
101
|
-
spent: spent(),
|
|
102
|
-
currency:
|
|
116
|
+
budget: parseMoneyInput(budget(), code, { required: false }),
|
|
117
|
+
spent: parseMoneyInput(spent(), code, { required: false }) ?? 0,
|
|
118
|
+
currency: code,
|
|
103
119
|
startDate: databaseDateTime(startDate()),
|
|
104
120
|
endDate: databaseDateTime(endDate()),
|
|
105
121
|
})
|
|
@@ -200,12 +216,14 @@ function submit(): void {
|
|
|
200
216
|
</label>
|
|
201
217
|
<label class="block">
|
|
202
218
|
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Budget</span>
|
|
203
|
-
<input x-model="budget" type="number" min="0" step="
|
|
219
|
+
<input x-model="budget" type="number" min="0" :step="moneyInputStep(currency())" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
|
|
220
|
+
<span :if="budgetError()" class="block mt-1 text-red-600 text-xs dark:text-red-400">{{ budgetError() }}</span>
|
|
204
221
|
</label>
|
|
205
222
|
<div class="grid gap-3 grid-cols-[1fr_7rem]">
|
|
206
223
|
<label class="block">
|
|
207
224
|
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Spent</span>
|
|
208
|
-
<input x-model="spent" type="number" min="0" step="
|
|
225
|
+
<input x-model="spent" type="number" min="0" :step="moneyInputStep(currency())" class="mt-1 px-3 py-2 w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" />
|
|
226
|
+
<span :if="spentError()" class="block mt-1 text-red-600 text-xs dark:text-red-400">{{ spentError() }}</span>
|
|
209
227
|
</label>
|
|
210
228
|
<label class="block">
|
|
211
229
|
<span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Currency</span>
|
|
@@ -27,8 +27,10 @@ interface CampaignPayload {
|
|
|
27
27
|
fromAddress: string
|
|
28
28
|
emailListId: string | null
|
|
29
29
|
scheduledAt: string | null
|
|
30
|
-
budget
|
|
31
|
-
|
|
30
|
+
/** Integer minor units of `currency`, or null for no budget. */
|
|
31
|
+
budget: number | null
|
|
32
|
+
/** Integer minor units of `currency`. */
|
|
33
|
+
spent: number
|
|
32
34
|
currency: string
|
|
33
35
|
startDate: string
|
|
34
36
|
endDate: string
|