@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,227 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "@/components/ui/select";
|
|
12
|
+
import { Loader2, Plus, Search, ShoppingCart } from "lucide-react";
|
|
13
|
+
import { toast } from "sonner";
|
|
14
|
+
|
|
15
|
+
import { call, base44 } from "../../lib/api";
|
|
16
|
+
import useAsync from "../../hooks/useAsync";
|
|
17
|
+
import usePagedList from "../../hooks/usePagedList";
|
|
18
|
+
import useRealtime from "../../hooks/useRealtime";
|
|
19
|
+
import useDebounce from "../../hooks/useDebounce";
|
|
20
|
+
import useMoney from "../../hooks/useMoney";
|
|
21
|
+
import { useAdminHref } from "../../context/BasePathContext";
|
|
22
|
+
import { ORDER_STATUSES } from "../../lib/constants";
|
|
23
|
+
import { formatDate } from "../../lib/format";
|
|
24
|
+
import { customerName } from "../../lib/order-utils";
|
|
25
|
+
import PageHeader from "../../components/PageHeader";
|
|
26
|
+
import DataTable from "../../components/DataTable";
|
|
27
|
+
import DateRangePicker from "../../components/DateRangePicker";
|
|
28
|
+
import StatusBadge from "../../components/StatusBadge";
|
|
29
|
+
|
|
30
|
+
const TABS = [{ value: "", label: "All" }, ...ORDER_STATUSES];
|
|
31
|
+
|
|
32
|
+
export default function OrdersList() {
|
|
33
|
+
const href = useAdminHref();
|
|
34
|
+
const navigate = useNavigate();
|
|
35
|
+
const { format } = useMoney();
|
|
36
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
37
|
+
|
|
38
|
+
const status = searchParams.get("status") || "";
|
|
39
|
+
const [query, setQuery] = useState("");
|
|
40
|
+
const [dateRange, setDateRange] = useState(null);
|
|
41
|
+
const [selected, setSelected] = useState([]);
|
|
42
|
+
const [bulkStatus, setBulkStatus] = useState("");
|
|
43
|
+
const [bulkBusy, setBulkBusy] = useState(false);
|
|
44
|
+
const [creating, setCreating] = useState(false);
|
|
45
|
+
|
|
46
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
47
|
+
const searchMode = Boolean(debouncedQuery.trim() || dateRange?.from);
|
|
48
|
+
|
|
49
|
+
const counts = useAsync(async () => {
|
|
50
|
+
const data = await call("admin-orders", "status-counts", {}, { silent: true });
|
|
51
|
+
return data?.counts || data || {};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const list = usePagedList(
|
|
55
|
+
async (limit, skip, sort) => {
|
|
56
|
+
if (searchMode) {
|
|
57
|
+
const data = await call("admin-orders", "search", {
|
|
58
|
+
q: debouncedQuery.trim(),
|
|
59
|
+
status: status || undefined,
|
|
60
|
+
date_min: dateRange?.from || undefined,
|
|
61
|
+
date_max: dateRange?.to || undefined,
|
|
62
|
+
limit,
|
|
63
|
+
skip,
|
|
64
|
+
sort,
|
|
65
|
+
});
|
|
66
|
+
return data?.rows || data || [];
|
|
67
|
+
}
|
|
68
|
+
return base44.entities["commerce.Order"].filter(status ? { status } : {}, sort, limit, skip);
|
|
69
|
+
},
|
|
70
|
+
{ deps: [debouncedQuery, status, dateRange?.from, dateRange?.to] }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Orders arrive without the admin doing anything, so keep the list and the
|
|
74
|
+
// status tab counts live. Quiet refetches leave the table on screen.
|
|
75
|
+
useRealtime(["commerce.Order", "commerce.OrderRefund"], () => {
|
|
76
|
+
list.refetchQuiet();
|
|
77
|
+
counts.refetchQuiet();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const setStatusTab = (value) => {
|
|
81
|
+
setSelected([]);
|
|
82
|
+
const next = new URLSearchParams(searchParams);
|
|
83
|
+
if (value) next.set("status", value);
|
|
84
|
+
else next.delete("status");
|
|
85
|
+
setSearchParams(next, { replace: true });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const applyBulk = async () => {
|
|
89
|
+
if (!bulkStatus || selected.length === 0) return;
|
|
90
|
+
setBulkBusy(true);
|
|
91
|
+
try {
|
|
92
|
+
await call("admin-orders", "bulk-status", { ids: selected, status: bulkStatus });
|
|
93
|
+
toast.success(`${selected.length} order(s) updated`);
|
|
94
|
+
setSelected([]);
|
|
95
|
+
setBulkStatus("");
|
|
96
|
+
list.refetch();
|
|
97
|
+
counts.refetch();
|
|
98
|
+
} catch {
|
|
99
|
+
/* toast handled by call() */
|
|
100
|
+
} finally {
|
|
101
|
+
setBulkBusy(false);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const addOrder = async () => {
|
|
106
|
+
setCreating(true);
|
|
107
|
+
try {
|
|
108
|
+
const data = await call("admin-orders", "create-draft");
|
|
109
|
+
const id = data?.id || data?.order_id || data?.order?.id;
|
|
110
|
+
if (id) navigate(href(`orders/${id}`));
|
|
111
|
+
} catch {
|
|
112
|
+
/* toast handled by call() */
|
|
113
|
+
} finally {
|
|
114
|
+
setCreating(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const columns = useMemo(
|
|
119
|
+
() => [
|
|
120
|
+
{
|
|
121
|
+
key: "order_number",
|
|
122
|
+
label: "Order",
|
|
123
|
+
render: (o) => (
|
|
124
|
+
<span className="font-medium">
|
|
125
|
+
#{o.order_number || o.id} <span className="font-normal text-muted-foreground">{customerName(o)}</span>
|
|
126
|
+
</span>
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
{ key: "created_date", label: "Date", sortable: true, render: (o) => formatDate(o.created_date) },
|
|
130
|
+
{ key: "status", label: "Status", render: (o) => <StatusBadge status={o.status} /> },
|
|
131
|
+
{ key: "total", label: "Total", sortable: true, className: "text-right", render: (o) => format(o.total || 0) },
|
|
132
|
+
],
|
|
133
|
+
[format]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const totalCount = Object.values(counts.data || {}).reduce((a, b) => a + (b || 0), 0);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<PageHeader
|
|
141
|
+
title="Orders"
|
|
142
|
+
actions={
|
|
143
|
+
<Button onClick={addOrder} disabled={creating}>
|
|
144
|
+
{creating ? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" /> : <Plus className="mr-1.5 h-4 w-4" />}
|
|
145
|
+
Add order
|
|
146
|
+
</Button>
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
{/* Status tabs */}
|
|
151
|
+
<div className="mb-4 flex flex-wrap gap-1 border-b pb-px">
|
|
152
|
+
{TABS.map((tab) => {
|
|
153
|
+
const count = tab.value === "" ? totalCount : (counts.data || {})[tab.value] || 0;
|
|
154
|
+
const active = status === tab.value;
|
|
155
|
+
return (
|
|
156
|
+
<button
|
|
157
|
+
key={tab.value || "all"}
|
|
158
|
+
onClick={() => setStatusTab(tab.value)}
|
|
159
|
+
className={`-mb-px border-b-2 px-3 py-2 text-sm transition-colors ${
|
|
160
|
+
active
|
|
161
|
+
? "border-primary font-medium text-foreground"
|
|
162
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
163
|
+
}`}
|
|
164
|
+
>
|
|
165
|
+
{tab.label}
|
|
166
|
+
<span className="ml-1 text-xs text-muted-foreground">({count})</span>
|
|
167
|
+
</button>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Filters + bulk bar */}
|
|
173
|
+
<div className="mb-4 flex flex-wrap items-center gap-2">
|
|
174
|
+
<div className="relative">
|
|
175
|
+
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
176
|
+
<Input
|
|
177
|
+
value={query}
|
|
178
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
179
|
+
placeholder="Search order #, name, email…"
|
|
180
|
+
className="w-64 pl-8"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
<DateRangePicker value={dateRange} onChange={setDateRange} />
|
|
184
|
+
{selected.length > 0 && (
|
|
185
|
+
<div className="ml-auto flex items-center gap-2 rounded-md border bg-muted/50 px-2 py-1">
|
|
186
|
+
<span className="text-sm text-muted-foreground">{selected.length} selected</span>
|
|
187
|
+
<Select value={bulkStatus} onValueChange={setBulkStatus}>
|
|
188
|
+
<SelectTrigger className="h-8 w-44">
|
|
189
|
+
<SelectValue placeholder="Change status to…" />
|
|
190
|
+
</SelectTrigger>
|
|
191
|
+
<SelectContent>
|
|
192
|
+
{ORDER_STATUSES.map((s) => (
|
|
193
|
+
<SelectItem key={s.value} value={s.value}>
|
|
194
|
+
{s.label}
|
|
195
|
+
</SelectItem>
|
|
196
|
+
))}
|
|
197
|
+
</SelectContent>
|
|
198
|
+
</Select>
|
|
199
|
+
<Button size="sm" onClick={applyBulk} disabled={!bulkStatus || bulkBusy}>
|
|
200
|
+
{bulkBusy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
201
|
+
Apply
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<DataTable
|
|
208
|
+
columns={columns}
|
|
209
|
+
rows={list.rows}
|
|
210
|
+
loading={list.loading}
|
|
211
|
+
sort={list.sort}
|
|
212
|
+
onSort={list.setSort}
|
|
213
|
+
selectable
|
|
214
|
+
selected={selected}
|
|
215
|
+
onSelectChange={setSelected}
|
|
216
|
+
onRowClick={(o) => navigate(href(`orders/${o.id}`))}
|
|
217
|
+
rowActions={(o) => [{ label: "View / edit", onClick: () => navigate(href(`orders/${o.id}`)) }]}
|
|
218
|
+
pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
|
|
219
|
+
empty={{
|
|
220
|
+
icon: ShoppingCart,
|
|
221
|
+
title: "No orders found",
|
|
222
|
+
description: searchMode ? "Try adjusting your search or date range." : "Orders will appear here as they come in.",
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogFooter,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from "@/components/ui/dialog";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { Label } from "@/components/ui/label";
|
|
12
|
+
import {
|
|
13
|
+
Select,
|
|
14
|
+
SelectContent,
|
|
15
|
+
SelectItem,
|
|
16
|
+
SelectTrigger,
|
|
17
|
+
SelectValue,
|
|
18
|
+
} from "@/components/ui/select";
|
|
19
|
+
import { Loader2 } from "lucide-react";
|
|
20
|
+
|
|
21
|
+
import { call, base44 } from "../../../lib/api";
|
|
22
|
+
import SearchSelect from "../../../components/SearchSelect";
|
|
23
|
+
import { variationLabel } from "../../../lib/product-utils";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Two-step product picker for the order editor:
|
|
27
|
+
* product search → (variable only) variation select → quantity.
|
|
28
|
+
*
|
|
29
|
+
* Props: { open, onOpenChange, onAdd({product, variation|null, quantity}) }
|
|
30
|
+
*/
|
|
31
|
+
export default function AddProductDialog({ open, onOpenChange, onAdd }) {
|
|
32
|
+
const [picked, setPicked] = useState(null); // {value,label,meta,product}
|
|
33
|
+
const [variations, setVariations] = useState(null); // null = not loaded
|
|
34
|
+
const [variationId, setVariationId] = useState("");
|
|
35
|
+
const [quantity, setQuantity] = useState(1);
|
|
36
|
+
const [loadingVariations, setLoadingVariations] = useState(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!open) {
|
|
40
|
+
setPicked(null);
|
|
41
|
+
setVariations(null);
|
|
42
|
+
setVariationId("");
|
|
43
|
+
setQuantity(1);
|
|
44
|
+
}
|
|
45
|
+
}, [open]);
|
|
46
|
+
|
|
47
|
+
const searchProducts = async (q) => {
|
|
48
|
+
const data = await call("admin-products", "search", { q, limit: 20 }, { silent: true });
|
|
49
|
+
const rows = data?.rows || data || [];
|
|
50
|
+
return rows.map((p) => ({
|
|
51
|
+
value: p.id,
|
|
52
|
+
label: p.name,
|
|
53
|
+
meta: [p.sku && `SKU: ${p.sku}`, p.type].filter(Boolean).join(" · "),
|
|
54
|
+
product: p,
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handlePick = async (opt) => {
|
|
59
|
+
setPicked(opt);
|
|
60
|
+
setVariations(null);
|
|
61
|
+
setVariationId("");
|
|
62
|
+
if (opt?.product?.type === "variable") {
|
|
63
|
+
setLoadingVariations(true);
|
|
64
|
+
try {
|
|
65
|
+
const rows = await base44.entities["commerce.ProductVariation"].filter({ product_id: opt.value }, "menu_order", 200);
|
|
66
|
+
setVariations(rows || []);
|
|
67
|
+
} finally {
|
|
68
|
+
setLoadingVariations(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const isVariable = picked?.product?.type === "variable";
|
|
74
|
+
const canAdd = picked && quantity > 0 && (!isVariable || variationId);
|
|
75
|
+
|
|
76
|
+
const submit = () => {
|
|
77
|
+
const variation = isVariable ? (variations || []).find((v) => v.id === variationId) : null;
|
|
78
|
+
onAdd({ product: picked.product, variation, quantity });
|
|
79
|
+
onOpenChange(false);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
84
|
+
<DialogContent className="sm:max-w-md">
|
|
85
|
+
<DialogHeader>
|
|
86
|
+
<DialogTitle>Add product</DialogTitle>
|
|
87
|
+
</DialogHeader>
|
|
88
|
+
|
|
89
|
+
<div className="space-y-4">
|
|
90
|
+
<div className="grid gap-1.5">
|
|
91
|
+
<Label>Product</Label>
|
|
92
|
+
<SearchSelect
|
|
93
|
+
search={searchProducts}
|
|
94
|
+
value={picked}
|
|
95
|
+
onChange={handlePick}
|
|
96
|
+
placeholder="Search products…"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{isVariable && (
|
|
101
|
+
<div className="grid gap-1.5">
|
|
102
|
+
<Label>Variation</Label>
|
|
103
|
+
{loadingVariations ? (
|
|
104
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
105
|
+
<Loader2 className="h-4 w-4 animate-spin" /> Loading variations…
|
|
106
|
+
</div>
|
|
107
|
+
) : (variations || []).length === 0 ? (
|
|
108
|
+
<p className="text-sm text-muted-foreground">This product has no variations yet.</p>
|
|
109
|
+
) : (
|
|
110
|
+
<Select value={variationId} onValueChange={setVariationId}>
|
|
111
|
+
<SelectTrigger>
|
|
112
|
+
<SelectValue placeholder="Choose a variation…" />
|
|
113
|
+
</SelectTrigger>
|
|
114
|
+
<SelectContent>
|
|
115
|
+
{(variations || []).map((v) => (
|
|
116
|
+
<SelectItem key={v.id} value={v.id}>
|
|
117
|
+
{variationLabel(v) || v.sku || v.id}
|
|
118
|
+
</SelectItem>
|
|
119
|
+
))}
|
|
120
|
+
</SelectContent>
|
|
121
|
+
</Select>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
<div className="grid gap-1.5">
|
|
127
|
+
<Label>Quantity</Label>
|
|
128
|
+
<Input
|
|
129
|
+
type="number"
|
|
130
|
+
min="1"
|
|
131
|
+
className="w-24"
|
|
132
|
+
value={quantity}
|
|
133
|
+
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value, 10) || 1))}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<DialogFooter>
|
|
139
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
140
|
+
Cancel
|
|
141
|
+
</Button>
|
|
142
|
+
<Button onClick={submit} disabled={!canAdd}>
|
|
143
|
+
Add to order
|
|
144
|
+
</Button>
|
|
145
|
+
</DialogFooter>
|
|
146
|
+
</DialogContent>
|
|
147
|
+
</Dialog>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from "@/components/ui/table";
|
|
12
|
+
import { Loader2, X } from "lucide-react";
|
|
13
|
+
import { toast } from "sonner";
|
|
14
|
+
|
|
15
|
+
import { call } from "../../../lib/api";
|
|
16
|
+
import SearchSelect from "../../../components/SearchSelect";
|
|
17
|
+
import { formatDate } from "../../../lib/format";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Downloadable product access for an order: grant / revoke rows.
|
|
21
|
+
* Props: { orderId, permissions, loading, onChanged() }
|
|
22
|
+
*/
|
|
23
|
+
export default function DownloadPermissionsPanel({ orderId, permissions, loading, onChanged }) {
|
|
24
|
+
const [picked, setPicked] = useState(null);
|
|
25
|
+
const [busy, setBusy] = useState(false);
|
|
26
|
+
|
|
27
|
+
const searchDownloadable = async (q) => {
|
|
28
|
+
const data = await call("admin-products", "search", { q, limit: 20 }, { silent: true });
|
|
29
|
+
const rows = data?.rows || data || [];
|
|
30
|
+
return rows
|
|
31
|
+
.filter((p) => p.downloadable)
|
|
32
|
+
.map((p) => ({ value: p.id, label: p.name, meta: p.sku ? `SKU: ${p.sku}` : undefined }));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const grant = async () => {
|
|
36
|
+
if (!picked) return;
|
|
37
|
+
setBusy(true);
|
|
38
|
+
try {
|
|
39
|
+
await call("admin-orders", "grant-download", { order_id: orderId, product_id: picked.value });
|
|
40
|
+
toast.success("Download access granted");
|
|
41
|
+
setPicked(null);
|
|
42
|
+
onChanged();
|
|
43
|
+
} catch {
|
|
44
|
+
/* toast handled by call() */
|
|
45
|
+
} finally {
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const revoke = async (permissionId) => {
|
|
51
|
+
try {
|
|
52
|
+
await call("admin-orders", "revoke-download", { order_id: orderId, permission_id: permissionId });
|
|
53
|
+
toast.success("Download access revoked");
|
|
54
|
+
onChanged();
|
|
55
|
+
} catch {
|
|
56
|
+
/* toast handled by call() */
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader className="pb-3">
|
|
63
|
+
<CardTitle className="text-base">Downloadable product permissions</CardTitle>
|
|
64
|
+
</CardHeader>
|
|
65
|
+
<CardContent className="space-y-3">
|
|
66
|
+
{loading ? (
|
|
67
|
+
<div className="flex justify-center py-4">
|
|
68
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
69
|
+
</div>
|
|
70
|
+
) : (permissions || []).length === 0 ? (
|
|
71
|
+
<p className="text-sm text-muted-foreground">No download permissions on this order.</p>
|
|
72
|
+
) : (
|
|
73
|
+
<Table>
|
|
74
|
+
<TableHeader>
|
|
75
|
+
<TableRow>
|
|
76
|
+
<TableHead>File</TableHead>
|
|
77
|
+
<TableHead className="w-32 text-right">Remaining</TableHead>
|
|
78
|
+
<TableHead className="w-32">Expires</TableHead>
|
|
79
|
+
<TableHead className="w-10" />
|
|
80
|
+
</TableRow>
|
|
81
|
+
</TableHeader>
|
|
82
|
+
<TableBody>
|
|
83
|
+
{permissions.map((p) => (
|
|
84
|
+
<TableRow key={p.id}>
|
|
85
|
+
<TableCell className="text-sm">{p.download_name || p.file_url || p.product_id}</TableCell>
|
|
86
|
+
<TableCell className="text-right text-sm">
|
|
87
|
+
{p.downloads_remaining === -1 ? "∞" : p.downloads_remaining}
|
|
88
|
+
<span className="text-xs text-muted-foreground"> (used {p.download_count || 0})</span>
|
|
89
|
+
</TableCell>
|
|
90
|
+
<TableCell className="text-sm">{p.access_expires ? formatDate(p.access_expires) : "Never"}</TableCell>
|
|
91
|
+
<TableCell>
|
|
92
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => revoke(p.id)} title="Revoke">
|
|
93
|
+
<X className="h-4 w-4" />
|
|
94
|
+
</Button>
|
|
95
|
+
</TableCell>
|
|
96
|
+
</TableRow>
|
|
97
|
+
))}
|
|
98
|
+
</TableBody>
|
|
99
|
+
</Table>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<div className="flex items-end gap-2 border-t pt-3">
|
|
103
|
+
<div className="flex-1">
|
|
104
|
+
<SearchSelect
|
|
105
|
+
search={searchDownloadable}
|
|
106
|
+
value={picked}
|
|
107
|
+
onChange={setPicked}
|
|
108
|
+
placeholder="Search downloadable products…"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
<Button size="sm" onClick={grant} disabled={!picked || busy}>
|
|
112
|
+
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
113
|
+
Grant access
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</CardContent>
|
|
117
|
+
</Card>
|
|
118
|
+
);
|
|
119
|
+
}
|