@base44/app-plugin-commerce 0.1.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
- package/base44/entities/commerce.Cart.jsonc +73 -0
- package/base44/entities/commerce.Coupon.jsonc +113 -0
- package/base44/entities/commerce.Customer.jsonc +96 -0
- package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
- package/base44/entities/commerce.EmailLog.jsonc +43 -0
- package/base44/entities/commerce.Order.jsonc +287 -0
- package/base44/entities/commerce.OrderNote.jsonc +31 -0
- package/base44/entities/commerce.OrderRefund.jsonc +64 -0
- package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
- package/base44/entities/commerce.Product.jsonc +291 -0
- package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
- package/base44/entities/commerce.ProductCategory.jsonc +51 -0
- package/base44/entities/commerce.ProductReview.jsonc +48 -0
- package/base44/entities/commerce.ProductTag.jsonc +30 -0
- package/base44/entities/commerce.ProductVariation.jsonc +167 -0
- package/base44/entities/commerce.ShippingClass.jsonc +30 -0
- package/base44/entities/commerce.ShippingZone.jsonc +41 -0
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
- package/base44/entities/commerce.StoreSettings.jsonc +23 -0
- package/base44/entities/commerce.TaxClass.jsonc +23 -0
- package/base44/entities/commerce.TaxRate.jsonc +68 -0
- package/base44/entities/commerce.Webhook.jsonc +57 -0
- package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
- package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
- package/base44/functions/commerce/admin-customers/entry.ts +141 -0
- package/base44/functions/commerce/admin-orders/entry.ts +396 -0
- package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
- package/base44/functions/commerce/admin-products/entry.ts +506 -0
- package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
- package/base44/functions/commerce/admin-reports/entry.ts +283 -0
- package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
- package/base44/functions/commerce/admin-tools/entry.ts +261 -0
- package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
- package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
- package/base44/functions/commerce/payments/entry.ts +238 -0
- package/base44/functions/commerce/seed-store/defaults.ts +162 -0
- package/base44/functions/commerce/seed-store/entry.ts +310 -0
- package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
- package/base44/functions/commerce/storefront-account/entry.ts +207 -0
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
- package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
- package/base44/shared/commerce/auth.ts +60 -0
- package/base44/shared/commerce/coupons.ts +257 -0
- package/base44/shared/commerce/data/continents.ts +75 -0
- package/base44/shared/commerce/data/countries.ts +307 -0
- package/base44/shared/commerce/data/currencies.ts +46 -0
- package/base44/shared/commerce/email-templates.ts +240 -0
- package/base44/shared/commerce/emails.ts +225 -0
- package/base44/shared/commerce/money.ts +66 -0
- package/base44/shared/commerce/orders.ts +251 -0
- package/base44/shared/commerce/payments.ts +495 -0
- package/base44/shared/commerce/reviews.ts +36 -0
- package/base44/shared/commerce/scan.ts +57 -0
- package/base44/shared/commerce/sequence.ts +35 -0
- package/base44/shared/commerce/settings.ts +57 -0
- package/base44/shared/commerce/shipping.ts +215 -0
- package/base44/shared/commerce/stock.ts +227 -0
- package/base44/shared/commerce/stripe.ts +463 -0
- package/base44/shared/commerce/tax.ts +136 -0
- package/base44/shared/commerce/totals.ts +314 -0
- package/base44/shared/commerce/webhooks.ts +116 -0
- package/package.json +37 -0
- package/scripts/install.js +156 -0
- package/skills/commerce/SKILL.md +62 -0
- package/skills/commerce/docs/api-admin.md +186 -0
- package/skills/commerce/docs/api-storefront.md +408 -0
- package/skills/commerce/installation-guidelines.md +91 -0
- package/skills/commerce/post-installation.md +157 -0
- package/skills/commerce/references/emails.md +13 -0
- package/skills/commerce/references/guest-access-security.md +18 -0
- package/skills/commerce/references/limits-and-performance.md +16 -0
- package/skills/commerce/references/media-and-downloads.md +4 -0
- package/skills/commerce/references/online-payments.md +201 -0
- package/skills/commerce/references/product-render.md +87 -0
- package/skills/commerce/references/scheduled-work.md +19 -0
- package/skills/commerce/references/storefront-product-page.md +83 -0
- package/skills/commerce/references/webhooks.md +8 -0
- package/src/commerce/admin/README.md +107 -0
- package/src/commerce/admin/bot/Markdown.jsx +138 -0
- package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
- package/src/commerce/admin/bot/pipe-tables.js +116 -0
- package/src/commerce/admin/components/AddressForm.jsx +78 -0
- package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
- package/src/commerce/admin/components/CountrySelect.jsx +81 -0
- package/src/commerce/admin/components/DataTable.jsx +192 -0
- package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
- package/src/commerce/admin/components/EmptyState.jsx +17 -0
- package/src/commerce/admin/components/MediaUploader.jsx +116 -0
- package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
- package/src/commerce/admin/components/MoneyInput.jsx +50 -0
- package/src/commerce/admin/components/PageHeader.jsx +29 -0
- package/src/commerce/admin/components/RichTextarea.jsx +21 -0
- package/src/commerce/admin/components/SearchSelect.jsx +142 -0
- package/src/commerce/admin/components/StatusBadge.jsx +17 -0
- package/src/commerce/admin/context/BasePathContext.jsx +26 -0
- package/src/commerce/admin/context/SettingsContext.jsx +207 -0
- package/src/commerce/admin/hooks/useAsync.js +46 -0
- package/src/commerce/admin/hooks/useDebounce.js +11 -0
- package/src/commerce/admin/hooks/useMoney.js +52 -0
- package/src/commerce/admin/hooks/usePagedList.js +83 -0
- package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
- package/src/commerce/admin/hooks/useRealtime.js +129 -0
- package/src/commerce/admin/index.jsx +34 -0
- package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
- package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
- package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
- package/src/commerce/admin/layout/Sidebar.jsx +130 -0
- package/src/commerce/admin/layout/Topbar.jsx +94 -0
- package/src/commerce/admin/lib/api.js +55 -0
- package/src/commerce/admin/lib/constants.js +157 -0
- package/src/commerce/admin/lib/format.js +27 -0
- package/src/commerce/admin/lib/geo-data.js +125 -0
- package/src/commerce/admin/lib/order-utils.js +147 -0
- package/src/commerce/admin/lib/paths.js +35 -0
- package/src/commerce/admin/lib/product-utils.js +55 -0
- package/src/commerce/admin/pages/Dashboard.jsx +245 -0
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
- package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
- package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
- package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
- package/src/commerce/admin/pages/products/Categories.jsx +236 -0
- package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
- package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
- package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
- package/src/commerce/admin/pages/products/Tags.jsx +150 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
- package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
- package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
- package/src/commerce/admin/routes.jsx +151 -0
- package/src/commerce/utils/index.js +19 -0
- package/src/commerce/utils/shipping-promos.js +99 -0
- package/src/commerce/utils/variants.js +411 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Label } from "@/components/ui/label";
|
|
5
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from "@/components/ui/select";
|
|
13
|
+
import CountrySelect from "../../components/CountrySelect";
|
|
14
|
+
import { statesFor } from "../../lib/geo-data";
|
|
15
|
+
import { CURRENCIES, CURRENCY_POSITIONS } from "../../lib/constants";
|
|
16
|
+
import useGroupForm from "./useGroupForm";
|
|
17
|
+
|
|
18
|
+
const DEFAULTS = {
|
|
19
|
+
store_name: "",
|
|
20
|
+
store_url: "",
|
|
21
|
+
order_received_path: "/order-received",
|
|
22
|
+
address: { address_1: "", address_2: "", city: "", state: "", postcode: "", country: "US" },
|
|
23
|
+
enable_taxes: true,
|
|
24
|
+
enable_coupons: true,
|
|
25
|
+
calc_discounts_sequentially: false,
|
|
26
|
+
currency: "USD",
|
|
27
|
+
currency_position: "left",
|
|
28
|
+
thousand_sep: ",",
|
|
29
|
+
decimal_sep: ".",
|
|
30
|
+
num_decimals: 2,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function GeneralSettings() {
|
|
34
|
+
const { form, setField, SaveButton } = useGroupForm("general", DEFAULTS);
|
|
35
|
+
const address = form.address || DEFAULTS.address;
|
|
36
|
+
const states = statesFor(address.country);
|
|
37
|
+
|
|
38
|
+
const setAddress = (key, value) => setField("address", { ...address, [key]: value });
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="max-w-3xl space-y-4">
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
44
|
+
<div className="space-y-1.5">
|
|
45
|
+
<CardTitle>Store details</CardTitle>
|
|
46
|
+
<CardDescription>This is where your business is located. Tax rates and shipping rates use this address.</CardDescription>
|
|
47
|
+
</div>
|
|
48
|
+
<SaveButton />
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
51
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
52
|
+
<Label>Store name</Label>
|
|
53
|
+
<Input value={form.store_name || ""} onChange={(e) => setField("store_name", e.target.value)} />
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
56
|
+
<Label>Storefront URL</Label>
|
|
57
|
+
<Input
|
|
58
|
+
type="url"
|
|
59
|
+
inputMode="url"
|
|
60
|
+
placeholder="https://shop.example.com"
|
|
61
|
+
value={form.store_url || ""}
|
|
62
|
+
onChange={(e) => setField("store_url", e.target.value)}
|
|
63
|
+
/>
|
|
64
|
+
<p className="text-xs text-muted-foreground">
|
|
65
|
+
Where customers shop. Used to build the page a customer returns to after paying — leave it
|
|
66
|
+
empty and the app's own address is used.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="space-y-1.5 sm:col-span-2">
|
|
70
|
+
<Label>Payment return path</Label>
|
|
71
|
+
<Input
|
|
72
|
+
placeholder="/order-received"
|
|
73
|
+
value={form.order_received_path || ""}
|
|
74
|
+
onChange={(e) => setField("order_received_path", e.target.value)}
|
|
75
|
+
/>
|
|
76
|
+
<p className="text-xs text-muted-foreground">
|
|
77
|
+
The storefront route customers come back to after paying. It must match the page you built —
|
|
78
|
+
a wrong path sends paying customers to a 404.
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="space-y-1.5">
|
|
82
|
+
<Label>Address line 1</Label>
|
|
83
|
+
<Input value={address.address_1 || ""} onChange={(e) => setAddress("address_1", e.target.value)} />
|
|
84
|
+
</div>
|
|
85
|
+
<div className="space-y-1.5">
|
|
86
|
+
<Label>Address line 2</Label>
|
|
87
|
+
<Input value={address.address_2 || ""} onChange={(e) => setAddress("address_2", e.target.value)} />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="space-y-1.5">
|
|
90
|
+
<Label>City</Label>
|
|
91
|
+
<Input value={address.city || ""} onChange={(e) => setAddress("city", e.target.value)} />
|
|
92
|
+
</div>
|
|
93
|
+
<div className="space-y-1.5">
|
|
94
|
+
<Label>Postcode / ZIP</Label>
|
|
95
|
+
<Input value={address.postcode || ""} onChange={(e) => setAddress("postcode", e.target.value)} />
|
|
96
|
+
</div>
|
|
97
|
+
<div className="space-y-1.5">
|
|
98
|
+
<Label>Country</Label>
|
|
99
|
+
<CountrySelect
|
|
100
|
+
value={address.country}
|
|
101
|
+
onChange={(code) => setField("address", { ...address, country: code, state: "" })}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="space-y-1.5">
|
|
105
|
+
<Label>State / Province</Label>
|
|
106
|
+
{states ? (
|
|
107
|
+
<Select value={address.state || ""} onValueChange={(v) => setAddress("state", v)}>
|
|
108
|
+
<SelectTrigger>
|
|
109
|
+
<SelectValue placeholder="Select state…" />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
{states.map((st) => (
|
|
113
|
+
<SelectItem key={st.code} value={st.code}>
|
|
114
|
+
{st.name}
|
|
115
|
+
</SelectItem>
|
|
116
|
+
))}
|
|
117
|
+
</SelectContent>
|
|
118
|
+
</Select>
|
|
119
|
+
) : (
|
|
120
|
+
<Input value={address.state || ""} onChange={(e) => setAddress("state", e.target.value)} />
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</CardContent>
|
|
124
|
+
</Card>
|
|
125
|
+
|
|
126
|
+
<Card>
|
|
127
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
128
|
+
<div className="space-y-1.5">
|
|
129
|
+
<CardTitle>Store options</CardTitle>
|
|
130
|
+
<CardDescription>
|
|
131
|
+
Where you ship is controlled by your shipping zones — see Settings → Shipping.
|
|
132
|
+
</CardDescription>
|
|
133
|
+
</div>
|
|
134
|
+
<SaveButton />
|
|
135
|
+
</CardHeader>
|
|
136
|
+
<CardContent className="space-y-4">
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
<label className="flex items-center gap-2 text-sm">
|
|
139
|
+
<Checkbox
|
|
140
|
+
checked={!!form.enable_taxes}
|
|
141
|
+
onCheckedChange={(v) => setField("enable_taxes", !!v)}
|
|
142
|
+
/>
|
|
143
|
+
Enable tax rates and calculations
|
|
144
|
+
</label>
|
|
145
|
+
<label className="flex items-center gap-2 text-sm">
|
|
146
|
+
<Checkbox
|
|
147
|
+
checked={!!form.enable_coupons}
|
|
148
|
+
onCheckedChange={(v) => setField("enable_coupons", !!v)}
|
|
149
|
+
/>
|
|
150
|
+
Enable the use of coupon codes
|
|
151
|
+
</label>
|
|
152
|
+
<label className="flex items-center gap-2 text-sm">
|
|
153
|
+
<Checkbox
|
|
154
|
+
checked={!!form.calc_discounts_sequentially}
|
|
155
|
+
onCheckedChange={(v) => setField("calc_discounts_sequentially", !!v)}
|
|
156
|
+
/>
|
|
157
|
+
Calculate coupon discounts sequentially
|
|
158
|
+
</label>
|
|
159
|
+
</div>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
165
|
+
<div className="space-y-1.5">
|
|
166
|
+
<CardTitle>Currency options</CardTitle>
|
|
167
|
+
<CardDescription>Affects how prices are displayed in the admin and by storefront APIs.</CardDescription>
|
|
168
|
+
</div>
|
|
169
|
+
<SaveButton />
|
|
170
|
+
</CardHeader>
|
|
171
|
+
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
172
|
+
<div className="space-y-1.5">
|
|
173
|
+
<Label>Currency</Label>
|
|
174
|
+
<Select value={form.currency} onValueChange={(v) => setField("currency", v)}>
|
|
175
|
+
<SelectTrigger>
|
|
176
|
+
<SelectValue />
|
|
177
|
+
</SelectTrigger>
|
|
178
|
+
<SelectContent>
|
|
179
|
+
{CURRENCIES.map((c) => (
|
|
180
|
+
<SelectItem key={c.code} value={c.code}>
|
|
181
|
+
{c.name} ({c.symbol})
|
|
182
|
+
</SelectItem>
|
|
183
|
+
))}
|
|
184
|
+
</SelectContent>
|
|
185
|
+
</Select>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="space-y-1.5">
|
|
188
|
+
<Label>Currency position</Label>
|
|
189
|
+
<Select value={form.currency_position} onValueChange={(v) => setField("currency_position", v)}>
|
|
190
|
+
<SelectTrigger>
|
|
191
|
+
<SelectValue />
|
|
192
|
+
</SelectTrigger>
|
|
193
|
+
<SelectContent>
|
|
194
|
+
{CURRENCY_POSITIONS.map((p) => (
|
|
195
|
+
<SelectItem key={p.value} value={p.value}>
|
|
196
|
+
{p.label}
|
|
197
|
+
</SelectItem>
|
|
198
|
+
))}
|
|
199
|
+
</SelectContent>
|
|
200
|
+
</Select>
|
|
201
|
+
</div>
|
|
202
|
+
<div className="space-y-1.5">
|
|
203
|
+
<Label>Thousand separator</Label>
|
|
204
|
+
<Input
|
|
205
|
+
value={form.thousand_sep ?? ","}
|
|
206
|
+
maxLength={1}
|
|
207
|
+
onChange={(e) => setField("thousand_sep", e.target.value)}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
<div className="space-y-1.5">
|
|
211
|
+
<Label>Decimal separator</Label>
|
|
212
|
+
<Input
|
|
213
|
+
value={form.decimal_sep ?? "."}
|
|
214
|
+
maxLength={1}
|
|
215
|
+
onChange={(e) => setField("decimal_sep", e.target.value)}
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="space-y-1.5">
|
|
219
|
+
<Label>Number of decimals</Label>
|
|
220
|
+
<Input
|
|
221
|
+
type="number"
|
|
222
|
+
min={0}
|
|
223
|
+
max={4}
|
|
224
|
+
value={form.num_decimals ?? 2}
|
|
225
|
+
onChange={(e) => setField("num_decimals", e.target.value === "" ? 2 : Number(e.target.value))}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</CardContent>
|
|
229
|
+
</Card>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Label } from "@/components/ui/label";
|
|
5
|
+
import { Switch } from "@/components/ui/switch";
|
|
6
|
+
import useGroupForm from "./useGroupForm";
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = {
|
|
9
|
+
manage_stock: true,
|
|
10
|
+
hold_stock_minutes: 60,
|
|
11
|
+
low_stock_threshold: 2,
|
|
12
|
+
out_of_stock_threshold: 0,
|
|
13
|
+
notify_low_stock: true,
|
|
14
|
+
notify_out_of_stock: true,
|
|
15
|
+
notification_recipient: "",
|
|
16
|
+
hide_out_of_stock: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function InventorySettings() {
|
|
20
|
+
const { form, setField, SaveButton } = useGroupForm("inventory", DEFAULTS);
|
|
21
|
+
const off = !form.manage_stock;
|
|
22
|
+
|
|
23
|
+
const num = (key, fallback = 0) => (e) =>
|
|
24
|
+
setField(key, e.target.value === "" ? fallback : Number(e.target.value));
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="max-w-3xl space-y-4">
|
|
28
|
+
<Card>
|
|
29
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
30
|
+
<div className="space-y-1.5">
|
|
31
|
+
<CardTitle>Inventory</CardTitle>
|
|
32
|
+
<CardDescription>Stock management defaults for products that track inventory.</CardDescription>
|
|
33
|
+
</div>
|
|
34
|
+
<SaveButton />
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent className="space-y-4">
|
|
37
|
+
<div className="flex items-start justify-between gap-4">
|
|
38
|
+
<div>
|
|
39
|
+
<Label>Manage stock</Label>
|
|
40
|
+
<p className="text-xs text-muted-foreground">Enable stock management store-wide.</p>
|
|
41
|
+
</div>
|
|
42
|
+
<Switch checked={!!form.manage_stock} onCheckedChange={(v) => setField("manage_stock", v)} />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="space-y-1.5">
|
|
46
|
+
<Label className={off ? "opacity-50" : ""}>Hold stock (minutes)</Label>
|
|
47
|
+
<Input
|
|
48
|
+
type="number"
|
|
49
|
+
min={0}
|
|
50
|
+
disabled={off}
|
|
51
|
+
value={form.hold_stock_minutes ?? 60}
|
|
52
|
+
onChange={num("hold_stock_minutes", 60)}
|
|
53
|
+
/>
|
|
54
|
+
<p className="text-xs text-muted-foreground">
|
|
55
|
+
Hold stock for unpaid orders for this many minutes. When the limit is reached, the
|
|
56
|
+
pending order is cancelled and its stock released. Release runs opportunistically —
|
|
57
|
+
see skills/commerce/references/scheduled-work.md.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
62
|
+
<div className="space-y-1.5">
|
|
63
|
+
<Label className={off ? "opacity-50" : ""}>Low stock threshold</Label>
|
|
64
|
+
<Input
|
|
65
|
+
type="number"
|
|
66
|
+
min={0}
|
|
67
|
+
disabled={off}
|
|
68
|
+
value={form.low_stock_threshold ?? 2}
|
|
69
|
+
onChange={num("low_stock_threshold", 2)}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="space-y-1.5">
|
|
73
|
+
<Label className={off ? "opacity-50" : ""}>Out of stock threshold</Label>
|
|
74
|
+
<Input
|
|
75
|
+
type="number"
|
|
76
|
+
disabled={off}
|
|
77
|
+
value={form.out_of_stock_threshold ?? 0}
|
|
78
|
+
onChange={num("out_of_stock_threshold", 0)}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</CardContent>
|
|
83
|
+
</Card>
|
|
84
|
+
|
|
85
|
+
<Card>
|
|
86
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
87
|
+
<div className="space-y-1.5">
|
|
88
|
+
<CardTitle>Notifications</CardTitle>
|
|
89
|
+
</div>
|
|
90
|
+
<SaveButton />
|
|
91
|
+
</CardHeader>
|
|
92
|
+
<CardContent className="space-y-4">
|
|
93
|
+
<div className="flex items-start justify-between gap-4">
|
|
94
|
+
<Label className={off ? "opacity-50" : ""}>Enable low stock notifications</Label>
|
|
95
|
+
<Switch
|
|
96
|
+
disabled={off}
|
|
97
|
+
checked={!!form.notify_low_stock}
|
|
98
|
+
onCheckedChange={(v) => setField("notify_low_stock", v)}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="flex items-start justify-between gap-4">
|
|
102
|
+
<Label className={off ? "opacity-50" : ""}>Enable out of stock notifications</Label>
|
|
103
|
+
<Switch
|
|
104
|
+
disabled={off}
|
|
105
|
+
checked={!!form.notify_out_of_stock}
|
|
106
|
+
onCheckedChange={(v) => setField("notify_out_of_stock", v)}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="space-y-1.5">
|
|
110
|
+
<Label className={off ? "opacity-50" : ""}>Notification recipient</Label>
|
|
111
|
+
<Input
|
|
112
|
+
type="email"
|
|
113
|
+
placeholder="Defaults to the admin recipients in Emails settings"
|
|
114
|
+
disabled={off}
|
|
115
|
+
value={form.notification_recipient || ""}
|
|
116
|
+
onChange={(e) => setField("notification_recipient", e.target.value)}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
|
|
122
|
+
<Card>
|
|
123
|
+
<CardHeader className="flex-row items-start justify-between gap-4 space-y-0">
|
|
124
|
+
<div className="space-y-1.5">
|
|
125
|
+
<CardTitle>Visibility</CardTitle>
|
|
126
|
+
</div>
|
|
127
|
+
<SaveButton />
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent>
|
|
130
|
+
<div className="flex items-start justify-between gap-4">
|
|
131
|
+
<div>
|
|
132
|
+
<Label>Hide out of stock items from the catalog</Label>
|
|
133
|
+
<p className="text-xs text-muted-foreground">
|
|
134
|
+
Storefront APIs will exclude out-of-stock products from listings.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
<Switch
|
|
138
|
+
checked={!!form.hide_out_of_stock}
|
|
139
|
+
onCheckedChange={(v) => setField("hide_out_of_stock", v)}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</CardContent>
|
|
143
|
+
</Card>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
7
|
+
import { Switch } from "@/components/ui/switch";
|
|
8
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from "@/components/ui/dialog";
|
|
16
|
+
import { ArrowDown, ArrowUp, Info, Loader2, Plus, Settings2, Trash2 } from "lucide-react";
|
|
17
|
+
import { toast } from "sonner";
|
|
18
|
+
import { base44 } from "../../lib/api";
|
|
19
|
+
import useAsync from "../../hooks/useAsync";
|
|
20
|
+
import usePaymentProvider from "../../hooks/usePaymentProvider";
|
|
21
|
+
|
|
22
|
+
const emptyBank = () => ({ account_name: "", account_number: "", bank_name: "", sort_code: "", iban: "", bic: "" });
|
|
23
|
+
|
|
24
|
+
export default function PaymentsSettings() {
|
|
25
|
+
const gateways = useAsync(() => base44.entities["commerce.PaymentGateway"].list("order", 100), []);
|
|
26
|
+
// Whether the store can take card payments is a live fact about the connected
|
|
27
|
+
// payment provider, not a constant — the notice below follows it.
|
|
28
|
+
const provider = usePaymentProvider();
|
|
29
|
+
const [dialog, setDialog] = useState(null); // { gateway (local copy) }
|
|
30
|
+
const [saving, setSaving] = useState(false);
|
|
31
|
+
const [busy, setBusy] = useState(false);
|
|
32
|
+
|
|
33
|
+
const rows = gateways.data || [];
|
|
34
|
+
|
|
35
|
+
const toggle = async (g, enabled) => {
|
|
36
|
+
try {
|
|
37
|
+
await base44.entities["commerce.PaymentGateway"].update(g.id, { enabled });
|
|
38
|
+
gateways.refetch();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
toast.error(err.message || "Failed to update gateway");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const move = async (index, dir) => {
|
|
45
|
+
const target = index + dir;
|
|
46
|
+
if (target < 0 || target >= rows.length) return;
|
|
47
|
+
const a = rows[index];
|
|
48
|
+
const b = rows[target];
|
|
49
|
+
setBusy(true);
|
|
50
|
+
try {
|
|
51
|
+
await Promise.all([
|
|
52
|
+
base44.entities["commerce.PaymentGateway"].update(a.id, { order: b.order ?? target }),
|
|
53
|
+
base44.entities["commerce.PaymentGateway"].update(b.id, { order: a.order ?? index }),
|
|
54
|
+
]);
|
|
55
|
+
gateways.refetch();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
toast.error(err.message || "Failed to reorder");
|
|
58
|
+
} finally {
|
|
59
|
+
setBusy(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const saveGateway = async () => {
|
|
64
|
+
setSaving(true);
|
|
65
|
+
try {
|
|
66
|
+
const g = dialog.gateway;
|
|
67
|
+
await base44.entities["commerce.PaymentGateway"].update(g.id, {
|
|
68
|
+
title: g.title,
|
|
69
|
+
description: g.description,
|
|
70
|
+
settings: g.settings || {},
|
|
71
|
+
});
|
|
72
|
+
toast.success("Payment method saved");
|
|
73
|
+
setDialog(null);
|
|
74
|
+
gateways.refetch();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
toast.error(err.message || "Failed to save");
|
|
77
|
+
} finally {
|
|
78
|
+
setSaving(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (gateways.loading) {
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex justify-center py-12">
|
|
85
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="max-w-3xl space-y-3">
|
|
92
|
+
{rows.map((g, i) => {
|
|
93
|
+
const isOnline = g.slug === provider.gatewaySlug;
|
|
94
|
+
return (
|
|
95
|
+
<Card key={g.id}>
|
|
96
|
+
<CardContent className="flex items-center gap-4 py-4">
|
|
97
|
+
<div className="flex flex-col">
|
|
98
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" disabled={i === 0 || busy} onClick={() => move(i, -1)}>
|
|
99
|
+
<ArrowUp className="h-3.5 w-3.5" />
|
|
100
|
+
</Button>
|
|
101
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" disabled={i === rows.length - 1 || busy} onClick={() => move(i, 1)}>
|
|
102
|
+
<ArrowDown className="h-3.5 w-3.5" />
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="min-w-0 flex-1">
|
|
106
|
+
<div className="font-medium">{g.method_title || g.title || g.slug}</div>
|
|
107
|
+
<div className="truncate text-sm text-muted-foreground">{g.method_description || g.description}</div>
|
|
108
|
+
{isOnline && !provider.loading && (
|
|
109
|
+
<Alert className="mt-3">
|
|
110
|
+
<Info className="h-4 w-4" />
|
|
111
|
+
{provider.connected ? (
|
|
112
|
+
<>
|
|
113
|
+
<AlertTitle>Payment provider connected</AlertTitle>
|
|
114
|
+
<AlertDescription>
|
|
115
|
+
Card payments are live: customers see this option at checkout, orders are
|
|
116
|
+
confirmed automatically once paid, and refunds can be sent back through the
|
|
117
|
+
provider{provider.providerLabel ? ` (${provider.providerLabel})` : ""}.
|
|
118
|
+
</AlertDescription>
|
|
119
|
+
</>
|
|
120
|
+
) : (
|
|
121
|
+
<>
|
|
122
|
+
<AlertTitle>No payment provider connected</AlertTitle>
|
|
123
|
+
<AlertDescription>
|
|
124
|
+
Card payments are ready to go but need a provider: connect the{" "}
|
|
125
|
+
{provider.providerLabel || "payment"} connector in this app's integrations, and
|
|
126
|
+
this option starts appearing at checkout — no code to write. Until then it stays
|
|
127
|
+
hidden from customers, so nobody can pick it and pay nothing. Prefer a different
|
|
128
|
+
provider? See{" "}
|
|
129
|
+
<code className="text-xs">skills/commerce/references/online-payments.md</code>.
|
|
130
|
+
</AlertDescription>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</Alert>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<Switch
|
|
138
|
+
checked={!!g.enabled}
|
|
139
|
+
disabled={busy}
|
|
140
|
+
title={
|
|
141
|
+
isOnline && !provider.connected
|
|
142
|
+
? "Stays hidden from customers until a payment provider is connected"
|
|
143
|
+
: undefined
|
|
144
|
+
}
|
|
145
|
+
onCheckedChange={(v) => toggle(g, v)}
|
|
146
|
+
/>
|
|
147
|
+
{!isOnline && (
|
|
148
|
+
<Button
|
|
149
|
+
variant="outline"
|
|
150
|
+
size="sm"
|
|
151
|
+
onClick={() =>
|
|
152
|
+
setDialog({
|
|
153
|
+
gateway: {
|
|
154
|
+
...g,
|
|
155
|
+
settings: {
|
|
156
|
+
...(g.settings || {}),
|
|
157
|
+
account_details: g.slug === "bacs" ? (g.settings?.account_details || []) : g.settings?.account_details,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
164
|
+
Manage
|
|
165
|
+
</Button>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</CardContent>
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
|
|
173
|
+
{dialog && (
|
|
174
|
+
<Dialog open onOpenChange={(o) => !o && setDialog(null)}>
|
|
175
|
+
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
|
176
|
+
<DialogHeader>
|
|
177
|
+
<DialogTitle>{dialog.gateway.method_title || dialog.gateway.title}</DialogTitle>
|
|
178
|
+
</DialogHeader>
|
|
179
|
+
<div className="space-y-4">
|
|
180
|
+
<div className="space-y-1.5">
|
|
181
|
+
<Label>Title</Label>
|
|
182
|
+
<Input
|
|
183
|
+
value={dialog.gateway.title || ""}
|
|
184
|
+
onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, title: e.target.value } })}
|
|
185
|
+
/>
|
|
186
|
+
<p className="text-xs text-muted-foreground">Shown to customers during checkout.</p>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="space-y-1.5">
|
|
189
|
+
<Label>Description</Label>
|
|
190
|
+
<Textarea
|
|
191
|
+
rows={2}
|
|
192
|
+
value={dialog.gateway.description || ""}
|
|
193
|
+
onChange={(e) => setDialog({ ...dialog, gateway: { ...dialog.gateway, description: e.target.value } })}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{dialog.gateway.slug === "bacs" && (
|
|
198
|
+
<BankAccountsEditor
|
|
199
|
+
accounts={dialog.gateway.settings?.account_details || []}
|
|
200
|
+
onChange={(account_details) =>
|
|
201
|
+
setDialog({ ...dialog, gateway: { ...dialog.gateway, settings: { ...dialog.gateway.settings, account_details } } })
|
|
202
|
+
}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
<Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button>
|
|
208
|
+
<Button onClick={saveGateway} disabled={saving}>
|
|
209
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
210
|
+
Save
|
|
211
|
+
</Button>
|
|
212
|
+
</DialogFooter>
|
|
213
|
+
</DialogContent>
|
|
214
|
+
</Dialog>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function BankAccountsEditor({ accounts, onChange }) {
|
|
221
|
+
const update = (i, key, val) => onChange(accounts.map((a, idx) => (idx === i ? { ...a, [key]: val } : a)));
|
|
222
|
+
const remove = (i) => onChange(accounts.filter((_, idx) => idx !== i));
|
|
223
|
+
const add = () => onChange([...accounts, emptyBank()]);
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="space-y-3">
|
|
227
|
+
<Label>Bank account details</Label>
|
|
228
|
+
{accounts.length === 0 && <p className="text-xs text-muted-foreground">No accounts yet.</p>}
|
|
229
|
+
{accounts.map((a, i) => (
|
|
230
|
+
<div key={i} className="space-y-2 rounded-md border p-3">
|
|
231
|
+
<div className="flex items-center justify-between">
|
|
232
|
+
<span className="text-sm font-medium">Account {i + 1}</span>
|
|
233
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => remove(i)}>
|
|
234
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
238
|
+
{[
|
|
239
|
+
["account_name", "Account name"],
|
|
240
|
+
["account_number", "Account number"],
|
|
241
|
+
["bank_name", "Bank name"],
|
|
242
|
+
["sort_code", "Sort code"],
|
|
243
|
+
["iban", "IBAN"],
|
|
244
|
+
["bic", "BIC / Swift"],
|
|
245
|
+
].map(([key, label]) => (
|
|
246
|
+
<div key={key} className="space-y-1">
|
|
247
|
+
<Label className="text-xs">{label}</Label>
|
|
248
|
+
<Input value={a[key] || ""} onChange={(e) => update(i, key, e.target.value)} />
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
<Button variant="outline" size="sm" onClick={add}>
|
|
255
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
256
|
+
Add account
|
|
257
|
+
</Button>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|