@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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,208 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Checkbox } from "@/components/ui/checkbox";
7
+ import { Card } from "@/components/ui/card";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@/components/ui/select";
15
+ import { Globe, Plus, Trash2 } from "lucide-react";
16
+
17
+ import { base44 } from "../../../../lib/api";
18
+ import SearchSelect from "../../../../components/SearchSelect";
19
+ import { isVariable } from "../../../../lib/product-utils";
20
+
21
+ const CUSTOM = "__custom__";
22
+
23
+ /**
24
+ * Assign global attributes (with term multi-select) or ad-hoc custom
25
+ * attributes ("Red | Blue | Green"). `variation: true` rows drive the
26
+ * Variations tab for variable products.
27
+ */
28
+ export default function AttributesTab({ product, up, refs }) {
29
+ const globalAttributes = refs.attributes || [];
30
+ const attributes = product.attributes || [];
31
+ const [addValue, setAddValue] = useState("");
32
+ const [termsCache, setTermsCache] = useState({}); // attribute_id -> [{id, name}]
33
+
34
+ // Load terms for every global attribute in use.
35
+ useEffect(() => {
36
+ const wanted = attributes.map((a) => a.attribute_id).filter(Boolean);
37
+ const missing = wanted.filter((id) => !termsCache[id]);
38
+ if (!missing.length) return;
39
+ let cancelled = false;
40
+ Promise.all(
41
+ missing.map((id) =>
42
+ base44.entities["commerce.ProductAttributeTerm"].filter({ attribute_id: id }, "menu_order", 500)
43
+ .then((terms) => [id, terms || []])
44
+ .catch(() => [id, []])
45
+ )
46
+ ).then((entries) => {
47
+ if (!cancelled) setTermsCache((c) => ({ ...c, ...Object.fromEntries(entries) }));
48
+ });
49
+ return () => {
50
+ cancelled = true;
51
+ };
52
+ // eslint-disable-next-line -- deps intentionally partial
53
+ }, [JSON.stringify(attributes.map((a) => a.attribute_id))]);
54
+
55
+ const setRow = (index, patch) => {
56
+ const next = attributes.map((a, i) => (i === index ? { ...a, ...patch } : a));
57
+ up({ attributes: next });
58
+ };
59
+
60
+ const removeRow = (index) => {
61
+ up({ attributes: attributes.filter((_, i) => i !== index) });
62
+ };
63
+
64
+ const addAttribute = () => {
65
+ if (!addValue) return;
66
+ if (addValue === CUSTOM) {
67
+ up({
68
+ attributes: [
69
+ ...attributes,
70
+ { attribute_id: "", name: "", position: attributes.length, visible: true, variation: false, options: [] },
71
+ ],
72
+ });
73
+ } else {
74
+ const attr = globalAttributes.find((a) => a.id === addValue);
75
+ if (!attr || attributes.some((a) => a.attribute_id === attr.id)) return;
76
+ up({
77
+ attributes: [
78
+ ...attributes,
79
+ {
80
+ attribute_id: attr.id,
81
+ name: attr.name,
82
+ position: attributes.length,
83
+ visible: true,
84
+ variation: false,
85
+ options: [],
86
+ },
87
+ ],
88
+ });
89
+ }
90
+ setAddValue("");
91
+ };
92
+
93
+ return (
94
+ <div className="space-y-4">
95
+ <div className="flex items-end gap-2">
96
+ <div className="space-y-1.5">
97
+ <Label>Add attribute</Label>
98
+ <Select value={addValue} onValueChange={setAddValue}>
99
+ <SelectTrigger className="w-64">
100
+ <SelectValue placeholder="Select an attribute…" />
101
+ </SelectTrigger>
102
+ <SelectContent>
103
+ <SelectItem value={CUSTOM}>Custom product attribute</SelectItem>
104
+ {globalAttributes
105
+ .filter((a) => !attributes.some((row) => row.attribute_id === a.id))
106
+ .map((a) => (
107
+ <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>
108
+ ))}
109
+ </SelectContent>
110
+ </Select>
111
+ </div>
112
+ <Button type="button" variant="outline" onClick={addAttribute} disabled={!addValue}>
113
+ <Plus className="mr-1 h-4 w-4" /> Add
114
+ </Button>
115
+ </div>
116
+
117
+ {attributes.length === 0 && (
118
+ <p className="text-sm text-muted-foreground">
119
+ No attributes yet. Attributes describe the product (e.g. Color, Size) and can power variations.
120
+ </p>
121
+ )}
122
+
123
+ <div className="space-y-3">
124
+ {attributes.map((row, i) => {
125
+ const isGlobal = Boolean(row.attribute_id);
126
+ const terms = termsCache[row.attribute_id] || [];
127
+ return (
128
+ <Card key={`${row.attribute_id || "custom"}-${i}`} className="p-3">
129
+ <div className="flex items-start justify-between gap-2">
130
+ <div className="min-w-0 flex-1 space-y-3">
131
+ <div className="flex items-center gap-2">
132
+ {isGlobal ? (
133
+ <>
134
+ <span className="font-medium">{row.name}</span>
135
+ <Badge variant="secondary" className="gap-1">
136
+ <Globe className="h-3 w-3" /> Global
137
+ </Badge>
138
+ </>
139
+ ) : (
140
+ <Input
141
+ className="h-8 w-56"
142
+ placeholder="Attribute name (e.g. Material)"
143
+ value={row.name}
144
+ onChange={(e) => setRow(i, { name: e.target.value })}
145
+ />
146
+ )}
147
+ </div>
148
+
149
+ {isGlobal ? (
150
+ <SearchSelect
151
+ multiple
152
+ placeholder="Select terms…"
153
+ value={(row.options || []).map((o) => ({ value: o, label: o }))}
154
+ onChange={(next) => setRow(i, { options: next.map((o) => o.value) })}
155
+ search={async (q) =>
156
+ terms
157
+ .filter((t) => !q || t.name.toLowerCase().includes(q.toLowerCase()))
158
+ .map((t) => ({ value: t.name, label: t.name }))
159
+ }
160
+ />
161
+ ) : (
162
+ <div className="space-y-1">
163
+ <Input
164
+ placeholder="Values separated by | (e.g. Cotton | Wool | Silk)"
165
+ value={(row.options || []).join(" | ")}
166
+ onChange={(e) =>
167
+ setRow(i, {
168
+ options: e.target.value
169
+ .split("|")
170
+ .map((s) => s.trim())
171
+ .filter(Boolean),
172
+ })
173
+ }
174
+ />
175
+ <p className="text-xs text-muted-foreground">Separate values with “|”.</p>
176
+ </div>
177
+ )}
178
+
179
+ <div className="flex items-center gap-4 text-sm">
180
+ <label className="flex items-center gap-1.5">
181
+ <Checkbox
182
+ checked={row.visible !== false}
183
+ onCheckedChange={(v) => setRow(i, { visible: Boolean(v) })}
184
+ />
185
+ Visible on the product page
186
+ </label>
187
+ {isVariable(product) && (
188
+ <label className="flex items-center gap-1.5">
189
+ <Checkbox
190
+ checked={Boolean(row.variation)}
191
+ onCheckedChange={(v) => setRow(i, { variation: Boolean(v) })}
192
+ />
193
+ Used for variations
194
+ </label>
195
+ )}
196
+ </div>
197
+ </div>
198
+ <Button type="button" variant="ghost" size="icon" onClick={() => removeRow(i)}>
199
+ <Trash2 className="h-4 w-4 text-muted-foreground" />
200
+ </Button>
201
+ </div>
202
+ </Card>
203
+ );
204
+ })}
205
+ </div>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Label } from "@/components/ui/label";
5
+ import { Plus, Trash2 } from "lucide-react";
6
+
7
+ import MediaUploader from "../../../../components/MediaUploader";
8
+
9
+ const num = (v, fallback) => (v === "" ? fallback : Number(v));
10
+
11
+ /** Downloadable files, download limit and expiry. */
12
+ export default function DownloadsTab({ product, up }) {
13
+ const downloads = product.downloads || [];
14
+
15
+ const setRow = (i, patch) =>
16
+ up({ downloads: downloads.map((d, j) => (j === i ? { ...d, ...patch } : d)) });
17
+
18
+ return (
19
+ <div className="max-w-xl space-y-4">
20
+ <div className="space-y-3">
21
+ <Label>Downloadable files</Label>
22
+ {downloads.map((d, i) => (
23
+ <div key={i} className="grid gap-2 rounded-md border p-3 sm:grid-cols-[1fr_1fr_auto]">
24
+ <div className="space-y-1.5">
25
+ <Label className="text-xs">Name</Label>
26
+ <Input
27
+ placeholder="e.g. User manual"
28
+ value={d.name || ""}
29
+ onChange={(e) => setRow(i, { name: e.target.value })}
30
+ />
31
+ </div>
32
+ <div className="space-y-1.5">
33
+ <Label className="text-xs">File</Label>
34
+ {d.file_url ? (
35
+ <div className="flex items-center gap-2">
36
+ <Input value={d.file_url} onChange={(e) => setRow(i, { file_url: e.target.value })} />
37
+ </div>
38
+ ) : (
39
+ <MediaUploader
40
+ accept="*/*"
41
+ label="Upload file"
42
+ value={null}
43
+ onChange={(img) => img && setRow(i, { file_url: img.src, name: d.name || img.name })}
44
+ />
45
+ )}
46
+ </div>
47
+ <div className="flex items-end">
48
+ <Button
49
+ type="button"
50
+ variant="ghost"
51
+ size="icon"
52
+ onClick={() => up({ downloads: downloads.filter((_, j) => j !== i) })}
53
+ >
54
+ <Trash2 className="h-4 w-4 text-muted-foreground" />
55
+ </Button>
56
+ </div>
57
+ </div>
58
+ ))}
59
+ <Button
60
+ type="button"
61
+ variant="outline"
62
+ size="sm"
63
+ onClick={() => up({ downloads: [...downloads, { name: "", file_url: "" }] })}
64
+ >
65
+ <Plus className="mr-1 h-4 w-4" /> Add file
66
+ </Button>
67
+ </div>
68
+
69
+ <div className="grid max-w-md grid-cols-2 gap-2">
70
+ <div className="space-y-1.5">
71
+ <Label>Download limit</Label>
72
+ <Input
73
+ type="number"
74
+ value={product.download_limit ?? -1}
75
+ onChange={(e) => up({ download_limit: num(e.target.value, -1) })}
76
+ />
77
+ <p className="text-xs text-muted-foreground">-1 for unlimited downloads.</p>
78
+ </div>
79
+ <div className="space-y-1.5">
80
+ <Label>Download expiry (days)</Label>
81
+ <Input
82
+ type="number"
83
+ value={product.download_expiry ?? -1}
84
+ onChange={(e) => up({ download_expiry: num(e.target.value, -1) })}
85
+ />
86
+ <p className="text-xs text-muted-foreground">-1 for no expiry.</p>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Label } from "@/components/ui/label";
4
+
5
+ import MoneyInput from "../../../../components/MoneyInput";
6
+
7
+ /** External/affiliate product: outbound URL + button text + pricing. */
8
+ export default function ExternalTab({ product, up }) {
9
+ return (
10
+ <div className="max-w-md space-y-4">
11
+ <div className="space-y-1.5">
12
+ <Label>Product URL</Label>
13
+ <Input
14
+ type="url"
15
+ placeholder="https://example.com/product"
16
+ value={product.external_url || ""}
17
+ onChange={(e) => up({ external_url: e.target.value })}
18
+ />
19
+ <p className="text-xs text-muted-foreground">Where customers are sent to buy this product.</p>
20
+ </div>
21
+ <div className="space-y-1.5">
22
+ <Label>Button text</Label>
23
+ <Input
24
+ placeholder="Buy product"
25
+ value={product.button_text || ""}
26
+ onChange={(e) => up({ button_text: e.target.value })}
27
+ />
28
+ </div>
29
+ <div className="grid grid-cols-2 gap-2">
30
+ <div className="space-y-1.5">
31
+ <Label>Regular price</Label>
32
+ <MoneyInput value={product.regular_price} onChange={(v) => up({ regular_price: v })} />
33
+ </div>
34
+ <div className="space-y-1.5">
35
+ <Label>Sale price</Label>
36
+ <MoneyInput value={product.sale_price} onChange={(v) => up({ sale_price: v })} />
37
+ </div>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,103 @@
1
+ import React from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Label } from "@/components/ui/label";
4
+ import { Switch } from "@/components/ui/switch";
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "@/components/ui/select";
12
+
13
+ import MoneyInput from "../../../../components/MoneyInput";
14
+ import { TAX_STATUSES } from "../../../../lib/constants";
15
+ import { isVariable } from "../../../../lib/product-utils";
16
+
17
+ /** Convert an ISO datetime to the value a date input expects (yyyy-mm-dd). */
18
+ const toDateInput = (iso) => (iso ? String(iso).slice(0, 10) : "");
19
+ const fromDateInput = (v) => (v ? new Date(`${v}T00:00:00Z`).toISOString() : null);
20
+
21
+ /** Pricing + tax. For variable products only tax fields apply (prices per variation). */
22
+ export default function GeneralTab({ product, up, refs }) {
23
+ const scheduled = Boolean(product.date_on_sale_from || product.date_on_sale_to);
24
+ const taxClasses = refs.taxClasses || [];
25
+
26
+ return (
27
+ <div className="max-w-md space-y-4">
28
+ {isVariable(product) ? (
29
+ <p className="text-sm text-muted-foreground">
30
+ Prices for variable products are set per variation on the Variations tab.
31
+ </p>
32
+ ) : (
33
+ <>
34
+ <div className="space-y-1.5">
35
+ <Label>Regular price</Label>
36
+ <MoneyInput
37
+ value={product.regular_price}
38
+ onChange={(v) => up({ regular_price: v })}
39
+ />
40
+ </div>
41
+ <div className="space-y-1.5">
42
+ <Label>Sale price</Label>
43
+ <MoneyInput value={product.sale_price} onChange={(v) => up({ sale_price: v })} />
44
+ </div>
45
+ <div className="flex items-center gap-2">
46
+ <Switch
47
+ checked={scheduled}
48
+ onCheckedChange={(v) =>
49
+ up(v ? { date_on_sale_from: new Date().toISOString() } : { date_on_sale_from: null, date_on_sale_to: null })
50
+ }
51
+ />
52
+ <Label>Schedule sale</Label>
53
+ </div>
54
+ {scheduled && (
55
+ <div className="grid grid-cols-2 gap-2">
56
+ <div className="space-y-1.5">
57
+ <Label className="text-xs">Sale start</Label>
58
+ <Input
59
+ type="date"
60
+ value={toDateInput(product.date_on_sale_from)}
61
+ onChange={(e) => up({ date_on_sale_from: fromDateInput(e.target.value) })}
62
+ />
63
+ </div>
64
+ <div className="space-y-1.5">
65
+ <Label className="text-xs">Sale end</Label>
66
+ <Input
67
+ type="date"
68
+ value={toDateInput(product.date_on_sale_to)}
69
+ onChange={(e) => up({ date_on_sale_to: fromDateInput(e.target.value) })}
70
+ />
71
+ </div>
72
+ </div>
73
+ )}
74
+ </>
75
+ )}
76
+
77
+ <div className="grid grid-cols-2 gap-2">
78
+ <div className="space-y-1.5">
79
+ <Label>Tax status</Label>
80
+ <Select value={product.tax_status} onValueChange={(v) => up({ tax_status: v })}>
81
+ <SelectTrigger><SelectValue /></SelectTrigger>
82
+ <SelectContent>
83
+ {TAX_STATUSES.map((t) => (
84
+ <SelectItem key={t.value} value={t.value}>{t.label}</SelectItem>
85
+ ))}
86
+ </SelectContent>
87
+ </Select>
88
+ </div>
89
+ <div className="space-y-1.5">
90
+ <Label>Tax class</Label>
91
+ <Select value={product.tax_class || "standard"} onValueChange={(v) => up({ tax_class: v })}>
92
+ <SelectTrigger><SelectValue /></SelectTrigger>
93
+ <SelectContent>
94
+ {(taxClasses.length ? taxClasses : [{ slug: "standard", name: "Standard" }]).map((c) => (
95
+ <SelectItem key={c.slug} value={c.slug}>{c.name}</SelectItem>
96
+ ))}
97
+ </SelectContent>
98
+ </Select>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,93 @@
1
+ import React from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Label } from "@/components/ui/label";
4
+ import { Switch } from "@/components/ui/switch";
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
+
14
+ import { BACKORDER_OPTIONS, STOCK_STATUSES } from "../../../../lib/constants";
15
+
16
+ const num = (v) => (v === "" ? null : Number(v));
17
+
18
+ /** SKU + stock management. */
19
+ export default function InventoryTab({ product, up }) {
20
+ return (
21
+ <div className="max-w-md space-y-4">
22
+ <div className="space-y-1.5">
23
+ <Label>SKU</Label>
24
+ <Input
25
+ placeholder="Unique stock keeping unit"
26
+ value={product.sku || ""}
27
+ onChange={(e) => up({ sku: e.target.value })}
28
+ />
29
+ </div>
30
+
31
+ <div className="flex items-center gap-2">
32
+ <Switch
33
+ checked={product.manage_stock}
34
+ onCheckedChange={(v) => up({ manage_stock: Boolean(v) })}
35
+ />
36
+ <Label>Track stock quantity for this product</Label>
37
+ </div>
38
+
39
+ {product.manage_stock ? (
40
+ <>
41
+ <div className="space-y-1.5">
42
+ <Label>Stock quantity</Label>
43
+ <Input
44
+ type="number"
45
+ value={product.stock_quantity ?? ""}
46
+ onChange={(e) => up({ stock_quantity: num(e.target.value) })}
47
+ />
48
+ </div>
49
+ <div className="space-y-1.5">
50
+ <Label>Allow backorders?</Label>
51
+ <Select value={product.backorders || "no"} onValueChange={(v) => up({ backorders: v })}>
52
+ <SelectTrigger><SelectValue /></SelectTrigger>
53
+ <SelectContent>
54
+ {BACKORDER_OPTIONS.map((o) => (
55
+ <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
56
+ ))}
57
+ </SelectContent>
58
+ </Select>
59
+ </div>
60
+ <div className="space-y-1.5">
61
+ <Label>Low stock threshold</Label>
62
+ <Input
63
+ type="number"
64
+ placeholder="Store default"
65
+ value={product.low_stock_amount ?? ""}
66
+ onChange={(e) => up({ low_stock_amount: num(e.target.value) })}
67
+ />
68
+ </div>
69
+ </>
70
+ ) : (
71
+ <div className="space-y-1.5">
72
+ <Label>Stock status</Label>
73
+ <Select value={product.stock_status || "instock"} onValueChange={(v) => up({ stock_status: v })}>
74
+ <SelectTrigger><SelectValue /></SelectTrigger>
75
+ <SelectContent>
76
+ {STOCK_STATUSES.map((s) => (
77
+ <SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
78
+ ))}
79
+ </SelectContent>
80
+ </Select>
81
+ </div>
82
+ )}
83
+
84
+ <label className="flex items-center gap-2 text-sm">
85
+ <Checkbox
86
+ checked={product.sold_individually}
87
+ onCheckedChange={(v) => up({ sold_individually: Boolean(v) })}
88
+ />
89
+ Limit purchases to 1 item per order
90
+ </label>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,102 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Label } from "@/components/ui/label";
3
+
4
+ import { base44, call } from "../../../../lib/api";
5
+ import SearchSelect from "../../../../components/SearchSelect";
6
+ import { isGrouped } from "../../../../lib/product-utils";
7
+
8
+ /** Search products server-side for the pickers. */
9
+ const searchProducts = async (q) => {
10
+ const data = await call("admin-products", "search", { q: q || undefined, limit: 10 }, { silent: true });
11
+ const rows = data?.rows || data || [];
12
+ return rows.map((p) => ({ value: p.id, label: p.name, meta: p.sku || undefined }));
13
+ };
14
+
15
+ /**
16
+ * Multi product picker bound to an array of product ids.
17
+ * Resolves labels for pre-existing ids on mount.
18
+ */
19
+ function ProductIdsPicker({ ids, onChange, placeholder }) {
20
+ const [options, setOptions] = useState(null); // [{value,label}]
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+ const known = new Map((options || []).map((o) => [o.value, o]));
25
+ const missing = (ids || []).filter((id) => !known.has(id));
26
+ if (!missing.length) {
27
+ if (options === null) setOptions([]);
28
+ return;
29
+ }
30
+ Promise.all(
31
+ missing.map((id) =>
32
+ base44.entities["commerce.Product"].get(id)
33
+ .then((p) => ({ value: id, label: p?.name || "(deleted product)" }))
34
+ .catch(() => ({ value: id, label: "(deleted product)" }))
35
+ )
36
+ ).then((resolved) => {
37
+ if (!cancelled) setOptions([...(options || []), ...resolved]);
38
+ });
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ // eslint-disable-next-line -- deps intentionally partial
43
+ }, [JSON.stringify(ids)]);
44
+
45
+ const value = (ids || []).map(
46
+ (id) => (options || []).find((o) => o.value === id) || { value: id, label: "…" }
47
+ );
48
+
49
+ return (
50
+ <SearchSelect
51
+ multiple
52
+ placeholder={placeholder}
53
+ search={searchProducts}
54
+ value={value}
55
+ onChange={(next) => {
56
+ setOptions(next);
57
+ onChange(next.map((o) => o.value));
58
+ }}
59
+ />
60
+ );
61
+ }
62
+
63
+ /** Upsells, cross-sells and (for grouped products) child products. */
64
+ export default function LinkedTab({ product, up }) {
65
+ return (
66
+ <div className="max-w-md space-y-4">
67
+ {isGrouped(product) && (
68
+ <div className="space-y-1.5">
69
+ <Label>Grouped products</Label>
70
+ <ProductIdsPicker
71
+ ids={product.grouped_products}
72
+ onChange={(grouped_products) => up({ grouped_products })}
73
+ placeholder="Search for products to group…"
74
+ />
75
+ <p className="text-xs text-muted-foreground">Products shown as part of this grouped product.</p>
76
+ </div>
77
+ )}
78
+
79
+ <div className="space-y-1.5">
80
+ <Label>Upsells</Label>
81
+ <ProductIdsPicker
82
+ ids={product.upsell_ids}
83
+ onChange={(upsell_ids) => up({ upsell_ids })}
84
+ placeholder="Search for products to upsell…"
85
+ />
86
+ <p className="text-xs text-muted-foreground">
87
+ Recommended instead of this product, e.g. premium alternatives.
88
+ </p>
89
+ </div>
90
+
91
+ <div className="space-y-1.5">
92
+ <Label>Cross-sells</Label>
93
+ <ProductIdsPicker
94
+ ids={product.cross_sell_ids}
95
+ onChange={(cross_sell_ids) => up({ cross_sell_ids })}
96
+ placeholder="Search for products to cross-sell…"
97
+ />
98
+ <p className="text-xs text-muted-foreground">Promoted in the cart alongside this product.</p>
99
+ </div>
100
+ </div>
101
+ );
102
+ }