@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,208 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
} from "@/components/ui/table";
|
|
13
|
+
import { Package, X } from "lucide-react";
|
|
14
|
+
|
|
15
|
+
import useMoney from "../../../hooks/useMoney";
|
|
16
|
+
import { round2 } from "../../../lib/order-utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Order line items table: product lines, fee lines, shipping lines, coupon chips.
|
|
20
|
+
*
|
|
21
|
+
* Props:
|
|
22
|
+
* - order: the (draft) order object being displayed/edited
|
|
23
|
+
* - editable: whether qty/price edits & removals are allowed (pending/on-hold)
|
|
24
|
+
* - onChange(patch): merge a partial {line_items|fee_lines|shipping_lines} into the draft
|
|
25
|
+
* - onRemoveCoupon(code): server-side removal (disabled while dirty upstream)
|
|
26
|
+
* - removingCoupon: code currently being removed
|
|
27
|
+
*/
|
|
28
|
+
export default function LineItemsTable({ order, editable, onChange, onRemoveCoupon, removingCoupon }) {
|
|
29
|
+
const { format } = useMoney();
|
|
30
|
+
const lines = order.line_items || [];
|
|
31
|
+
const fees = order.fee_lines || [];
|
|
32
|
+
const shipping = order.shipping_lines || [];
|
|
33
|
+
const coupons = order.coupon_lines || [];
|
|
34
|
+
|
|
35
|
+
const updateLine = (lineId, patch) => {
|
|
36
|
+
onChange({
|
|
37
|
+
line_items: lines.map((l) => {
|
|
38
|
+
if (l.line_id !== lineId) return l;
|
|
39
|
+
const next = { ...l, ...patch };
|
|
40
|
+
// Keep display totals in sync locally; the server recalculates
|
|
41
|
+
// authoritative totals (incl. tax) on save.
|
|
42
|
+
next.subtotal = round2((next.price || 0) * (next.quantity || 0));
|
|
43
|
+
next.total = next.subtotal;
|
|
44
|
+
return next;
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const removeLine = (lineId) => onChange({ line_items: lines.filter((l) => l.line_id !== lineId) });
|
|
50
|
+
const removeFee = (lineId) => onChange({ fee_lines: fees.filter((f) => f.line_id !== lineId) });
|
|
51
|
+
const removeShipping = (lineId) =>
|
|
52
|
+
onChange({ shipping_lines: shipping.filter((s) => s.line_id !== lineId) });
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<Table>
|
|
57
|
+
<TableHeader>
|
|
58
|
+
<TableRow>
|
|
59
|
+
<TableHead>Item</TableHead>
|
|
60
|
+
<TableHead className="w-28 text-right">Cost</TableHead>
|
|
61
|
+
<TableHead className="w-20 text-right">Qty</TableHead>
|
|
62
|
+
<TableHead className="w-28 text-right">Total</TableHead>
|
|
63
|
+
<TableHead className="w-24 text-right">Tax</TableHead>
|
|
64
|
+
{editable && <TableHead className="w-10" />}
|
|
65
|
+
</TableRow>
|
|
66
|
+
</TableHeader>
|
|
67
|
+
<TableBody>
|
|
68
|
+
{lines.length === 0 && fees.length === 0 && shipping.length === 0 && (
|
|
69
|
+
<TableRow>
|
|
70
|
+
<TableCell colSpan={editable ? 6 : 5} className="py-8 text-center text-sm text-muted-foreground">
|
|
71
|
+
No items. {editable ? "Use “Add product(s)” below." : ""}
|
|
72
|
+
</TableCell>
|
|
73
|
+
</TableRow>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{lines.map((line) => (
|
|
77
|
+
<TableRow key={line.line_id}>
|
|
78
|
+
<TableCell>
|
|
79
|
+
<div className="flex items-center gap-3">
|
|
80
|
+
{line.image ? (
|
|
81
|
+
<img src={line.image} alt="" className="h-9 w-9 rounded object-cover" />
|
|
82
|
+
) : (
|
|
83
|
+
<div className="flex h-9 w-9 items-center justify-center rounded bg-muted">
|
|
84
|
+
<Package className="h-4 w-4 text-muted-foreground" />
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
<div className="min-w-0">
|
|
88
|
+
<p className="truncate text-sm font-medium">{line.name}</p>
|
|
89
|
+
<p className="text-xs text-muted-foreground">
|
|
90
|
+
{line.sku && <span>SKU: {line.sku}</span>}
|
|
91
|
+
{(line.attributes || []).length > 0 && (
|
|
92
|
+
<span>
|
|
93
|
+
{line.sku ? " · " : ""}
|
|
94
|
+
{line.attributes.map((a) => `${a.name}: ${a.option}`).join(", ")}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</TableCell>
|
|
101
|
+
<TableCell className="text-right">
|
|
102
|
+
{editable ? (
|
|
103
|
+
<Input
|
|
104
|
+
type="number"
|
|
105
|
+
step="any"
|
|
106
|
+
className="ml-auto h-8 w-24 text-right"
|
|
107
|
+
value={line.price ?? 0}
|
|
108
|
+
onChange={(e) => updateLine(line.line_id, { price: Number(e.target.value) || 0 })}
|
|
109
|
+
/>
|
|
110
|
+
) : (
|
|
111
|
+
format(line.price || 0)
|
|
112
|
+
)}
|
|
113
|
+
</TableCell>
|
|
114
|
+
<TableCell className="text-right">
|
|
115
|
+
{editable ? (
|
|
116
|
+
<Input
|
|
117
|
+
type="number"
|
|
118
|
+
min="1"
|
|
119
|
+
className="ml-auto h-8 w-16 text-right"
|
|
120
|
+
value={line.quantity ?? 1}
|
|
121
|
+
onChange={(e) =>
|
|
122
|
+
updateLine(line.line_id, { quantity: Math.max(1, parseInt(e.target.value, 10) || 1) })
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
`× ${line.quantity}`
|
|
127
|
+
)}
|
|
128
|
+
</TableCell>
|
|
129
|
+
<TableCell className="text-right">{format(line.total || 0)}</TableCell>
|
|
130
|
+
<TableCell className="text-right text-muted-foreground">{format(line.total_tax || 0)}</TableCell>
|
|
131
|
+
{editable && (
|
|
132
|
+
<TableCell className="text-right">
|
|
133
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => removeLine(line.line_id)}>
|
|
134
|
+
<X className="h-4 w-4" />
|
|
135
|
+
</Button>
|
|
136
|
+
</TableCell>
|
|
137
|
+
)}
|
|
138
|
+
</TableRow>
|
|
139
|
+
))}
|
|
140
|
+
|
|
141
|
+
{fees.map((fee) => (
|
|
142
|
+
<TableRow key={fee.line_id} className="bg-muted/30">
|
|
143
|
+
<TableCell>
|
|
144
|
+
<span className="text-sm">
|
|
145
|
+
<Badge variant="outline" className="mr-2">Fee</Badge>
|
|
146
|
+
{fee.name}
|
|
147
|
+
</span>
|
|
148
|
+
</TableCell>
|
|
149
|
+
<TableCell />
|
|
150
|
+
<TableCell />
|
|
151
|
+
<TableCell className="text-right">{format(fee.total || 0)}</TableCell>
|
|
152
|
+
<TableCell className="text-right text-muted-foreground">{format(fee.total_tax || 0)}</TableCell>
|
|
153
|
+
{editable && (
|
|
154
|
+
<TableCell className="text-right">
|
|
155
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => removeFee(fee.line_id)}>
|
|
156
|
+
<X className="h-4 w-4" />
|
|
157
|
+
</Button>
|
|
158
|
+
</TableCell>
|
|
159
|
+
)}
|
|
160
|
+
</TableRow>
|
|
161
|
+
))}
|
|
162
|
+
|
|
163
|
+
{shipping.map((line) => (
|
|
164
|
+
<TableRow key={line.line_id} className="bg-muted/30">
|
|
165
|
+
<TableCell>
|
|
166
|
+
<span className="text-sm">
|
|
167
|
+
<Badge variant="outline" className="mr-2">Shipping</Badge>
|
|
168
|
+
{line.method_title || line.method_id}
|
|
169
|
+
</span>
|
|
170
|
+
</TableCell>
|
|
171
|
+
<TableCell />
|
|
172
|
+
<TableCell />
|
|
173
|
+
<TableCell className="text-right">{format(line.total || 0)}</TableCell>
|
|
174
|
+
<TableCell className="text-right text-muted-foreground">{format(line.total_tax || 0)}</TableCell>
|
|
175
|
+
{editable && (
|
|
176
|
+
<TableCell className="text-right">
|
|
177
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => removeShipping(line.line_id)}>
|
|
178
|
+
<X className="h-4 w-4" />
|
|
179
|
+
</Button>
|
|
180
|
+
</TableCell>
|
|
181
|
+
)}
|
|
182
|
+
</TableRow>
|
|
183
|
+
))}
|
|
184
|
+
</TableBody>
|
|
185
|
+
</Table>
|
|
186
|
+
|
|
187
|
+
{coupons.length > 0 && (
|
|
188
|
+
<div className="flex flex-wrap items-center gap-2 border-t px-3 py-2">
|
|
189
|
+
<span className="text-xs text-muted-foreground">Coupons:</span>
|
|
190
|
+
{coupons.map((c) => (
|
|
191
|
+
<Badge key={c.code} variant="secondary" className="gap-1 pr-1">
|
|
192
|
+
{c.code} (−{format(c.discount || 0)})
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className="rounded-full p-0.5 hover:bg-muted-foreground/20 disabled:opacity-50"
|
|
196
|
+
disabled={removingCoupon === c.code}
|
|
197
|
+
onClick={() => onRemoveCoupon(c.code)}
|
|
198
|
+
title="Remove coupon"
|
|
199
|
+
>
|
|
200
|
+
<X className="h-3 w-3" />
|
|
201
|
+
</button>
|
|
202
|
+
</Badge>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { Textarea } from "@/components/ui/textarea";
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "@/components/ui/select";
|
|
12
|
+
import { Loader2, X } from "lucide-react";
|
|
13
|
+
import { toast } from "sonner";
|
|
14
|
+
|
|
15
|
+
import { call } from "../../../lib/api";
|
|
16
|
+
import { formatDateTime } from "../../../lib/format";
|
|
17
|
+
|
|
18
|
+
/** Note tone: system = gray, private = amber, note-to-customer = blue. */
|
|
19
|
+
function noteClasses(note) {
|
|
20
|
+
if (note.is_customer_note) return "border-blue-200 bg-blue-50";
|
|
21
|
+
if (note.added_by === "system") return "border-gray-200 bg-gray-50";
|
|
22
|
+
return "border-amber-200 bg-amber-50";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Order notes feed + add-note form.
|
|
27
|
+
* Props: { orderId, notes, loading, onChanged() }
|
|
28
|
+
*/
|
|
29
|
+
export default function OrderNotesPanel({ orderId, notes, loading, onChanged }) {
|
|
30
|
+
const [text, setText] = useState("");
|
|
31
|
+
const [kind, setKind] = useState("private");
|
|
32
|
+
const [busy, setBusy] = useState(false);
|
|
33
|
+
|
|
34
|
+
const addNote = async () => {
|
|
35
|
+
if (!text.trim()) return;
|
|
36
|
+
setBusy(true);
|
|
37
|
+
try {
|
|
38
|
+
await call("admin-orders", "add-note", {
|
|
39
|
+
order_id: orderId,
|
|
40
|
+
note: text.trim(),
|
|
41
|
+
is_customer_note: kind === "customer",
|
|
42
|
+
});
|
|
43
|
+
toast.success(kind === "customer" ? "Note sent to customer" : "Note added");
|
|
44
|
+
setText("");
|
|
45
|
+
onChanged();
|
|
46
|
+
} catch {
|
|
47
|
+
/* toast handled by call() */
|
|
48
|
+
} finally {
|
|
49
|
+
setBusy(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const deleteNote = async (noteId) => {
|
|
54
|
+
try {
|
|
55
|
+
await call("admin-orders", "delete-note", { note_id: noteId });
|
|
56
|
+
onChanged();
|
|
57
|
+
} catch {
|
|
58
|
+
/* toast handled by call() */
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader className="pb-3">
|
|
65
|
+
<CardTitle className="text-base">Order notes</CardTitle>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent className="space-y-3">
|
|
68
|
+
{loading ? (
|
|
69
|
+
<div className="flex justify-center py-4">
|
|
70
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
71
|
+
</div>
|
|
72
|
+
) : (notes || []).length === 0 ? (
|
|
73
|
+
<p className="text-sm text-muted-foreground">No notes yet.</p>
|
|
74
|
+
) : (
|
|
75
|
+
<ul className="max-h-96 space-y-2 overflow-y-auto pr-1">
|
|
76
|
+
{notes.map((n) => (
|
|
77
|
+
<li key={n.id} className={`group relative rounded-md border p-2.5 text-sm ${noteClasses(n)}`}>
|
|
78
|
+
<p className="whitespace-pre-wrap">{n.note}</p>
|
|
79
|
+
<p className="mt-1.5 text-xs text-muted-foreground">
|
|
80
|
+
{formatDateTime(n.created_date)}
|
|
81
|
+
{n.added_by && n.added_by !== "system" ? ` · ${n.added_by}` : n.added_by === "system" ? " · system" : ""}
|
|
82
|
+
{n.is_customer_note ? " · sent to customer" : ""}
|
|
83
|
+
</p>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => deleteNote(n.id)}
|
|
87
|
+
className="absolute right-1.5 top-1.5 hidden rounded p-0.5 hover:bg-black/5 group-hover:block"
|
|
88
|
+
title="Delete note"
|
|
89
|
+
>
|
|
90
|
+
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
|
91
|
+
</button>
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
<div className="space-y-2 border-t pt-3">
|
|
98
|
+
<Textarea
|
|
99
|
+
rows={3}
|
|
100
|
+
placeholder="Add a note…"
|
|
101
|
+
value={text}
|
|
102
|
+
onChange={(e) => setText(e.target.value)}
|
|
103
|
+
/>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Select value={kind} onValueChange={setKind}>
|
|
106
|
+
<SelectTrigger className="h-8 flex-1">
|
|
107
|
+
<SelectValue />
|
|
108
|
+
</SelectTrigger>
|
|
109
|
+
<SelectContent>
|
|
110
|
+
<SelectItem value="private">Private note</SelectItem>
|
|
111
|
+
<SelectItem value="customer">Note to customer</SelectItem>
|
|
112
|
+
</SelectContent>
|
|
113
|
+
</Select>
|
|
114
|
+
<Button size="sm" onClick={addNote} disabled={busy || !text.trim()}>
|
|
115
|
+
{busy && <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
|
116
|
+
Add
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</CardContent>
|
|
121
|
+
</Card>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { Check, Copy, CreditCard, ExternalLink, Loader2, RotateCw } from "lucide-react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
|
|
9
|
+
import { call } from "../../../lib/api";
|
|
10
|
+
import useMoney from "../../../hooks/useMoney";
|
|
11
|
+
import usePaymentProvider from "../../../hooks/usePaymentProvider";
|
|
12
|
+
import { formatDateTime } from "../../../lib/format";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Payment state for an order, plus the two things an operator wants when an
|
|
16
|
+
* order is unpaid: a **payment link** to send the customer, and a way to
|
|
17
|
+
* **re-check** whether the money arrived.
|
|
18
|
+
*
|
|
19
|
+
* Both come from `commerce/payments`, which is provider-neutral — this panel
|
|
20
|
+
* never names a payment provider. When none is connected the actions are
|
|
21
|
+
* disabled with a "No payment provider connected" tooltip rather than being
|
|
22
|
+
* hidden, so the operator can see the capability exists and what unlocks it.
|
|
23
|
+
*
|
|
24
|
+
* Props: { order, onChanged }
|
|
25
|
+
*/
|
|
26
|
+
export default function PaymentPanel({ order, onChanged }) {
|
|
27
|
+
const { format } = useMoney();
|
|
28
|
+
const provider = usePaymentProvider();
|
|
29
|
+
const [working, setWorking] = useState(""); // link | verify
|
|
30
|
+
const [link, setLink] = useState("");
|
|
31
|
+
const [copied, setCopied] = useState(false);
|
|
32
|
+
|
|
33
|
+
const paid = Boolean(order.date_paid) || ["processing", "completed", "refunded"].includes(order.status);
|
|
34
|
+
const known = !provider.loading && Boolean(provider.gatewaySlug);
|
|
35
|
+
const isOnline = known && order.payment_method === provider.gatewaySlug;
|
|
36
|
+
// With a provider connected, an unpaid order can always be collected online —
|
|
37
|
+
// whatever its current method is. An admin order starts with no method at all,
|
|
38
|
+
// and "just send them a card link" is a normal thing to want for a manual one;
|
|
39
|
+
// the backend records the switch on the order.
|
|
40
|
+
const canAct = provider.connected && !paid;
|
|
41
|
+
const willSwitchMethod = canAct && known && !isOnline;
|
|
42
|
+
const disabledReason = !provider.connected ? "No payment provider connected" : undefined;
|
|
43
|
+
|
|
44
|
+
const createLink = async () => {
|
|
45
|
+
setWorking("link");
|
|
46
|
+
try {
|
|
47
|
+
// No return_url — the admin doesn't know the storefront's routes; the
|
|
48
|
+
// backend builds it from the store's settings.
|
|
49
|
+
const data = await call("payments", "create-link", { order_id: order.id });
|
|
50
|
+
setLink(data?.url || "");
|
|
51
|
+
setCopied(false);
|
|
52
|
+
if (data?.url) {
|
|
53
|
+
toast.success("Payment link created");
|
|
54
|
+
if (willSwitchMethod) onChanged?.(); // the order's method changed
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
/* toast handled by call() */
|
|
58
|
+
} finally {
|
|
59
|
+
setWorking("");
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const checkPayment = async () => {
|
|
64
|
+
setWorking("verify");
|
|
65
|
+
try {
|
|
66
|
+
const data = await call("payments", "verify", { order_id: order.id });
|
|
67
|
+
if (data?.paid) {
|
|
68
|
+
toast.success("Payment confirmed");
|
|
69
|
+
onChanged?.();
|
|
70
|
+
} else {
|
|
71
|
+
toast.info("No payment received yet");
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
/* toast handled by call() */
|
|
75
|
+
} finally {
|
|
76
|
+
setWorking("");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const copy = async () => {
|
|
81
|
+
try {
|
|
82
|
+
await navigator.clipboard.writeText(link);
|
|
83
|
+
setCopied(true);
|
|
84
|
+
toast.success("Link copied");
|
|
85
|
+
} catch {
|
|
86
|
+
toast.error("Could not copy — select the link and copy it manually");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Card>
|
|
92
|
+
<CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
|
|
93
|
+
<CardTitle className="text-base">Payment</CardTitle>
|
|
94
|
+
{paid ? (
|
|
95
|
+
<Badge variant="secondary" className="font-normal">Paid</Badge>
|
|
96
|
+
) : (
|
|
97
|
+
<Badge variant="outline" className="font-normal">Awaiting payment</Badge>
|
|
98
|
+
)}
|
|
99
|
+
</CardHeader>
|
|
100
|
+
<CardContent className="space-y-3">
|
|
101
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-sm">
|
|
102
|
+
<dt className="text-muted-foreground">Method</dt>
|
|
103
|
+
<dd>{order.payment_method_title || order.payment_method || "—"}</dd>
|
|
104
|
+
<dt className="text-muted-foreground">Total</dt>
|
|
105
|
+
<dd>{format(order.total || 0)}</dd>
|
|
106
|
+
{order.date_paid && (
|
|
107
|
+
<>
|
|
108
|
+
<dt className="text-muted-foreground">Paid</dt>
|
|
109
|
+
<dd>{formatDateTime(order.date_paid)}</dd>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
{order.transaction_id && (
|
|
113
|
+
<>
|
|
114
|
+
<dt className="text-muted-foreground">Reference</dt>
|
|
115
|
+
<dd className="truncate font-mono text-xs">{order.transaction_id}</dd>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
</dl>
|
|
119
|
+
|
|
120
|
+
{known && !isOnline && !paid && !provider.connected && (
|
|
121
|
+
<p className="text-xs text-muted-foreground">
|
|
122
|
+
{order.payment_method
|
|
123
|
+
? `This order is set to be paid outside the store (${order.payment_method_title || order.payment_method}), so mark it paid by moving it to Processing once the money arrives.`
|
|
124
|
+
: "No payment method is set on this order. Take payment outside the store and move it to Processing, or connect a payment provider to send the customer a payment link."}
|
|
125
|
+
</p>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{!paid && (provider.connected || isOnline) && (
|
|
129
|
+
<>
|
|
130
|
+
<div className="flex flex-wrap gap-2">
|
|
131
|
+
<Button
|
|
132
|
+
variant="outline"
|
|
133
|
+
size="sm"
|
|
134
|
+
disabled={!canAct || Boolean(working) || provider.loading}
|
|
135
|
+
title={disabledReason}
|
|
136
|
+
onClick={createLink}
|
|
137
|
+
>
|
|
138
|
+
{working === "link" ? (
|
|
139
|
+
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
140
|
+
) : (
|
|
141
|
+
<CreditCard className="mr-1.5 h-3.5 w-3.5" />
|
|
142
|
+
)}
|
|
143
|
+
{link ? "New payment link" : "Create payment link"}
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
variant="outline"
|
|
147
|
+
size="sm"
|
|
148
|
+
disabled={!canAct || Boolean(working) || provider.loading}
|
|
149
|
+
title={disabledReason}
|
|
150
|
+
onClick={checkPayment}
|
|
151
|
+
>
|
|
152
|
+
{working === "verify" ? (
|
|
153
|
+
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
154
|
+
) : (
|
|
155
|
+
<RotateCw className="mr-1.5 h-3.5 w-3.5" />
|
|
156
|
+
)}
|
|
157
|
+
Check payment
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{!provider.connected && !provider.loading && (
|
|
162
|
+
<p className="text-xs text-muted-foreground">
|
|
163
|
+
No payment provider is connected, so this order can't be paid online yet.
|
|
164
|
+
</p>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{willSwitchMethod && !link && (
|
|
168
|
+
<p className="text-xs text-muted-foreground">
|
|
169
|
+
{order.payment_method
|
|
170
|
+
? `Sending a payment link switches this order from ${order.payment_method_title || order.payment_method} to card payment.`
|
|
171
|
+
: "Sending a payment link sets this order to card payment."}
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{link && (
|
|
176
|
+
<div className="space-y-1.5">
|
|
177
|
+
<p className="text-xs text-muted-foreground">
|
|
178
|
+
Send this to the customer — it opens the provider's secure payment page. The order
|
|
179
|
+
updates itself once they pay.
|
|
180
|
+
</p>
|
|
181
|
+
<div className="flex gap-1.5">
|
|
182
|
+
<Input readOnly value={link} className="h-8 font-mono text-xs" onFocus={(e) => e.target.select()} />
|
|
183
|
+
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" onClick={copy} title="Copy link">
|
|
184
|
+
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
185
|
+
</Button>
|
|
186
|
+
<Button variant="outline" size="icon" className="h-8 w-8 shrink-0" asChild title="Open">
|
|
187
|
+
<a href={link} target="_blank" rel="noreferrer">
|
|
188
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
189
|
+
</a>
|
|
190
|
+
</Button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
</CardContent>
|
|
197
|
+
</Card>
|
|
198
|
+
);
|
|
199
|
+
}
|