@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,116 @@
1
+ import React, { useRef, useState } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { ArrowDown, ArrowUp, ImagePlus, Loader2, X } from "lucide-react";
4
+ import { toast } from "sonner";
5
+ import { base44 } from "../lib/api";
6
+
7
+ /** Upload a file via the Core integration; returns the public URL. */
8
+ export async function uploadFile(file) {
9
+ const res = await base44.integrations.Core.UploadFile({ file });
10
+ return res?.file_url || res?.data?.file_url;
11
+ }
12
+
13
+ /**
14
+ * Image uploader.
15
+ *
16
+ * Single mode (multiple=false): value = {src, name, alt} | null
17
+ * Gallery mode (multiple=true): value = [{src, name, alt}], reorder via up/down
18
+ *
19
+ * Props: { value, onChange, multiple?, accept?, label? }
20
+ */
21
+ export default function MediaUploader({
22
+ value,
23
+ onChange,
24
+ multiple = false,
25
+ accept = "image/*",
26
+ label = multiple ? "Add images" : "Set image",
27
+ }) {
28
+ const inputRef = useRef(null);
29
+ const [uploading, setUploading] = useState(false);
30
+
31
+ const items = multiple ? value || [] : value ? [value] : [];
32
+
33
+ const handleFiles = async (files) => {
34
+ if (!files?.length) return;
35
+ setUploading(true);
36
+ try {
37
+ const uploaded = [];
38
+ for (const file of Array.from(files)) {
39
+ const src = await uploadFile(file);
40
+ if (src) uploaded.push({ src, name: file.name, alt: "" });
41
+ }
42
+ if (multiple) onChange([...(value || []), ...uploaded]);
43
+ else onChange(uploaded[0] || null);
44
+ } catch (err) {
45
+ toast.error(err.response?.data?.error || err.message || "Upload failed");
46
+ } finally {
47
+ setUploading(false);
48
+ if (inputRef.current) inputRef.current.value = "";
49
+ }
50
+ };
51
+
52
+ const removeAt = (i) => {
53
+ if (multiple) onChange(items.filter((_, idx) => idx !== i));
54
+ else onChange(null);
55
+ };
56
+
57
+ const move = (i, dir) => {
58
+ const next = [...items];
59
+ const j = i + dir;
60
+ if (j < 0 || j >= next.length) return;
61
+ [next[i], next[j]] = [next[j], next[i]];
62
+ onChange(next);
63
+ };
64
+
65
+ return (
66
+ <div className="space-y-2">
67
+ {items.length > 0 && (
68
+ <div className={multiple ? "grid grid-cols-3 gap-2" : ""}>
69
+ {items.map((img, i) => (
70
+ <div key={`${img.src}-${i}`} className="group relative overflow-hidden rounded-md border">
71
+ <img
72
+ src={img.src}
73
+ alt={img.alt || img.name || ""}
74
+ className={`w-full object-cover ${multiple ? "aspect-square" : "max-h-56"}`}
75
+ />
76
+ <div className="absolute inset-x-0 top-0 flex justify-end gap-0.5 bg-gradient-to-b from-black/50 to-transparent p-1 opacity-0 transition-opacity group-hover:opacity-100">
77
+ {multiple && (
78
+ <>
79
+ <Button type="button" size="icon" variant="secondary" className="h-6 w-6" onClick={() => move(i, -1)}>
80
+ <ArrowUp className="h-3 w-3" />
81
+ </Button>
82
+ <Button type="button" size="icon" variant="secondary" className="h-6 w-6" onClick={() => move(i, 1)}>
83
+ <ArrowDown className="h-3 w-3" />
84
+ </Button>
85
+ </>
86
+ )}
87
+ <Button type="button" size="icon" variant="destructive" className="h-6 w-6" onClick={() => removeAt(i)}>
88
+ <X className="h-3 w-3" />
89
+ </Button>
90
+ </div>
91
+ </div>
92
+ ))}
93
+ </div>
94
+ )}
95
+ <input
96
+ ref={inputRef}
97
+ type="file"
98
+ accept={accept}
99
+ multiple={multiple}
100
+ className="hidden"
101
+ onChange={(e) => handleFiles(e.target.files)}
102
+ />
103
+ <Button
104
+ type="button"
105
+ variant="outline"
106
+ size="sm"
107
+ disabled={uploading}
108
+ onClick={() => inputRef.current?.click()}
109
+ className="w-full"
110
+ >
111
+ {uploading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <ImagePlus className="mr-2 h-4 w-4" />}
112
+ {label}
113
+ </Button>
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Plus, X } from "lucide-react";
5
+
6
+ /**
7
+ * Key/value editor for `meta_data` arrays.
8
+ * Props: { value: [{key, value}], onChange }
9
+ */
10
+ export default function MetaDataEditor({ value = [], onChange }) {
11
+ const setRow = (i, field, v) => {
12
+ const next = value.map((row, idx) => (idx === i ? { ...row, [field]: v } : row));
13
+ onChange(next);
14
+ };
15
+
16
+ const removeRow = (i) => onChange(value.filter((_, idx) => idx !== i));
17
+ const addRow = () => onChange([...(value || []), { key: "", value: "" }]);
18
+
19
+ return (
20
+ <div className="space-y-2">
21
+ {(value || []).map((row, i) => (
22
+ <div key={i} className="flex items-center gap-2">
23
+ <Input
24
+ placeholder="Key"
25
+ value={row.key || ""}
26
+ onChange={(e) => setRow(i, "key", e.target.value)}
27
+ className="w-1/3"
28
+ />
29
+ <Input
30
+ placeholder="Value"
31
+ value={row.value || ""}
32
+ onChange={(e) => setRow(i, "value", e.target.value)}
33
+ className="flex-1"
34
+ />
35
+ <Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => removeRow(i)}>
36
+ <X className="h-4 w-4" />
37
+ </Button>
38
+ </div>
39
+ ))}
40
+ <Button type="button" variant="outline" size="sm" onClick={addRow}>
41
+ <Plus className="mr-1 h-4 w-4" /> Add field
42
+ </Button>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,50 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import useMoney from "../hooks/useMoney";
4
+
5
+ /**
6
+ * Numeric input with the store currency symbol.
7
+ * Props: { value: number|null, onChange(number|null), placeholder?, disabled?, className? }
8
+ */
9
+ export default function MoneyInput({ value, onChange, placeholder = "0.00", disabled, className = "" }) {
10
+ const { symbol } = useMoney();
11
+ const [text, setText] = useState(value ?? value === 0 ? String(value) : "");
12
+
13
+ // Keep local text in sync when the outer value changes (e.g. after save).
14
+ useEffect(() => {
15
+ const asNum = text === "" ? null : Number(text);
16
+ if ((value ?? null) !== (isNaN(asNum) ? null : asNum)) {
17
+ setText(value === null || value === undefined ? "" : String(value));
18
+ }
19
+ // eslint-disable-next-line -- deps intentionally partial
20
+ }, [value]);
21
+
22
+ const handleChange = (e) => {
23
+ const raw = e.target.value;
24
+ setText(raw);
25
+ if (raw.trim() === "") {
26
+ onChange(null);
27
+ return;
28
+ }
29
+ const num = Number(raw);
30
+ if (!isNaN(num)) onChange(num);
31
+ };
32
+
33
+ return (
34
+ <div className={`relative ${className}`}>
35
+ <span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-sm text-muted-foreground">
36
+ {symbol}
37
+ </span>
38
+ <Input
39
+ type="number"
40
+ step="any"
41
+ inputMode="decimal"
42
+ className="pl-9"
43
+ value={text}
44
+ onChange={handleChange}
45
+ placeholder={placeholder}
46
+ disabled={disabled}
47
+ />
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Button } from "@/components/ui/button";
4
+ import { ArrowLeft } from "lucide-react";
5
+
6
+ /**
7
+ * Page title row.
8
+ * Props: { title, description?, actions? (node), backHref? }
9
+ */
10
+ export default function PageHeader({ title, description, actions, backHref }) {
11
+ return (
12
+ <div className="mb-6 flex flex-wrap items-start justify-between gap-3">
13
+ <div className="flex items-start gap-2">
14
+ {backHref && (
15
+ <Button variant="ghost" size="icon" asChild className="mt-0.5 h-8 w-8">
16
+ <Link to={backHref}>
17
+ <ArrowLeft className="h-4 w-4" />
18
+ </Link>
19
+ </Button>
20
+ )}
21
+ <div>
22
+ <h1 className="text-xl font-semibold tracking-tight">{title}</h1>
23
+ {description && <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>}
24
+ </div>
25
+ </div>
26
+ {actions && <div className="flex items-center gap-2">{actions}</div>}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { Textarea } from "@/components/ui/textarea";
3
+
4
+ /**
5
+ * Description editor. Persists plain text / HTML as-is.
6
+ * Deliberately a plain textarea to avoid a WYSIWYG dependency — swap in your
7
+ * editor of choice (e.g. TipTap) here if you need rich editing.
8
+ *
9
+ * Props: { value, onChange(string), rows?, placeholder? }
10
+ */
11
+ export default function RichTextarea({ value, onChange, rows = 6, placeholder }) {
12
+ return (
13
+ <Textarea
14
+ value={value || ""}
15
+ onChange={(e) => onChange(e.target.value)}
16
+ rows={rows}
17
+ placeholder={placeholder}
18
+ className="font-mono text-sm"
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,142 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
3
+ import {
4
+ Command,
5
+ CommandEmpty,
6
+ CommandGroup,
7
+ CommandInput,
8
+ CommandItem,
9
+ CommandList,
10
+ } from "@/components/ui/command";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
14
+ import useDebounce from "../hooks/useDebounce";
15
+
16
+ /**
17
+ * Async searchable picker (single or multi).
18
+ *
19
+ * Props:
20
+ * - search(q) → Promise<[{ value, label, meta? }]>
21
+ * - value: {value,label} | null (single) | [{value,label}] (multiple)
22
+ * - onChange(next)
23
+ * - multiple?: boolean
24
+ * - placeholder?: string
25
+ * - disabled?, className?
26
+ */
27
+ export default function SearchSelect({
28
+ search,
29
+ value,
30
+ onChange,
31
+ multiple = false,
32
+ placeholder = "Search…",
33
+ disabled = false,
34
+ className = "",
35
+ }) {
36
+ const [open, setOpen] = useState(false);
37
+ const [query, setQuery] = useState("");
38
+ const [options, setOptions] = useState([]);
39
+ const [loading, setLoading] = useState(false);
40
+ const debouncedQuery = useDebounce(query, 300);
41
+
42
+ useEffect(() => {
43
+ if (!open) return;
44
+ let cancelled = false;
45
+ setLoading(true);
46
+ Promise.resolve(search(debouncedQuery))
47
+ .then((opts) => !cancelled && setOptions(opts || []))
48
+ .catch(() => !cancelled && setOptions([]))
49
+ .finally(() => !cancelled && setLoading(false));
50
+ return () => {
51
+ cancelled = true;
52
+ };
53
+ }, [open, debouncedQuery, search]);
54
+
55
+ const selectedValues = multiple ? (value || []).map((v) => v.value) : value ? [value.value] : [];
56
+
57
+ const pick = (opt) => {
58
+ if (multiple) {
59
+ const exists = (value || []).some((v) => v.value === opt.value);
60
+ onChange(exists ? (value || []).filter((v) => v.value !== opt.value) : [...(value || []), opt]);
61
+ } else {
62
+ onChange(value?.value === opt.value ? null : opt);
63
+ setOpen(false);
64
+ }
65
+ };
66
+
67
+ const removeChip = (v) => onChange((value || []).filter((x) => x.value !== v));
68
+
69
+ return (
70
+ <div className={className}>
71
+ <Popover open={open} onOpenChange={setOpen}>
72
+ <PopoverTrigger asChild>
73
+ <Button
74
+ type="button"
75
+ variant="outline"
76
+ role="combobox"
77
+ disabled={disabled}
78
+ className="w-full justify-between font-normal"
79
+ >
80
+ <span className="truncate text-left">
81
+ {multiple
82
+ ? (value || []).length
83
+ ? `${value.length} selected`
84
+ : placeholder
85
+ : value?.label || placeholder}
86
+ </span>
87
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
88
+ </Button>
89
+ </PopoverTrigger>
90
+ <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
91
+ <Command shouldFilter={false}>
92
+ <CommandInput placeholder={placeholder} value={query} onValueChange={setQuery} />
93
+ <CommandList>
94
+ {loading ? (
95
+ <div className="flex items-center justify-center py-4">
96
+ <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
97
+ </div>
98
+ ) : (
99
+ <>
100
+ <CommandEmpty>No results.</CommandEmpty>
101
+ <CommandGroup>
102
+ {options.map((opt) => (
103
+ <CommandItem key={opt.value} value={String(opt.value)} onSelect={() => pick(opt)}>
104
+ <Check
105
+ className={`mr-2 h-4 w-4 ${
106
+ selectedValues.includes(opt.value) ? "opacity-100" : "opacity-0"
107
+ }`}
108
+ />
109
+ <div className="min-w-0">
110
+ <div className="truncate">{opt.label}</div>
111
+ {opt.meta && (
112
+ <div className="truncate text-xs text-muted-foreground">{opt.meta}</div>
113
+ )}
114
+ </div>
115
+ </CommandItem>
116
+ ))}
117
+ </CommandGroup>
118
+ </>
119
+ )}
120
+ </CommandList>
121
+ </Command>
122
+ </PopoverContent>
123
+ </Popover>
124
+ {multiple && (value || []).length > 0 && (
125
+ <div className="mt-2 flex flex-wrap gap-1">
126
+ {value.map((v) => (
127
+ <Badge key={v.value} variant="secondary" className="gap-1 pr-1">
128
+ {v.label}
129
+ <button
130
+ type="button"
131
+ onClick={() => removeChip(v.value)}
132
+ className="rounded-full p-0.5 hover:bg-muted-foreground/20"
133
+ >
134
+ <X className="h-3 w-3" />
135
+ </button>
136
+ </Badge>
137
+ ))}
138
+ </div>
139
+ )}
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { Badge } from "@/components/ui/badge";
3
+ import { ORDER_STATUSES, statusMeta } from "../lib/constants";
4
+
5
+ /**
6
+ * Colored status pill.
7
+ * Props: { status, map? } — `map` defaults to ORDER_STATUSES; pass
8
+ * REVIEW_STATUSES / WEBHOOK_STATUSES / STOCK_STATUSES for other domains.
9
+ */
10
+ export default function StatusBadge({ status, map = ORDER_STATUSES, className = "" }) {
11
+ const meta = statusMeta(map, status);
12
+ return (
13
+ <Badge variant="outline" className={`${meta.color} ${className}`}>
14
+ {meta.label}
15
+ </Badge>
16
+ );
17
+ }
@@ -0,0 +1,26 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { normalizeBasePath } from "../lib/paths";
3
+
4
+ /**
5
+ * The URL prefix the admin app is mounted under (default `/admin`).
6
+ * All internal links are built from this, so the admin folder works no matter
7
+ * where the consumer mounts it: `<AdminApp basePath="/backoffice" />`.
8
+ */
9
+ const BasePathContext = createContext("/admin");
10
+
11
+ export function BasePathProvider({ value = "/admin", children }) {
12
+ // normalizeBasePath also tolerates the route *pattern* being passed in
13
+ // (`basePath="/admin/*"`), an easy copy/paste slip from the mount — every
14
+ // internal link would otherwise carry a literal "*" segment.
15
+ return <BasePathContext.Provider value={normalizeBasePath(value)}>{children}</BasePathContext.Provider>;
16
+ }
17
+
18
+ export function useBasePath() {
19
+ return useContext(BasePathContext);
20
+ }
21
+
22
+ /** Returns a builder: href("orders/123") → "/admin/orders/123"; href() → "/admin". */
23
+ export function useAdminHref() {
24
+ const base = useBasePath();
25
+ return (path = "") => (path ? `${base}/${String(path).replace(/^\/+/, "")}` : base);
26
+ }
@@ -0,0 +1,207 @@
1
+ import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Loader2, Store } from "lucide-react";
9
+ import { toast } from "sonner";
10
+ import { base44, call } from "../lib/api";
11
+
12
+ const SettingsCtx = createContext(null);
13
+
14
+ export function useSettings() {
15
+ return useContext(SettingsCtx);
16
+ }
17
+
18
+ /**
19
+ * Loads all StoreSettings groups once and exposes:
20
+ * { settings, recordIds, get(group, key, fallback), saveGroup(group, values), refresh, isSeeded, loading }
21
+ * When the store is not seeded (no `general` group), renders the first-run
22
+ * SetupScreen instead of children.
23
+ */
24
+ export function SettingsProvider({ children }) {
25
+ const [state, setState] = useState({ loading: true, groups: {}, ids: {} });
26
+
27
+ const load = useCallback(async () => {
28
+ try {
29
+ const records = await base44.entities["commerce.StoreSettings"].list(undefined, 100);
30
+ const groups = {};
31
+ const ids = {};
32
+ (records || []).forEach((r) => {
33
+ groups[r.group_id] = r.values || {};
34
+ ids[r.group_id] = r.id;
35
+ });
36
+ setState({ loading: false, groups, ids });
37
+ } catch (err) {
38
+ setState({ loading: false, groups: {}, ids: {}, error: err });
39
+ }
40
+ }, []);
41
+
42
+ useEffect(() => {
43
+ load();
44
+ }, [load]);
45
+
46
+ const get = useCallback(
47
+ (group, key, fallback) => {
48
+ const v = state.groups?.[group]?.[key];
49
+ return v === undefined || v === null ? fallback : v;
50
+ },
51
+ [state.groups]
52
+ );
53
+
54
+ const saveGroup = useCallback(
55
+ async (group, values) => {
56
+ const id = state.ids[group];
57
+ if (id) await base44.entities["commerce.StoreSettings"].update(id, { values });
58
+ else await base44.entities["commerce.StoreSettings"].create({ group_id: group, values });
59
+ await load();
60
+ },
61
+ [state.ids, load]
62
+ );
63
+
64
+ const isSeeded = !!state.groups.general;
65
+ const value = {
66
+ loading: state.loading,
67
+ settings: state.groups,
68
+ recordIds: state.ids,
69
+ get,
70
+ saveGroup,
71
+ refresh: load,
72
+ isSeeded,
73
+ };
74
+
75
+ if (state.loading) {
76
+ return (
77
+ <div className="flex h-screen items-center justify-center">
78
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
79
+ </div>
80
+ );
81
+ }
82
+
83
+ if (!isSeeded) {
84
+ return (
85
+ <SettingsCtx.Provider value={value}>
86
+ <SetupScreen onDone={load} />
87
+ </SettingsCtx.Provider>
88
+ );
89
+ }
90
+
91
+ return <SettingsCtx.Provider value={value}>{children}</SettingsCtx.Provider>;
92
+ }
93
+
94
+ /** The app's name as the browser knows it, ignoring the platform's placeholder title. */
95
+ function appName() {
96
+ const title = (typeof document === "undefined" ? "" : document.title || "").trim();
97
+ return /^base44( app)?$/i.test(title) ? "" : title;
98
+ }
99
+
100
+ /** First-run screen: initialize store defaults via the commerce/seed-store function. */
101
+ function SetupScreen({ onDone }) {
102
+ const [hasProducts, setHasProducts] = useState(null);
103
+ const [withSample, setWithSample] = useState(false);
104
+ // Nothing the backend can read carries the app's name (a function's env is
105
+ // only BASE44_APP_ID), so the store name has to come from here.
106
+ const [storeName, setStoreName] = useState(() => appName());
107
+ const [running, setRunning] = useState(false);
108
+ const [schemaErrors, setSchemaErrors] = useState(null);
109
+
110
+ useEffect(() => {
111
+ base44.entities["commerce.Product"].list(undefined, 1)
112
+ .then((rows) => setHasProducts((rows || []).length > 0))
113
+ .catch(() => setHasProducts(true)); // on error, hide the sample-data option
114
+ }, []);
115
+
116
+ const run = async () => {
117
+ setRunning(true);
118
+ setSchemaErrors(null);
119
+ try {
120
+ await call("seed-store", null, { with_sample_data: withSample, store_name: storeName.trim() }, { silent: true });
121
+ toast.success("Store defaults initialized");
122
+ onDone();
123
+ } catch (err) {
124
+ if (err.code === "schema_incompatible") {
125
+ setSchemaErrors(err.details?.errors || [{ entity: "unknown", error: err.message }]);
126
+ } else {
127
+ toast.error(err.message);
128
+ }
129
+ } finally {
130
+ setRunning(false);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <div className="flex min-h-screen items-center justify-center bg-muted/30 p-4">
136
+ <Card className="w-full max-w-lg">
137
+ <CardHeader>
138
+ <div className="mb-2 flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
139
+ <Store className="h-5 w-5 text-primary" />
140
+ </div>
141
+ <CardTitle>Set up your store</CardTitle>
142
+ <CardDescription>
143
+ This looks like a fresh installation. Initialize the store with default
144
+ settings (currency, tax classes, payment methods, shipping zone and
145
+ email configuration) to start using the admin.
146
+ </CardDescription>
147
+ </CardHeader>
148
+ <CardContent className="space-y-4">
149
+ <div className="space-y-1.5">
150
+ <Label htmlFor="store-name">Store name</Label>
151
+ <Input
152
+ id="store-name"
153
+ value={storeName}
154
+ placeholder="Your store's name"
155
+ onChange={(e) => setStoreName(e.target.value)}
156
+ />
157
+ <p className="text-xs text-muted-foreground">
158
+ Shown to customers and used as the sender name on every transactional email.
159
+ Editable later in Settings → General.
160
+ </p>
161
+ </div>
162
+ {hasProducts === false && (
163
+ <div className="flex items-start gap-2 rounded-md border p-3">
164
+ <Checkbox
165
+ id="with-sample"
166
+ checked={withSample}
167
+ onCheckedChange={(v) => setWithSample(!!v)}
168
+ />
169
+ <div className="grid gap-1">
170
+ <Label htmlFor="with-sample" className="cursor-pointer">
171
+ Include sample data
172
+ </Label>
173
+ <p className="text-xs text-muted-foreground">
174
+ Adds demo categories, products (simple, variable, downloadable,
175
+ external, grouped) and coupons so you can explore the admin.
176
+ </p>
177
+ </div>
178
+ </div>
179
+ )}
180
+ {schemaErrors && (
181
+ <Alert variant="destructive">
182
+ <AlertTitle>Entity schemas are incompatible</AlertTitle>
183
+ <AlertDescription>
184
+ <p className="mb-2">
185
+ The entity schemas in this app have been modified in a way that
186
+ prevents seeding. Restore the template schemas or fix the fields
187
+ below, then retry.
188
+ </p>
189
+ <ul className="list-disc space-y-1 pl-4 text-xs">
190
+ {schemaErrors.map((e, i) => (
191
+ <li key={i}>
192
+ <span className="font-semibold">{e.entity}</span>: {e.error}
193
+ </li>
194
+ ))}
195
+ </ul>
196
+ </AlertDescription>
197
+ </Alert>
198
+ )}
199
+ <Button onClick={run} disabled={running || !storeName.trim()} className="w-full">
200
+ {running && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
201
+ Initialize store defaults
202
+ </Button>
203
+ </CardContent>
204
+ </Card>
205
+ </div>
206
+ );
207
+ }