@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
// The extension is its own little package: a Node (CommonJS) bundle loaded by
|
|
3
|
+
// VS Code, not part of the app or the framework, so no other tsconfig
|
|
4
|
+
// includes it. `bun run typecheck` here checks it.
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "es2022",
|
|
7
|
+
"lib": ["es2022"],
|
|
8
|
+
"module": "esnext",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"types": ["bun"],
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.41",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
|
-
"@iconify-json/hugeicons": "^1.2.
|
|
58
|
-
"@stacksjs/mobile": "^0.75.
|
|
59
|
-
"@stacksjs/sanitizer": "^0.2.
|
|
57
|
+
"@iconify-json/hugeicons": "^1.2.35",
|
|
58
|
+
"@stacksjs/mobile": "^0.75.41",
|
|
59
|
+
"@stacksjs/sanitizer": "^0.2.348",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { AnalyticsRange, CampaignAnalyticsResponse } from '../../../../functions/analytics'
|
|
3
3
|
import { fetchCampaignAnalytics } from '../../../../functions/analytics'
|
|
4
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
4
5
|
import { pushToast } from '../../../../functions/toasts'
|
|
5
6
|
|
|
6
7
|
const analytics = state<CampaignAnalyticsResponse | null>(null)
|
|
@@ -15,10 +16,9 @@ function formatNumber(value: number | null | undefined): string {
|
|
|
15
16
|
: new Intl.NumberFormat().format(value)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
: new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
19
|
+
/** Campaign budgets and spend are integer minor units of `currency` (1999 is $19.99). */
|
|
20
|
+
function formatMoney(value: number | null, currency: string): string {
|
|
21
|
+
return value === null ? 'Not recorded' : formatCurrency(value, currency)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function formatRate(value: number | null | undefined): string {
|
|
@@ -165,7 +165,7 @@ onMount(() => {
|
|
|
165
165
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(campaign.opens) }}</td>
|
|
166
166
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(campaign.clicks) }}</td>
|
|
167
167
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(campaign.conversions) }}</td>
|
|
168
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{
|
|
168
|
+
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatMoney(campaign.spent, campaign.currency) }}</td>
|
|
169
169
|
</tr>
|
|
170
170
|
</template>
|
|
171
171
|
</tbody>
|
|
@@ -186,8 +186,8 @@ onMount(() => {
|
|
|
186
186
|
<p class="text-gray-500 text-xs dark:text-neutral-400">{{ spend.campaigns }} campaigns</p>
|
|
187
187
|
</div>
|
|
188
188
|
<div class="text-right">
|
|
189
|
-
<p class="font-medium text-gray-900 text-sm dark:text-white">{{
|
|
190
|
-
<p class="text-gray-500 text-xs dark:text-neutral-400">of {{
|
|
189
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ formatMoney(spend.spent, spend.currency) }}</p>
|
|
190
|
+
<p class="text-gray-500 text-xs dark:text-neutral-400">of {{ formatMoney(spend.budget, spend.currency) }}</p>
|
|
191
191
|
</div>
|
|
192
192
|
</div>
|
|
193
193
|
</template>
|
|
@@ -233,7 +233,7 @@ onMount(() => {
|
|
|
233
233
|
<td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-300">{{ channel.currency }}</td>
|
|
234
234
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ channel.campaigns }}</td>
|
|
235
235
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatNumber(channel.sent) }}</td>
|
|
236
|
-
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{
|
|
236
|
+
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatMoney(channel.spent, channel.currency) }}</td>
|
|
237
237
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatRate(channel.clickRate) }}</td>
|
|
238
238
|
<td class="px-4 py-3 text-gray-500 text-right text-sm dark:text-neutral-300">{{ formatRate(channel.conversionRate) }}</td>
|
|
239
239
|
</tr>
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { AnalyticsRange, SalesAnalyticsResponse } from '../../../../functions/analytics'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
import { fetchSalesAnalytics } from '../../../../functions/analytics'
|
|
4
5
|
import { pushToast } from '../../../../functions/toasts'
|
|
5
6
|
|
|
7
|
+
// Every amount on this page is a sum of order, payment and line-item columns,
|
|
8
|
+
// which hold integer minor units. formatCurrency converts them for display
|
|
9
|
+
// (stacksjs/stacks#2851).
|
|
6
10
|
const analytics = state<SalesAnalyticsResponse | null>(null)
|
|
7
11
|
const analyticsRange = state<AnalyticsRange>('month')
|
|
8
12
|
const dateRangeDisplay = state('Last 30 days')
|
|
@@ -13,9 +17,6 @@ function formatNumber(value: number): string {
|
|
|
13
17
|
return new Intl.NumberFormat().format(value)
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
function formatCurrency(value: number, currency: string): string {
|
|
17
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
18
|
-
}
|
|
19
20
|
|
|
20
21
|
function formatDateRange(start: string, end: string): string {
|
|
21
22
|
const formatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { BillingSubscription } from '../../../../functions/billing/dashboard'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const subscription = useReactiveProp<BillingSubscription | null>('subscription', null)
|
|
5
6
|
|
|
@@ -16,7 +17,9 @@ function amount(): string {
|
|
|
16
17
|
return 'Not reported'
|
|
17
18
|
if (!currency)
|
|
18
19
|
return `${new Intl.NumberFormat().format(value)} minor units`
|
|
19
|
-
|
|
20
|
+
// Provider amounts are integer minor units: dividing by 100 was a hundred
|
|
21
|
+
// times too small for a zero-decimal currency such as JPY.
|
|
22
|
+
return formatCurrency(value, currency)
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function date(value: string): string {
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CouponRecord } from '../../../../app/Actions/Dashboard/Commerce/coupon-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const record = useReactiveProp('record', null as CouponRecord | null)
|
|
7
|
+
const currency = useReactiveProp('currency', 'USD')
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
10
|
+
// Fixed discounts and order thresholds are integer minor units.
|
|
8
11
|
function money(value: number): string {
|
|
9
|
-
return
|
|
12
|
+
return formatCurrency(value, currency())
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
function discount(current: CouponRecord | null): string {
|
|
@@ -4,10 +4,12 @@ import type {
|
|
|
4
4
|
CouponRecord,
|
|
5
5
|
CouponStatus,
|
|
6
6
|
} from '../../../../app/Actions/Dashboard/Commerce/coupon-records'
|
|
7
|
+
import { minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
7
8
|
|
|
8
9
|
const open = useReactiveProp('open', false)
|
|
9
10
|
const mode = useReactiveProp('mode', 'add')
|
|
10
11
|
const record = useReactiveProp('record', null as CouponRecord | null)
|
|
12
|
+
const currency = useReactiveProp('currency', 'USD')
|
|
11
13
|
const busy = useReactiveProp('busy', false)
|
|
12
14
|
const errors = useReactiveProp('errors', {} as Record<string, string>)
|
|
13
15
|
const emit = defineEmits()
|
|
@@ -40,9 +42,13 @@ effect(() => {
|
|
|
40
42
|
status.set(current?.status || 'Active')
|
|
41
43
|
isActive.set(current?.isActive ?? true)
|
|
42
44
|
discountType.set(current?.discountType || 'percentage')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// A percentage is shown as stored; money fields are stored as integer
|
|
46
|
+
// minor units and shown as the decimal.
|
|
47
|
+
discountValue.set(current
|
|
48
|
+
? current.discountType === 'fixed_amount' ? minorToInput(current.discountValue, currency()) : String(current.discountValue)
|
|
49
|
+
: '10')
|
|
50
|
+
minOrderAmount.set(current?.minOrderAmount ? minorToInput(current.minOrderAmount, currency()) : '')
|
|
51
|
+
maxDiscountAmount.set(current?.maxDiscountAmount ? minorToInput(current.maxDiscountAmount, currency()) : '')
|
|
46
52
|
freeProductId.set(current?.freeProductId || '')
|
|
47
53
|
usageLimit.set(current?.usageLimit ? String(current.usageLimit) : '')
|
|
48
54
|
startDate.set(current?.startDate || '')
|
|
@@ -56,8 +62,24 @@ function optionalNumber(value: string): number | undefined {
|
|
|
56
62
|
return Number.isFinite(result) ? result : undefined
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
function optionalMoney(value: string): number | undefined {
|
|
66
|
+
return parseMoneyInput(value, currency(), { required: false }) ?? undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function discountValueValid(): boolean {
|
|
70
|
+
if (discountType() === 'fixed_amount')
|
|
71
|
+
return !moneyInputError(discountValue(), currency(), { min: 1 })
|
|
72
|
+
const rate = Number(discountValue())
|
|
73
|
+
return Number.isFinite(rate) && rate >= 0.01 && rate <= 100
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
function valid(): boolean {
|
|
60
|
-
return Boolean(
|
|
77
|
+
return Boolean(
|
|
78
|
+
code().trim()
|
|
79
|
+
&& discountValueValid()
|
|
80
|
+
&& !moneyInputError(minOrderAmount(), currency(), { required: false })
|
|
81
|
+
&& !moneyInputError(maxDiscountAmount(), currency(), { required: false }),
|
|
82
|
+
)
|
|
61
83
|
}
|
|
62
84
|
|
|
63
85
|
function submit(): void {
|
|
@@ -69,9 +91,11 @@ function submit(): void {
|
|
|
69
91
|
status: status(),
|
|
70
92
|
isActive: isActive(),
|
|
71
93
|
discountType: discountType(),
|
|
72
|
-
discountValue:
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
discountValue: discountType() === 'fixed_amount'
|
|
95
|
+
? parseMoneyInput(discountValue(), currency(), { min: 1 })
|
|
96
|
+
: Number(discountValue()),
|
|
97
|
+
minOrderAmount: optionalMoney(minOrderAmount()),
|
|
98
|
+
maxDiscountAmount: optionalMoney(maxDiscountAmount()),
|
|
75
99
|
freeProductId: freeProductId().trim() || undefined,
|
|
76
100
|
usageLimit: optionalNumber(usageLimit()),
|
|
77
101
|
startDate: startDate() || undefined,
|
|
@@ -103,9 +127,9 @@ function errorMessages(): string[] {
|
|
|
103
127
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span><select x-model="status" 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"><option value="Active">Active</option><option value="Scheduled">Scheduled</option><option value="Expired">Expired</option></select></label>
|
|
104
128
|
<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>
|
|
105
129
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Discount type</span><select x-model="discountType" 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"><option value="percentage">Percentage</option><option value="fixed_amount">Fixed amount</option></select></label>
|
|
106
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Discount value</span><input x-model="discountValue" type="number" min="0.01" step="0.01" 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>
|
|
107
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Minimum order amount</span><input x-model="minOrderAmount" type="number" min="0" step="
|
|
108
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Maximum discount amount</span><input x-model="maxDiscountAmount" type="number" min="0" step="
|
|
130
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Discount value</span><input x-model="discountValue" type="number" :min="discountType() === 'fixed_amount' ? '0' : '0.01'" :step="discountType() === 'fixed_amount' ? moneyInputStep(currency()) : '0.01'" 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>
|
|
131
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Minimum order amount</span><input x-model="minOrderAmount" type="number" min="0" :step="moneyInputStep(currency())" placeholder="No minimum" 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
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Maximum discount amount</span><input x-model="maxDiscountAmount" type="number" min="0" :step="moneyInputStep(currency())" placeholder="No maximum" 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>
|
|
109
133
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Usage limit</span><input x-model="usageLimit" type="number" min="1" step="1" placeholder="No limit" 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>
|
|
110
134
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Free product ID</span><input x-model="freeProductId" type="text" placeholder="Optional" 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>
|
|
111
135
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Start date</span><input x-model="startDate" type="date" 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>
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from '../../../../app/Actions/Dashboard/Commerce/coupon-records'
|
|
8
8
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
9
9
|
import { pushToast } from '../../../../functions/toasts'
|
|
10
|
+
import { minorToInput } from '@stacksjs/commerce/money'
|
|
10
11
|
|
|
11
12
|
interface CouponIndexResponse {
|
|
12
13
|
records: CouponRecord[]
|
|
@@ -104,7 +105,8 @@ function exportHref(): string {
|
|
|
104
105
|
record.status,
|
|
105
106
|
record.isActive ? 'Enabled' : 'Disabled',
|
|
106
107
|
record.discountType,
|
|
107
|
-
|
|
108
|
+
// A fixed amount is stored in minor units; the CSV carries the decimal.
|
|
109
|
+
record.discountType === 'fixed_amount' ? minorToInput(record.discountValue, 'USD') : String(record.discountValue),
|
|
108
110
|
String(record.usageCount),
|
|
109
111
|
record.usageLimit === null ? '' : String(record.usageLimit),
|
|
110
112
|
record.startDate,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CouponRecord } from '../../../../app/Actions/Dashboard/Commerce/coupon-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as CouponRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
7
|
+
const currency = useReactiveProp('currency', 'USD')
|
|
6
8
|
const emit = defineEmits()
|
|
7
9
|
|
|
8
10
|
function statusClass(status: string): string {
|
|
@@ -13,10 +15,11 @@ function statusClass(status: string): string {
|
|
|
13
15
|
return 'bg-gray-100 text-gray-700 dark:bg-neutral-700 dark:text-neutral-300'
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// A percentage is a rate, not money; a fixed amount is integer minor units.
|
|
16
19
|
function discount(record: CouponRecord): string {
|
|
17
20
|
return record.discountType === 'percentage'
|
|
18
21
|
? `${record.discountValue}%`
|
|
19
|
-
:
|
|
22
|
+
: formatCurrency(record.discountValue, currency())
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function date(value: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommerceCustomerRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-customer-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const record = useReactiveProp('record', null as CommerceCustomerRecord | null)
|
|
@@ -7,12 +8,7 @@ const currency = useReactiveProp('currency', 'USD')
|
|
|
7
8
|
const emit = defineEmits()
|
|
8
9
|
|
|
9
10
|
function formatMoney(value: number): string {
|
|
10
|
-
|
|
11
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
15
|
-
}
|
|
11
|
+
return formatCurrency(value, currency())
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
function formatDate(value: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommerceCustomerRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-customer-records'
|
|
3
|
+
import { minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const mode = useReactiveProp('mode', 'add')
|
|
@@ -30,7 +31,8 @@ effect(() => {
|
|
|
30
31
|
name.set(current?.name || '')
|
|
31
32
|
email.set(current?.email || '')
|
|
32
33
|
phone.set(current?.phone || '')
|
|
33
|
-
|
|
34
|
+
// Stored as integer minor units; the field shows the decimal.
|
|
35
|
+
totalSpent.set(minorToInput(current?.totalSpent ?? 0, currency()))
|
|
34
36
|
lastOrder.set(current?.lastOrder.slice(0, 10) || '')
|
|
35
37
|
status.set(current?.status === 'Inactive' ? 'Inactive' : 'Active')
|
|
36
38
|
avatar.set(current?.avatar || '')
|
|
@@ -47,12 +49,10 @@ function validAvatar(): boolean {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function valid(): boolean {
|
|
50
|
-
const spend = Number(totalSpent())
|
|
51
52
|
return Boolean(
|
|
52
53
|
name().trim().length >= 2
|
|
53
54
|
&& email().trim()
|
|
54
|
-
&&
|
|
55
|
-
&& spend >= 0
|
|
55
|
+
&& !moneyInputError(totalSpent(), currency())
|
|
56
56
|
&& validAvatar()
|
|
57
57
|
)
|
|
58
58
|
}
|
|
@@ -64,7 +64,7 @@ function submit(): void {
|
|
|
64
64
|
name: name().trim(),
|
|
65
65
|
email: email().trim(),
|
|
66
66
|
phone: phone().trim(),
|
|
67
|
-
totalSpent:
|
|
67
|
+
totalSpent: parseMoneyInput(totalSpent(), currency()),
|
|
68
68
|
lastOrder: lastOrder() || undefined,
|
|
69
69
|
status: status(),
|
|
70
70
|
avatar: avatar().trim(),
|
|
@@ -95,7 +95,7 @@ function errorMessages(): string[] {
|
|
|
95
95
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Email</span><input x-model="email" type="email" 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>
|
|
96
96
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Phone <span class="font-normal text-gray-400 dark:text-neutral-500">(optional)</span></span><input x-model="phone" type="tel" maxlength="50" 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>
|
|
97
97
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span><select x-model="status" 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"><option value="Active">Active</option><option value="Inactive">Inactive</option></select></label>
|
|
98
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Recorded spend ({{ currency() }})</span><input x-model="totalSpent" type="number" min="0" step="
|
|
98
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Recorded spend ({{ currency() }})</span><input x-model="totalSpent" 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>
|
|
99
99
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Last order</span><input x-model="lastOrder" type="date" 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>
|
|
100
100
|
<label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Avatar URL</span><input x-model="avatar" type="url" placeholder="https://cdn.example.com/customers/avatar.jpg" 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" /><span class="block mt-1 text-gray-400 text-xs dark:text-neutral-500">Use a persisted HTTP asset URL. The dashboard uses initials when rendering the table.</span></label>
|
|
101
101
|
</div>
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
} from '../../../../app/Actions/Dashboard/Commerce/commerce-customer-records'
|
|
6
6
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
7
7
|
import { pushToast } from '../../../../functions/toasts'
|
|
8
|
+
import { formatCurrency, minorToInput } from '@stacksjs/commerce/money'
|
|
8
9
|
|
|
9
10
|
interface CustomerIndexResponse {
|
|
10
11
|
records: CommerceCustomerRecord[]
|
|
@@ -59,16 +60,7 @@ function timestamp(value: string): number {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
function formatMoney(value: number): string {
|
|
62
|
-
|
|
63
|
-
return new Intl.NumberFormat(undefined, {
|
|
64
|
-
style: 'currency',
|
|
65
|
-
currency: currency(),
|
|
66
|
-
maximumFractionDigits: 2,
|
|
67
|
-
}).format(value)
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
71
|
-
}
|
|
63
|
+
return formatCurrency(value, currency())
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
function filteredRecords(): CommerceCustomerRecord[] {
|
|
@@ -122,7 +114,7 @@ function exportHref(): string {
|
|
|
122
114
|
record.email,
|
|
123
115
|
record.phone,
|
|
124
116
|
record.status,
|
|
125
|
-
|
|
117
|
+
minorToInput(record.totalSpent, currency()),
|
|
126
118
|
record.lastOrder,
|
|
127
119
|
record.avatar,
|
|
128
120
|
])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { CommerceCustomerRecord } from '../../../../app/Actions/Dashboard/Commerce/commerce-customer-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const records = useReactiveProp('records', [] as CommerceCustomerRecord[])
|
|
5
6
|
const loading = useReactiveProp('loading', false)
|
|
@@ -7,12 +8,7 @@ const currency = useReactiveProp('currency', 'USD')
|
|
|
7
8
|
const emit = defineEmits()
|
|
8
9
|
|
|
9
10
|
function formatMoney(value: number): string {
|
|
10
|
-
|
|
11
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency() }).format(value)
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return `${value.toFixed(2)} ${currency()}`
|
|
15
|
-
}
|
|
11
|
+
return formatCurrency(value, currency())
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
function formatDate(value: string): string {
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { GiftCardSummary } from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const { summary } = defineProps<{ summary: GiftCardSummary }>()
|
|
5
6
|
|
|
6
7
|
function money(value: number, currency: string): string {
|
|
7
|
-
|
|
8
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return `${currency} ${value.toFixed(2)}`
|
|
12
|
-
}
|
|
8
|
+
return formatCurrency(value, currency)
|
|
13
9
|
}
|
|
14
10
|
</script>
|
|
15
11
|
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { GiftCardRecord } from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const record = useReactiveProp('record', null as GiftCardRecord | null)
|
|
6
7
|
const emit = defineEmits()
|
|
7
8
|
|
|
8
9
|
function money(value: number, currency: string): string {
|
|
9
|
-
|
|
10
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return `${currency} ${value.toFixed(2)}`
|
|
14
|
-
}
|
|
10
|
+
return formatCurrency(value, currency)
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
function dateTime(value: string): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { GiftCardRecord, GiftCardStatus } from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
|
|
3
|
+
import { minorToInput, moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const open = useReactiveProp('open', false)
|
|
5
6
|
const mode = useReactiveProp('mode', 'add')
|
|
@@ -8,8 +9,9 @@ const busy = useReactiveProp('busy', false)
|
|
|
8
9
|
const errors = useReactiveProp('errors', {} as Record<string, string>)
|
|
9
10
|
const emit = defineEmits()
|
|
10
11
|
const code = state('')
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
// Balances as typed, in major units. Stored as integer minor units.
|
|
13
|
+
const initialBalance = state('50.00')
|
|
14
|
+
const currentBalance = state('50.00')
|
|
13
15
|
const currency = state('USD')
|
|
14
16
|
const status = state<GiftCardStatus>('ACTIVE')
|
|
15
17
|
const purchaserId = state('')
|
|
@@ -24,7 +26,7 @@ const lastUsedDate = state('')
|
|
|
24
26
|
const templateId = state('')
|
|
25
27
|
const customerId = state('')
|
|
26
28
|
let hydrationKey = ''
|
|
27
|
-
let previousInitialBalance = '50'
|
|
29
|
+
let previousInitialBalance = '50.00'
|
|
28
30
|
|
|
29
31
|
function generatedCode(): string {
|
|
30
32
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
@@ -61,11 +63,12 @@ effect(() => {
|
|
|
61
63
|
return
|
|
62
64
|
hydrationKey = key
|
|
63
65
|
code.set(current?.code || generatedCode())
|
|
64
|
-
const
|
|
66
|
+
const cardCurrency = current?.currency || 'USD'
|
|
67
|
+
const hydratedInitialBalance = current?.initialBalance ? minorToInput(current.initialBalance, cardCurrency) : '50.00'
|
|
65
68
|
initialBalance.set(hydratedInitialBalance)
|
|
66
|
-
currentBalance.set(
|
|
69
|
+
currentBalance.set(current ? minorToInput(current.currentBalance ?? current.initialBalance, cardCurrency) : hydratedInitialBalance)
|
|
67
70
|
previousInitialBalance = hydratedInitialBalance
|
|
68
|
-
currency.set(
|
|
71
|
+
currency.set(cardCurrency)
|
|
69
72
|
status.set(current?.status || 'ACTIVE')
|
|
70
73
|
purchaserId.set(current?.purchaserId || '')
|
|
71
74
|
recipientEmail.set(current?.recipientEmail || '')
|
|
@@ -102,13 +105,13 @@ function optionalCustomerId(): number | undefined {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
function balanceError(): string {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
return
|
|
111
|
-
if (
|
|
108
|
+
const initialError = moneyInputError(initialBalance(), currency(), { min: 1 })
|
|
109
|
+
if (initialError)
|
|
110
|
+
return `Initial balance: ${initialError}`
|
|
111
|
+
const currentError = moneyInputError(currentBalance(), currency())
|
|
112
|
+
if (currentError)
|
|
113
|
+
return `Current balance: ${currentError}`
|
|
114
|
+
if (parseMoneyInput(currentBalance(), currency()) > parseMoneyInput(initialBalance(), currency()))
|
|
112
115
|
return 'Current balance cannot exceed the initial balance.'
|
|
113
116
|
return ''
|
|
114
117
|
}
|
|
@@ -126,8 +129,8 @@ function submit(): void {
|
|
|
126
129
|
return
|
|
127
130
|
emit('submit', {
|
|
128
131
|
code: code().trim().toUpperCase(),
|
|
129
|
-
initialBalance:
|
|
130
|
-
currentBalance:
|
|
132
|
+
initialBalance: parseMoneyInput(initialBalance(), currency()),
|
|
133
|
+
currentBalance: parseMoneyInput(currentBalance(), currency()),
|
|
131
134
|
currency: currency().trim().toUpperCase(),
|
|
132
135
|
status: status(),
|
|
133
136
|
purchaser_id: purchaserId().trim() || undefined,
|
|
@@ -165,8 +168,8 @@ function errorMessages(): string[] {
|
|
|
165
168
|
</ul>
|
|
166
169
|
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
|
167
170
|
<label class="block sm:col-span-2"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Code</span><input x-model="code" type="text" maxlength="50" 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>
|
|
168
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Initial balance</span><input x-model="initialBalance" type="number" min="
|
|
169
|
-
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Current balance</span><input x-model="currentBalance" :max="initialBalance()" type="number" min="0" step="
|
|
171
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Initial balance</span><input x-model="initialBalance" 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" @input="syncCurrentBalance($event)" /></label>
|
|
172
|
+
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Current balance</span><input x-model="currentBalance" :max="initialBalance()" 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>
|
|
170
173
|
<p :if="balanceError()" class="sm:col-span-2 -mt-2 text-red-600 text-xs dark:text-red-400">{{ balanceError() }}</p>
|
|
171
174
|
<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>
|
|
172
175
|
<label class="block"><span class="font-medium text-gray-700 text-sm dark:text-neutral-200">Status</span><select x-model="status" 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"><option value="ACTIVE">Active</option><option value="USED">Used</option><option value="EXPIRED">Expired</option><option value="DEACTIVATED">Deactivated</option></select></label>
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
|
|
7
7
|
import { DashboardApiError, dashboardApi } from '../../../../functions/dashboard-api'
|
|
8
8
|
import { pushToast } from '../../../../functions/toasts'
|
|
9
|
+
import { minorToInput } from '@stacksjs/commerce/money'
|
|
9
10
|
|
|
10
11
|
interface GiftCardIndexResponse {
|
|
11
12
|
records: GiftCardRecord[]
|
|
@@ -125,8 +126,8 @@ function exportHref(): string {
|
|
|
125
126
|
record.isDigital ? 'Digital' : 'Physical',
|
|
126
127
|
record.isReloadable ? 'Reloadable' : 'Single use',
|
|
127
128
|
record.currency,
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
minorToInput(record.initialBalance, record.currency),
|
|
130
|
+
minorToInput(record.currentBalance, record.currency),
|
|
130
131
|
record.expiryDate,
|
|
131
132
|
record.lastUsedDate,
|
|
132
133
|
])
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { GiftCardRecord, GiftCardStatus } from '../../../../app/Actions/Dashboard/Commerce/gift-card-records'
|
|
3
|
+
import { formatCurrency } from '@stacksjs/commerce/money'
|
|
3
4
|
|
|
4
5
|
const { records = [], loading = false } = defineProps<{ records: GiftCardRecord[], loading?: boolean }>()
|
|
5
6
|
const emit = defineEmits<{
|
|
@@ -9,12 +10,7 @@ const emit = defineEmits<{
|
|
|
9
10
|
}>()
|
|
10
11
|
|
|
11
12
|
function money(value: number, currency: string): string {
|
|
12
|
-
|
|
13
|
-
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return `${currency} ${value.toFixed(2)}`
|
|
17
|
-
}
|
|
13
|
+
return formatCurrency(value, currency)
|
|
18
14
|
}
|
|
19
15
|
|
|
20
16
|
function date(value: string): string {
|
|
@@ -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 open = useReactiveProp('open', false)
|
|
5
6
|
const record = useReactiveProp('record', null as CommerceOrderRecord | null)
|
|
@@ -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 {
|