@stacksjs/defaults 0.75.38 → 0.75.41
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-desktop/SKILL.md +12 -0
- 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/Payment/CreatePaymentIntentAction.ts +1 -1
- package/app/Actions/Payment/CreateSetupIntentAction.ts +1 -1
- 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/.vscode/settings.json +1 -1
- package/ide/vscode/README.md +65 -20
- package/ide/vscode/package.json +102 -12
- package/ide/vscode/src/extension.ts +59 -0
- package/ide/vscode/src/project.ts +440 -0
- package/ide/vscode/src/stacks.ts +475 -0
- package/ide/vscode/src/vscode.d.ts +181 -0
- package/ide/vscode/tests/extension.test.ts +420 -0
- package/ide/vscode/tests/project.test.ts +268 -0
- package/ide/vscode/tsconfig.json +19 -0
- package/package.json +4 -4
- 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,6 +3,7 @@ import type {
|
|
|
3
3
|
CommerceOrderRecord,
|
|
4
4
|
OrderCustomerOption,
|
|
5
5
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-order-records'
|
|
6
|
+
import { minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
6
7
|
|
|
7
8
|
const open = useReactiveProp('open', false)
|
|
8
9
|
const mode = useReactiveProp('mode', 'add')
|
|
@@ -43,12 +44,14 @@ effect(() => {
|
|
|
43
44
|
customerId.set(current?.customerId || '')
|
|
44
45
|
couponId.set(current?.couponId || '')
|
|
45
46
|
status.set(current?.status || statuses()[0] || 'PENDING')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
// Stored amounts are integer minor units; the fields show the decimal.
|
|
48
|
+
const orderCurrency = current?.currency || defaultCurrency()
|
|
49
|
+
currency.set(orderCurrency)
|
|
50
|
+
totalAmount.set(minorToInput(current?.totalAmount ?? 0, orderCurrency))
|
|
51
|
+
taxAmount.set(minorToInput(current?.taxAmount ?? 0, orderCurrency))
|
|
52
|
+
discountAmount.set(minorToInput(current?.discountAmount ?? 0, orderCurrency))
|
|
53
|
+
deliveryFee.set(minorToInput(current?.deliveryFee ?? 0, orderCurrency))
|
|
54
|
+
tipAmount.set(minorToInput(current?.tipAmount ?? 0, orderCurrency))
|
|
52
55
|
orderType.set(current?.orderType || orderTypes()[0] || 'DELIVERY')
|
|
53
56
|
deliveryAddress.set(current?.deliveryAddress || '')
|
|
54
57
|
specialInstructions.set(current?.specialInstructions || '')
|
|
@@ -60,9 +63,14 @@ function label(value: string): string {
|
|
|
60
63
|
return value.replaceAll('_', ' ').toLowerCase().replace(/\b\w/g, letter => letter.toUpperCase())
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// An empty optional amount means zero, as it did before amounts were
|
|
67
|
+
// converted to minor units.
|
|
68
|
+
function validNumber(value: string, required = false): boolean {
|
|
69
|
+
return !moneyInputError(value, currency(), { required })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function minorAmount(value: string): number {
|
|
73
|
+
return parseMoneyInput(value, currency(), { required: false }) ?? 0
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
function valid(): boolean {
|
|
@@ -70,7 +78,7 @@ function valid(): boolean {
|
|
|
70
78
|
status().trim()
|
|
71
79
|
&& orderType().trim()
|
|
72
80
|
&& currency().trim().length === 3
|
|
73
|
-
&& validNumber(totalAmount())
|
|
81
|
+
&& validNumber(totalAmount(), true)
|
|
74
82
|
&& validNumber(taxAmount())
|
|
75
83
|
&& validNumber(discountAmount())
|
|
76
84
|
&& validNumber(deliveryFee())
|
|
@@ -85,12 +93,12 @@ function submit(): void {
|
|
|
85
93
|
customerId: customerId() ? Number(customerId()) : undefined,
|
|
86
94
|
couponId: couponId() ? Number(couponId()) : undefined,
|
|
87
95
|
status: status().trim(),
|
|
88
|
-
totalAmount:
|
|
96
|
+
totalAmount: parseMoneyInput(totalAmount(), currency()),
|
|
89
97
|
currency: currency().trim().toUpperCase(),
|
|
90
|
-
taxAmount:
|
|
91
|
-
discountAmount:
|
|
92
|
-
deliveryFee:
|
|
93
|
-
tipAmount:
|
|
98
|
+
taxAmount: minorAmount(taxAmount()),
|
|
99
|
+
discountAmount: minorAmount(discountAmount()),
|
|
100
|
+
deliveryFee: minorAmount(deliveryFee()),
|
|
101
|
+
tipAmount: minorAmount(tipAmount()),
|
|
94
102
|
orderType: orderType().trim(),
|
|
95
103
|
deliveryAddress: deliveryAddress().trim() || undefined,
|
|
96
104
|
specialInstructions: specialInstructions().trim() || undefined,
|
|
@@ -123,11 +131,11 @@ function errorMessages(): string[] {
|
|
|
123
131
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span><select x-model="status" 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"><template :for="option in statuses()" :key="option"><option :value="option">{{ label(option) }}</option></template></select></label>
|
|
124
132
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Order type</span><select x-model="orderType" 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"><template :for="option in orderTypes()" :key="option"><option :value="option">{{ label(option) }}</option></template></select></label>
|
|
125
133
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Currency</span><input x-model="currency" type="text" minlength="3" maxlength="3" required spellcheck="false" class="mt-1 px-3 py-2 w-full font-mono text-gray-900 text-sm uppercase dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label>
|
|
126
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Total amount</span><input x-model="totalAmount" type="number" min="0" step="
|
|
127
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Tax amount</span><input x-model="taxAmount" type="number" min="0" step="
|
|
128
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Discount amount</span><input x-model="discountAmount" type="number" min="0" step="
|
|
129
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Delivery fee</span><input x-model="deliveryFee" type="number" min="0" step="
|
|
130
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Tip amount</span><input x-model="tipAmount" type="number" min="0" step="
|
|
134
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Total amount</span><input x-model="totalAmount" type="number" min="0" :step="moneyInputStep(currency())" 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>
|
|
135
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Tax amount</span><input x-model="taxAmount" 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" /></label>
|
|
136
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Discount amount</span><input x-model="discountAmount" 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" /></label>
|
|
137
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Delivery fee</span><input x-model="deliveryFee" 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" /></label>
|
|
138
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Tip amount</span><input x-model="tipAmount" 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" /></label>
|
|
131
139
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Estimated delivery time</span><input x-model="estimatedDeliveryTime" type="datetime-local" 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>
|
|
132
140
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Coupon relation ID</span><input x-model="couponId" type="number" min="1" step="1" 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>
|
|
133
141
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Applied coupon reference</span><input x-model="appliedCouponId" type="text" class="mt-1 px-3 py-2 w-full font-mono 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>
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-order-records'
|
|
7
7
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
8
8
|
import { pushToast } from '../../../../functions/toasts'
|
|
9
|
+
import { formatCurrency, minorToInput } from '@stacksjs/commerce/money'
|
|
9
10
|
|
|
10
11
|
interface OrderIndexResponse {
|
|
11
12
|
records: CommerceOrderRecord[]
|
|
@@ -75,16 +76,7 @@ function timestamp(value: string): number {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function formatMoney(value: number, currency: string): string {
|
|
78
|
-
|
|
79
|
-
return new Intl.NumberFormat(undefined, {
|
|
80
|
-
style: 'currency',
|
|
81
|
-
currency,
|
|
82
|
-
maximumFractionDigits: 2,
|
|
83
|
-
}).format(value)
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return `${value.toFixed(2)} ${currency}`
|
|
87
|
-
}
|
|
79
|
+
return formatCurrency(value, currency)
|
|
88
80
|
}
|
|
89
81
|
|
|
90
82
|
function label(value: string): string {
|
|
@@ -154,12 +146,12 @@ function exportHref(): string {
|
|
|
154
146
|
record.status,
|
|
155
147
|
record.orderType,
|
|
156
148
|
String(record.itemCount),
|
|
157
|
-
|
|
149
|
+
minorToInput(record.totalAmount, record.currency),
|
|
158
150
|
record.currency,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
151
|
+
minorToInput(record.taxAmount, record.currency),
|
|
152
|
+
minorToInput(record.discountAmount, record.currency),
|
|
153
|
+
minorToInput(record.deliveryFee, record.currency),
|
|
154
|
+
minorToInput(record.tipAmount, record.currency),
|
|
163
155
|
record.deliveryAddress,
|
|
164
156
|
record.createdAt,
|
|
165
157
|
])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommerceOrderRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-order-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as CommerceOrderRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
@@ -10,12 +11,7 @@ function label(value: string): string {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function formatMoney(value: number, currency: string): string {
|
|
13
|
-
|
|
14
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return `${value.toFixed(2)} ${currency}`
|
|
18
|
-
}
|
|
14
|
+
return formatCurrency(value, currency)
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
function formatDate(value: string): string {
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
PaymentRecord,
|
|
4
4
|
} from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
5
5
|
import { isSuccessfulPayment } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
6
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
6
7
|
|
|
7
8
|
interface CountRow {
|
|
8
9
|
key: string
|
|
@@ -55,12 +56,7 @@ function label(value: string): string {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function money(value: number, currency: string): string {
|
|
58
|
-
|
|
59
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value / 100)
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return `${currency} ${(value / 100).toFixed(2)}`
|
|
63
|
-
}
|
|
59
|
+
return formatCurrency(value, currency)
|
|
64
60
|
}
|
|
65
61
|
</script>
|
|
66
62
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { PaymentRecord } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as PaymentRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
@@ -10,12 +11,7 @@ function label(value: string): string {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function money(value: number, currency: string): string {
|
|
13
|
-
|
|
14
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value / 100)
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return `${currency} ${(value / 100).toFixed(2)}`
|
|
18
|
-
}
|
|
14
|
+
return formatCurrency(value, currency)
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
function date(value: string): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
2
3
|
interface PosCartLine {
|
|
3
4
|
productId: string
|
|
4
5
|
name: string
|
|
@@ -17,12 +18,7 @@ const totalAmount = useReactiveProp('totalAmount', 0)
|
|
|
17
18
|
const emit = defineEmits()
|
|
18
19
|
|
|
19
20
|
function formatMoney(value: number): string {
|
|
20
|
-
|
|
21
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
25
|
-
}
|
|
21
|
+
return formatCurrency(value, currency())
|
|
26
22
|
}
|
|
27
23
|
</script>
|
|
28
24
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommercePosProduct } from '../../../../app/Actions/Dashboard/Commerce/commerce-pos'
|
|
3
3
|
import type { ProductOption } from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
4
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
4
5
|
|
|
5
6
|
const products = useReactiveProp('products', [] as CommercePosProduct[])
|
|
6
7
|
const categories = useReactiveProp('categories', [] as ProductOption[])
|
|
@@ -16,12 +17,7 @@ function initials(name: string): string {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function formatMoney(value: number): string {
|
|
19
|
-
|
|
20
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
24
|
-
}
|
|
20
|
+
return formatCurrency(value, currency())
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
function sellable(product: CommercePosProduct): boolean {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommercePosCustomer } from '../../../../app/Actions/Dashboard/Commerce/commerce-pos'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
interface PosPaymentMethod {
|
|
5
6
|
id: string
|
|
@@ -38,12 +39,7 @@ effect(() => {
|
|
|
38
39
|
})
|
|
39
40
|
|
|
40
41
|
function formatMoney(value: number): string {
|
|
41
|
-
|
|
42
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
46
|
-
}
|
|
42
|
+
return formatCurrency(value, currency())
|
|
47
43
|
}
|
|
48
44
|
|
|
49
45
|
function submit(): void {
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
import type { ProductOption } from '../../../../app/Actions/Dashboard/Commerce/commerce-product-records'
|
|
7
7
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
8
8
|
import { pushToast } from '../../../../functions/toasts'
|
|
9
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
9
10
|
|
|
10
11
|
interface PosPaymentMethod {
|
|
11
12
|
id: string
|
|
@@ -91,8 +92,10 @@ const checkoutKey = state('')
|
|
|
91
92
|
const receipt = state<PosReceipt | null>(null)
|
|
92
93
|
const receiptOpen = state(false)
|
|
93
94
|
|
|
95
|
+
// Amounts are integer minor units (cents for USD). Only tax can produce a
|
|
96
|
+
// fraction of one, and it rounds to the nearest unit as the server does.
|
|
94
97
|
function money(value: number): number {
|
|
95
|
-
return Math.round(
|
|
98
|
+
return Math.round(value)
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
function cartUnits(): number {
|
|
@@ -112,12 +115,7 @@ function totalAmount(): number {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
function formatMoney(value: number): string {
|
|
115
|
-
|
|
116
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: defaultCurrency() }).format(value)
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return `${value.toFixed(2)} ${defaultCurrency()}`
|
|
120
|
-
}
|
|
118
|
+
return formatCurrency(value, defaultCurrency())
|
|
121
119
|
}
|
|
122
120
|
|
|
123
121
|
function visibleProducts(): CommercePosProduct[] {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script client>
|
|
2
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
2
3
|
interface PosReceiptLine {
|
|
3
4
|
productId: number
|
|
4
5
|
name: string
|
|
@@ -25,14 +26,7 @@ const receipt = useReactiveProp('receipt', null as PosReceipt | null)
|
|
|
25
26
|
const emit = defineEmits()
|
|
26
27
|
|
|
27
28
|
function formatMoney(value: number): string {
|
|
28
|
-
|
|
29
|
-
const currency = current?.currency || 'USD'
|
|
30
|
-
try {
|
|
31
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return `${value.toFixed(2)} ${currency}`
|
|
35
|
-
}
|
|
29
|
+
return formatCurrency(value, receipt()?.currency || 'USD')
|
|
36
30
|
}
|
|
37
31
|
|
|
38
32
|
function formatDate(value: string): string {
|
|
@@ -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 {
|