medusa-plugin-ordinant 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/.medusa/server/src/admin/index.js +1636 -0
- package/.medusa/server/src/admin/index.mjs +1635 -0
- package/.medusa/server/src/api/admin/ordinant/classifications/[sku]/confirm/route.js +25 -0
- package/.medusa/server/src/api/admin/ordinant/classifications/bulk-confirm/route.js +29 -0
- package/.medusa/server/src/api/admin/ordinant/classifications/route.js +23 -0
- package/.medusa/server/src/api/admin/ordinant/classify-all/route.js +108 -0
- package/.medusa/server/src/api/admin/ordinant/dictionary/route.js +17 -0
- package/.medusa/server/src/api/admin/ordinant/generate-all-skus/route.js +31 -0
- package/.medusa/server/src/api/admin/ordinant/overview/route.js +69 -0
- package/.medusa/server/src/api/admin/ordinant/products/[id]/classify/route.js +18 -0
- package/.medusa/server/src/api/admin/ordinant/products/[id]/generate-skus/route.js +34 -0
- package/.medusa/server/src/api/admin/ordinant/products/[id]/overview/route.js +59 -0
- package/.medusa/server/src/api/admin/ordinant/stats/route.js +17 -0
- package/.medusa/server/src/api/admin/ordinant/status/route.js +52 -0
- package/.medusa/server/src/api/middlewares.js +14 -0
- package/.medusa/server/src/api/store/ordinant/preview/route.js +110 -0
- package/.medusa/server/src/api/store/ordinant/preview/validators.js +34 -0
- package/.medusa/server/src/lib/client.js +136 -0
- package/.medusa/server/src/modules/ordinant/index.js +14 -0
- package/.medusa/server/src/modules/ordinant/service.js +27 -0
- package/.medusa/server/src/storefront.d.ts +61 -0
- package/.medusa/server/src/storefront.js +61 -0
- package/.medusa/server/src/subscribers/product-upsert.js +52 -0
- package/.medusa/server/src/workflows/bulk-operations.js +59 -0
- package/.medusa/server/src/workflows/classify-product.js +80 -0
- package/.medusa/server/src/workflows/confirm-classification.js +20 -0
- package/.medusa/server/src/workflows/generate-skus.js +81 -0
- package/.medusa/server/src/workflows/hooks/validate-cart-completion.js +127 -0
- package/README.md +495 -0
- package/package.json +92 -0
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
|
+
const ui = require("@medusajs/ui");
|
|
5
|
+
const reactQuery = require("@tanstack/react-query");
|
|
6
|
+
const react = require("react");
|
|
7
|
+
const Medusa = require("@medusajs/js-sdk");
|
|
8
|
+
const icons = require("@medusajs/icons");
|
|
9
|
+
const reactRouterDom = require("react-router-dom");
|
|
10
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
|
+
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
12
|
+
const sdk = new Medusa__default.default({
|
|
13
|
+
baseUrl: typeof window !== "undefined" ? window.location.origin : "/",
|
|
14
|
+
debug: false,
|
|
15
|
+
auth: {
|
|
16
|
+
type: "session"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
function useDictionary() {
|
|
20
|
+
return reactQuery.useQuery({
|
|
21
|
+
queryKey: ["ordinant-dictionary"],
|
|
22
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/dictionary"),
|
|
23
|
+
staleTime: 60 * 60 * 1e3
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const CATEGORY_FIELDS = {
|
|
27
|
+
nfa_class: [
|
|
28
|
+
"handgun",
|
|
29
|
+
"long_gun",
|
|
30
|
+
"receiver_frame",
|
|
31
|
+
"short_barreled_rifle",
|
|
32
|
+
"short_barreled_shotgun",
|
|
33
|
+
"machine_gun",
|
|
34
|
+
"aow",
|
|
35
|
+
"destructive_device",
|
|
36
|
+
"suppressor",
|
|
37
|
+
"firearm_part"
|
|
38
|
+
],
|
|
39
|
+
mag_capacity: [
|
|
40
|
+
"magazine",
|
|
41
|
+
"handgun",
|
|
42
|
+
"long_gun",
|
|
43
|
+
"short_barreled_rifle",
|
|
44
|
+
"short_barreled_shotgun",
|
|
45
|
+
"machine_gun"
|
|
46
|
+
],
|
|
47
|
+
caliber: [
|
|
48
|
+
"handgun",
|
|
49
|
+
"long_gun",
|
|
50
|
+
"receiver_frame",
|
|
51
|
+
"ammunition",
|
|
52
|
+
"short_barreled_rifle",
|
|
53
|
+
"short_barreled_shotgun",
|
|
54
|
+
"machine_gun",
|
|
55
|
+
"aow",
|
|
56
|
+
"suppressor"
|
|
57
|
+
],
|
|
58
|
+
action_type: [
|
|
59
|
+
"handgun",
|
|
60
|
+
"long_gun",
|
|
61
|
+
"short_barreled_rifle",
|
|
62
|
+
"short_barreled_shotgun",
|
|
63
|
+
"machine_gun",
|
|
64
|
+
"aow"
|
|
65
|
+
],
|
|
66
|
+
feature_flags: [
|
|
67
|
+
"handgun",
|
|
68
|
+
"long_gun",
|
|
69
|
+
"receiver_frame",
|
|
70
|
+
"short_barreled_rifle",
|
|
71
|
+
"short_barreled_shotgun"
|
|
72
|
+
],
|
|
73
|
+
barrel_length_in: [
|
|
74
|
+
"handgun",
|
|
75
|
+
"long_gun",
|
|
76
|
+
"short_barreled_rifle",
|
|
77
|
+
"short_barreled_shotgun",
|
|
78
|
+
"machine_gun",
|
|
79
|
+
"aow"
|
|
80
|
+
],
|
|
81
|
+
oal_in: [
|
|
82
|
+
"handgun",
|
|
83
|
+
"long_gun",
|
|
84
|
+
"short_barreled_rifle",
|
|
85
|
+
"short_barreled_shotgun",
|
|
86
|
+
"machine_gun",
|
|
87
|
+
"aow"
|
|
88
|
+
],
|
|
89
|
+
ammo_type: ["ammunition"],
|
|
90
|
+
blade_type: ["knife"],
|
|
91
|
+
blade_length_in: ["knife"],
|
|
92
|
+
oc_volume_oz: ["oc_spray"],
|
|
93
|
+
armor_type: ["body_armor"],
|
|
94
|
+
semiauto_centerfire: [
|
|
95
|
+
"handgun",
|
|
96
|
+
"long_gun",
|
|
97
|
+
"receiver_frame",
|
|
98
|
+
"short_barreled_rifle"
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
const appliesTo = (def, category) => {
|
|
102
|
+
if (def.key === "category") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (def.applies_to && def.applies_to.length > 0) {
|
|
106
|
+
return category !== void 0 && def.applies_to.includes(category);
|
|
107
|
+
}
|
|
108
|
+
const allowed = CATEGORY_FIELDS[def.key];
|
|
109
|
+
if (allowed) {
|
|
110
|
+
return category !== void 0 && allowed.includes(category);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
const humanize = (key) => key.replace(/_/g, " ").replace(/\b(nfa|oc|oal|awb|sku|upc)\b/gi, (m) => m.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
115
|
+
const labelValue = (v) => v.replace(/_/g, " ").replace(/\b(nfa|oc|sbr|sbs|aow|dd)\b/gi, (m) => m.toUpperCase());
|
|
116
|
+
function ReadableAttributes({
|
|
117
|
+
attributes
|
|
118
|
+
}) {
|
|
119
|
+
if (!attributes || Object.keys(attributes).length === 0) {
|
|
120
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-muted", children: "—" });
|
|
121
|
+
}
|
|
122
|
+
const entries = Object.entries(attributes);
|
|
123
|
+
entries.sort(([a], [b]) => a === "category" ? -1 : b === "category" ? 1 : a.localeCompare(b));
|
|
124
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: entries.map(([key, value]) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { size: "2xsmall", className: "font-normal", children: [
|
|
125
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-ui-fg-muted", children: [
|
|
126
|
+
humanize(key),
|
|
127
|
+
":"
|
|
128
|
+
] }),
|
|
129
|
+
" ",
|
|
130
|
+
formatValue(value)
|
|
131
|
+
] }, key)) });
|
|
132
|
+
}
|
|
133
|
+
function formatValue(value) {
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
return value.map((v) => labelValue(String(v))).join(", ") || "none";
|
|
136
|
+
}
|
|
137
|
+
if (typeof value === "boolean") {
|
|
138
|
+
return value ? "yes" : "no";
|
|
139
|
+
}
|
|
140
|
+
return labelValue(String(value));
|
|
141
|
+
}
|
|
142
|
+
function AttributeEditor({
|
|
143
|
+
value,
|
|
144
|
+
onChange
|
|
145
|
+
}) {
|
|
146
|
+
const { data: dictionary, isLoading, error } = useDictionary();
|
|
147
|
+
if (isLoading) {
|
|
148
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading attribute options…" });
|
|
149
|
+
}
|
|
150
|
+
if (error || !dictionary) {
|
|
151
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", children: [
|
|
152
|
+
"Could not load attribute options: ",
|
|
153
|
+
(error == null ? void 0 : error.message) ?? "unknown"
|
|
154
|
+
] });
|
|
155
|
+
}
|
|
156
|
+
const category = typeof value["category"] === "string" ? value["category"] : void 0;
|
|
157
|
+
const set = (key, v) => {
|
|
158
|
+
const next = { ...value };
|
|
159
|
+
if (v === void 0 || v === "" || Array.isArray(v) && v.length === 0) {
|
|
160
|
+
delete next[key];
|
|
161
|
+
} else {
|
|
162
|
+
next[key] = v;
|
|
163
|
+
}
|
|
164
|
+
if (key === "category") {
|
|
165
|
+
const nextCategory = typeof v === "string" ? v : void 0;
|
|
166
|
+
for (const k of Object.keys(next)) {
|
|
167
|
+
const def = dictionary.attributes.find((d) => d.key === k);
|
|
168
|
+
if (def && !appliesTo(def, nextCategory)) {
|
|
169
|
+
delete next[k];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
onChange(next);
|
|
174
|
+
};
|
|
175
|
+
const attrs = dictionary.attributes.filter((def) => appliesTo(def, category)).sort(
|
|
176
|
+
(a, b) => a.hard_gate === b.hard_gate ? a.key.localeCompare(b.key) : a.hard_gate ? -1 : 1
|
|
177
|
+
);
|
|
178
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
179
|
+
category === void 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: "Choose a category to see the fields that apply to it." }),
|
|
180
|
+
attrs.map((def) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
181
|
+
AttributeField,
|
|
182
|
+
{
|
|
183
|
+
def,
|
|
184
|
+
value: value[def.key],
|
|
185
|
+
onChange: (v) => set(def.key, v)
|
|
186
|
+
},
|
|
187
|
+
def.key
|
|
188
|
+
))
|
|
189
|
+
] });
|
|
190
|
+
}
|
|
191
|
+
const NONE = "__none__";
|
|
192
|
+
function AttributeField({
|
|
193
|
+
def,
|
|
194
|
+
value,
|
|
195
|
+
onChange
|
|
196
|
+
}) {
|
|
197
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
198
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
199
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: "small", weight: "plus", children: humanize(def.key) }),
|
|
200
|
+
def.hard_gate && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", color: "orange", children: "required" })
|
|
201
|
+
] }),
|
|
202
|
+
def.description && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted", children: def.description }),
|
|
203
|
+
/* @__PURE__ */ jsxRuntime.jsx(Control, { def, value, onChange })
|
|
204
|
+
] });
|
|
205
|
+
}
|
|
206
|
+
function Control({
|
|
207
|
+
def,
|
|
208
|
+
value,
|
|
209
|
+
onChange
|
|
210
|
+
}) {
|
|
211
|
+
switch (def.type) {
|
|
212
|
+
case "enum":
|
|
213
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
214
|
+
ui.Select,
|
|
215
|
+
{
|
|
216
|
+
size: "small",
|
|
217
|
+
value: typeof value === "string" ? value : NONE,
|
|
218
|
+
onValueChange: (v) => onChange(v === NONE ? void 0 : v),
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Not set" }) }),
|
|
221
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Select.Content, { children: [
|
|
222
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: NONE, children: "Not set / unknown" }),
|
|
223
|
+
(def.enum_values ?? []).map((ev) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: ev, children: labelValue(ev) }, ev))
|
|
224
|
+
] })
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
case "flags": {
|
|
229
|
+
const selected = new Set(Array.isArray(value) ? value : []);
|
|
230
|
+
const toggle = (ev, on) => {
|
|
231
|
+
const next = new Set(selected);
|
|
232
|
+
if (on) {
|
|
233
|
+
next.add(ev);
|
|
234
|
+
} else {
|
|
235
|
+
next.delete(ev);
|
|
236
|
+
}
|
|
237
|
+
onChange([...next]);
|
|
238
|
+
};
|
|
239
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-2", children: (def.enum_values ?? []).map((ev) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2", children: [
|
|
240
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
241
|
+
ui.Checkbox,
|
|
242
|
+
{
|
|
243
|
+
checked: selected.has(ev),
|
|
244
|
+
onCheckedChange: (c) => toggle(ev, c === true)
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", children: labelValue(ev) })
|
|
248
|
+
] }, ev)) });
|
|
249
|
+
}
|
|
250
|
+
case "bool":
|
|
251
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
252
|
+
ui.Switch,
|
|
253
|
+
{
|
|
254
|
+
checked: value === true,
|
|
255
|
+
onCheckedChange: (c) => onChange(c ? true : void 0)
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
case "int":
|
|
259
|
+
case "float":
|
|
260
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
261
|
+
ui.Input,
|
|
262
|
+
{
|
|
263
|
+
size: "small",
|
|
264
|
+
type: "number",
|
|
265
|
+
step: def.type === "int" ? 1 : "any",
|
|
266
|
+
value: value === void 0 || value === null ? "" : String(value),
|
|
267
|
+
onChange: (e) => {
|
|
268
|
+
const raw = e.target.value;
|
|
269
|
+
if (raw === "") {
|
|
270
|
+
onChange(void 0);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const n = def.type === "int" ? parseInt(raw, 10) : parseFloat(raw);
|
|
274
|
+
onChange(Number.isNaN(n) ? void 0 : n);
|
|
275
|
+
},
|
|
276
|
+
className: "w-32"
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
default:
|
|
280
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
281
|
+
ui.Input,
|
|
282
|
+
{
|
|
283
|
+
size: "small",
|
|
284
|
+
value: typeof value === "string" ? value : "",
|
|
285
|
+
onChange: (e) => onChange(e.target.value || void 0)
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const BRAND_BLUE = "#2F6BE0";
|
|
291
|
+
const BRAND_BLUE_SOFT = "rgba(47, 107, 224, 0.10)";
|
|
292
|
+
function StatusBadge({
|
|
293
|
+
status,
|
|
294
|
+
confidence
|
|
295
|
+
}) {
|
|
296
|
+
const base = "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium leading-none";
|
|
297
|
+
switch (status) {
|
|
298
|
+
case "confirmed":
|
|
299
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: base, style: { background: BRAND_BLUE, color: "#fff" }, children: "confirmed" });
|
|
300
|
+
case "proposed":
|
|
301
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: base, style: { background: BRAND_BLUE_SOFT, color: BRAND_BLUE }, children: [
|
|
302
|
+
"to review",
|
|
303
|
+
confidence != null ? ` · ${Math.round(confidence * 100)}%` : ""
|
|
304
|
+
] });
|
|
305
|
+
case "needs_sku":
|
|
306
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${base} bg-ui-bg-subtle text-ui-fg-subtle border border-ui-border-base`, children: "needs SKU" });
|
|
307
|
+
default:
|
|
308
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${base} bg-ui-bg-subtle text-ui-fg-muted`, children: "unclassified" });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function FilterTile({
|
|
312
|
+
label,
|
|
313
|
+
value,
|
|
314
|
+
active,
|
|
315
|
+
onClick
|
|
316
|
+
}) {
|
|
317
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
318
|
+
"button",
|
|
319
|
+
{
|
|
320
|
+
type: "button",
|
|
321
|
+
onClick,
|
|
322
|
+
className: "px-6 py-4 text-left transition-colors hover:bg-ui-bg-subtle-hover focus:outline-none",
|
|
323
|
+
style: {
|
|
324
|
+
borderBottom: active ? `2px solid ${BRAND_BLUE}` : "2px solid transparent",
|
|
325
|
+
background: active ? BRAND_BLUE_SOFT : void 0
|
|
326
|
+
},
|
|
327
|
+
"aria-pressed": active,
|
|
328
|
+
children: [
|
|
329
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: label }),
|
|
330
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
331
|
+
"div",
|
|
332
|
+
{
|
|
333
|
+
className: "mt-1 text-2xl font-semibold",
|
|
334
|
+
style: active ? { color: BRAND_BLUE } : void 0,
|
|
335
|
+
children: value ?? "—"
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const STRIPE_KEYFRAMES = "@keyframes ordinant-stripes { from { background-position: 0 0 } to { background-position: 34px 0 } }";
|
|
343
|
+
function ProgressModal({
|
|
344
|
+
open,
|
|
345
|
+
title,
|
|
346
|
+
subtitle,
|
|
347
|
+
done,
|
|
348
|
+
total
|
|
349
|
+
}) {
|
|
350
|
+
if (!open) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const indeterminate = total <= 0;
|
|
354
|
+
const pct = indeterminate ? 100 : Math.min(100, Math.round(done / total * 100));
|
|
355
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
356
|
+
"div",
|
|
357
|
+
{
|
|
358
|
+
className: "fixed inset-0 z-[9999] flex items-center justify-center",
|
|
359
|
+
style: { background: "rgba(0,0,0,0.45)" },
|
|
360
|
+
children: [
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx("style", { children: STRIPE_KEYFRAMES }),
|
|
362
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-[440px] max-w-[90vw] rounded-xl bg-ui-bg-base p-6 shadow-elevation-modal", children: [
|
|
363
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", children: title }),
|
|
364
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-1", children: subtitle }),
|
|
365
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-5 h-3 w-full overflow-hidden rounded-full bg-ui-bg-subtle", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
366
|
+
"div",
|
|
367
|
+
{
|
|
368
|
+
style: {
|
|
369
|
+
height: "100%",
|
|
370
|
+
width: `${pct}%`,
|
|
371
|
+
transition: indeterminate ? void 0 : "width 0.35s ease",
|
|
372
|
+
backgroundImage: `repeating-linear-gradient(45deg, ${BRAND_BLUE} 0 12px, rgba(255,255,255,0.28) 12px 24px)`,
|
|
373
|
+
backgroundSize: "34px 34px",
|
|
374
|
+
animation: "ordinant-stripes 0.7s linear infinite",
|
|
375
|
+
borderRadius: 9999
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
) }),
|
|
379
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center justify-between", children: [
|
|
380
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: indeterminate ? "Working…" : `${done} of ${total}` }),
|
|
381
|
+
!indeterminate && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", leading: "compact", weight: "plus", style: { color: BRAND_BLUE }, children: [
|
|
382
|
+
pct,
|
|
383
|
+
"%"
|
|
384
|
+
] })
|
|
385
|
+
] })
|
|
386
|
+
] })
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function BrandButton({
|
|
392
|
+
children,
|
|
393
|
+
onClick,
|
|
394
|
+
loading,
|
|
395
|
+
disabled
|
|
396
|
+
}) {
|
|
397
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
398
|
+
"button",
|
|
399
|
+
{
|
|
400
|
+
type: "button",
|
|
401
|
+
onClick,
|
|
402
|
+
disabled: disabled || loading,
|
|
403
|
+
className: "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-white transition-opacity disabled:opacity-50",
|
|
404
|
+
style: { background: BRAND_BLUE },
|
|
405
|
+
children: [
|
|
406
|
+
loading && /* @__PURE__ */ jsxRuntime.jsx(
|
|
407
|
+
"span",
|
|
408
|
+
{
|
|
409
|
+
className: "inline-block h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent",
|
|
410
|
+
"aria-hidden": true
|
|
411
|
+
}
|
|
412
|
+
),
|
|
413
|
+
children
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const OrdinantProductWidget = ({
|
|
419
|
+
data: product
|
|
420
|
+
}) => {
|
|
421
|
+
const queryClient = reactQuery.useQueryClient();
|
|
422
|
+
const displayKey = ["ordinant-product-overview", product.id];
|
|
423
|
+
const { data, isLoading, error } = reactQuery.useQuery({
|
|
424
|
+
queryKey: displayKey,
|
|
425
|
+
queryFn: () => sdk.client.fetch(
|
|
426
|
+
`/admin/ordinant/products/${product.id}/overview`
|
|
427
|
+
)
|
|
428
|
+
});
|
|
429
|
+
const rows = (data == null ? void 0 : data.rows) ?? [];
|
|
430
|
+
const needsSku = (data == null ? void 0 : data.needs_sku) ?? false;
|
|
431
|
+
const invalidate = () => queryClient.invalidateQueries({ queryKey: displayKey });
|
|
432
|
+
const classify = reactQuery.useMutation({
|
|
433
|
+
mutationFn: () => sdk.client.fetch(`/admin/ordinant/products/${product.id}/classify`, {
|
|
434
|
+
method: "POST"
|
|
435
|
+
}),
|
|
436
|
+
onSuccess: () => {
|
|
437
|
+
invalidate();
|
|
438
|
+
ui.toast.success("Classification proposed — review and confirm below.");
|
|
439
|
+
},
|
|
440
|
+
onError: (err) => ui.toast.error(`Classification failed: ${err.message}`)
|
|
441
|
+
});
|
|
442
|
+
const generateSkus = reactQuery.useMutation({
|
|
443
|
+
mutationFn: () => sdk.client.fetch(`/admin/ordinant/products/${product.id}/generate-skus`, {
|
|
444
|
+
method: "POST"
|
|
445
|
+
}),
|
|
446
|
+
onSuccess: () => {
|
|
447
|
+
invalidate();
|
|
448
|
+
ui.toast.success("SKUs generated and classified — review below.");
|
|
449
|
+
},
|
|
450
|
+
onError: (err) => ui.toast.error(`Could not generate SKUs: ${err.message}`)
|
|
451
|
+
});
|
|
452
|
+
const classifiable = rows.some((r) => r.status !== "needs_sku");
|
|
453
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
455
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
456
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: "Ordinant compliance" }),
|
|
457
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Confirmed classifications are attested by you and used to gate checkout. Unconfirmed variants cannot sell." })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
460
|
+
needsSku && /* @__PURE__ */ jsxRuntime.jsx(
|
|
461
|
+
ui.Button,
|
|
462
|
+
{
|
|
463
|
+
size: "small",
|
|
464
|
+
variant: "secondary",
|
|
465
|
+
onClick: () => generateSkus.mutate(),
|
|
466
|
+
isLoading: generateSkus.isPending,
|
|
467
|
+
disabled: generateSkus.isPending,
|
|
468
|
+
children: "Generate SKUs"
|
|
469
|
+
}
|
|
470
|
+
),
|
|
471
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
472
|
+
ui.Button,
|
|
473
|
+
{
|
|
474
|
+
size: "small",
|
|
475
|
+
variant: "secondary",
|
|
476
|
+
onClick: () => classify.mutate(),
|
|
477
|
+
isLoading: classify.isPending,
|
|
478
|
+
disabled: classify.isPending || !classifiable,
|
|
479
|
+
children: "Classify"
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
] })
|
|
483
|
+
] }),
|
|
484
|
+
isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Loading…" }) }),
|
|
485
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", children: [
|
|
486
|
+
"Could not reach Ordinant: ",
|
|
487
|
+
error.message
|
|
488
|
+
] }) }),
|
|
489
|
+
!isLoading && !error && rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(VariantRow, { row, displayKey }, row.variant_id))
|
|
490
|
+
] });
|
|
491
|
+
};
|
|
492
|
+
const VariantRow = ({
|
|
493
|
+
row,
|
|
494
|
+
displayKey
|
|
495
|
+
}) => {
|
|
496
|
+
const queryClient = reactQuery.useQueryClient();
|
|
497
|
+
const [editing, setEditing] = react.useState(false);
|
|
498
|
+
const [draft, setDraft] = react.useState({});
|
|
499
|
+
const confirm = reactQuery.useMutation({
|
|
500
|
+
mutationFn: (attributes) => sdk.client.fetch(
|
|
501
|
+
`/admin/ordinant/classifications/${encodeURIComponent(row.sku)}/confirm`,
|
|
502
|
+
{ method: "POST", body: { attributes } }
|
|
503
|
+
),
|
|
504
|
+
onSuccess: () => {
|
|
505
|
+
queryClient.invalidateQueries({ queryKey: displayKey });
|
|
506
|
+
setEditing(false);
|
|
507
|
+
ui.toast.success(`${row.sku} confirmed — it can sell now.`);
|
|
508
|
+
},
|
|
509
|
+
onError: (err) => ui.toast.error(`Confirmation failed: ${err.message}`)
|
|
510
|
+
});
|
|
511
|
+
const startEditing = () => {
|
|
512
|
+
setDraft(row.attributes ?? {});
|
|
513
|
+
setEditing(true);
|
|
514
|
+
};
|
|
515
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
516
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
517
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
518
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", children: row.status === "needs_sku" ? row.variant_title ?? "variant" : row.sku }),
|
|
519
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: row.status, confidence: row.confidence })
|
|
520
|
+
] }),
|
|
521
|
+
row.status !== "needs_sku" && !editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
522
|
+
row.status === "proposed" && row.attributes && /* @__PURE__ */ jsxRuntime.jsx(
|
|
523
|
+
ui.Button,
|
|
524
|
+
{
|
|
525
|
+
size: "small",
|
|
526
|
+
onClick: () => confirm.mutate(row.attributes),
|
|
527
|
+
isLoading: confirm.isPending,
|
|
528
|
+
disabled: confirm.isPending,
|
|
529
|
+
style: { background: BRAND_BLUE },
|
|
530
|
+
children: "Confirm"
|
|
531
|
+
}
|
|
532
|
+
),
|
|
533
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: startEditing, children: "Edit" })
|
|
534
|
+
] })
|
|
535
|
+
] }),
|
|
536
|
+
row.status === "needs_sku" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-1", children: "No SKU — use “Generate SKUs” above, or add one under Variants, so this variant can be classified and sold." }),
|
|
537
|
+
row.status !== "needs_sku" && !editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2", children: [
|
|
538
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReadableAttributes, { attributes: row.attributes }),
|
|
539
|
+
row.notes && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-1", children: row.notes })
|
|
540
|
+
] }),
|
|
541
|
+
editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 flex flex-col gap-3", children: [
|
|
542
|
+
/* @__PURE__ */ jsxRuntime.jsx(AttributeEditor, { value: draft, onChange: setDraft }),
|
|
543
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
544
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
545
|
+
ui.Button,
|
|
546
|
+
{
|
|
547
|
+
size: "small",
|
|
548
|
+
onClick: () => confirm.mutate(draft),
|
|
549
|
+
isLoading: confirm.isPending,
|
|
550
|
+
disabled: confirm.isPending,
|
|
551
|
+
style: { background: BRAND_BLUE },
|
|
552
|
+
children: "Confirm classification"
|
|
553
|
+
}
|
|
554
|
+
),
|
|
555
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
556
|
+
ui.Button,
|
|
557
|
+
{
|
|
558
|
+
size: "small",
|
|
559
|
+
variant: "secondary",
|
|
560
|
+
onClick: () => setEditing(false),
|
|
561
|
+
disabled: confirm.isPending,
|
|
562
|
+
children: "Cancel"
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
] })
|
|
566
|
+
] })
|
|
567
|
+
] });
|
|
568
|
+
};
|
|
569
|
+
adminSdk.defineWidgetConfig({
|
|
570
|
+
zone: "product.details.after"
|
|
571
|
+
});
|
|
572
|
+
const BRAND = {
|
|
573
|
+
name: "Ordinant",
|
|
574
|
+
website: "https://ordinant.com"
|
|
575
|
+
};
|
|
576
|
+
const BrandIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(icons.ShieldCheck, { ...props });
|
|
577
|
+
function BrandMark({ size = 56 }) {
|
|
578
|
+
const inner = Math.round(size * 0.5);
|
|
579
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
580
|
+
"div",
|
|
581
|
+
{
|
|
582
|
+
className: "flex items-center justify-center rounded-full",
|
|
583
|
+
style: { width: size, height: size, background: BRAND_BLUE_SOFT },
|
|
584
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.ShieldCheck, { style: { color: BRAND_BLUE, width: inner, height: inner } })
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
function BrandWordmark({ className }) {
|
|
589
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className, style: { color: BRAND_BLUE }, children: BRAND.name });
|
|
590
|
+
}
|
|
591
|
+
const formatCategory = (attributes) => {
|
|
592
|
+
const c = attributes == null ? void 0 : attributes["category"];
|
|
593
|
+
return typeof c === "string" ? c.replace(/_/g, " ") : "—";
|
|
594
|
+
};
|
|
595
|
+
function CatalogTable({
|
|
596
|
+
rows,
|
|
597
|
+
onChange
|
|
598
|
+
}) {
|
|
599
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { className: "w-full table-fixed", style: { tableLayout: "fixed", width: "100%" }, children: [
|
|
600
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
601
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[38%]", children: "Product" }),
|
|
602
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[24%]", children: "SKU" }),
|
|
603
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[16%]", children: "Category" }),
|
|
604
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[14%]", children: "Status" }),
|
|
605
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[8%] text-right", children: "Edit" })
|
|
606
|
+
] }) }),
|
|
607
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: rows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
|
|
608
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Cell, { className: "truncate", children: [
|
|
609
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", className: "truncate", children: row.product_title }),
|
|
610
|
+
row.variant_title && row.variant_title !== "Default variant" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle truncate", children: row.variant_title })
|
|
611
|
+
] }),
|
|
612
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "truncate", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate font-mono text-xs text-ui-fg-subtle", children: row.sku || "—" }) }),
|
|
613
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "truncate", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "truncate capitalize", children: formatCategory(row.attributes) }) }),
|
|
614
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: row.status, confidence: row.confidence }) }),
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { className: "text-right", children: (row.status === "proposed" || row.status === "confirmed") && /* @__PURE__ */ jsxRuntime.jsx(EditDrawer, { row, onChange }) })
|
|
616
|
+
] }, row.variant_id)) })
|
|
617
|
+
] }) });
|
|
618
|
+
}
|
|
619
|
+
function EditDrawer({
|
|
620
|
+
row,
|
|
621
|
+
onChange
|
|
622
|
+
}) {
|
|
623
|
+
const [open, setOpen] = react.useState(false);
|
|
624
|
+
const [draft, setDraft] = react.useState(row.attributes ?? {});
|
|
625
|
+
const confirm = reactQuery.useMutation({
|
|
626
|
+
mutationFn: (attributes) => sdk.client.fetch(
|
|
627
|
+
`/admin/ordinant/classifications/${encodeURIComponent(row.sku)}/confirm`,
|
|
628
|
+
{ method: "POST", body: { attributes } }
|
|
629
|
+
),
|
|
630
|
+
onSuccess: () => {
|
|
631
|
+
onChange();
|
|
632
|
+
setOpen(false);
|
|
633
|
+
ui.toast.success(`${row.sku} confirmed.`);
|
|
634
|
+
},
|
|
635
|
+
onError: (err) => ui.toast.error(`Confirmation failed: ${err.message}`)
|
|
636
|
+
});
|
|
637
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
638
|
+
ui.Drawer,
|
|
639
|
+
{
|
|
640
|
+
open,
|
|
641
|
+
onOpenChange: (o) => {
|
|
642
|
+
if (o) {
|
|
643
|
+
setDraft(row.attributes ?? {});
|
|
644
|
+
}
|
|
645
|
+
setOpen(o);
|
|
646
|
+
},
|
|
647
|
+
children: [
|
|
648
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", children: "Edit" }) }),
|
|
649
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
650
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: row.product_title }) }),
|
|
651
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Body, { className: "flex flex-col gap-3 overflow-y-auto", children: [
|
|
652
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
653
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-xs text-ui-fg-subtle", children: row.sku }),
|
|
654
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: row.status, confidence: row.confidence })
|
|
655
|
+
] }),
|
|
656
|
+
row.notes && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: row.notes }),
|
|
657
|
+
/* @__PURE__ */ jsxRuntime.jsx(AttributeEditor, { value: draft, onChange: setDraft })
|
|
658
|
+
] }),
|
|
659
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
660
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: confirm.isPending, children: "Cancel" }) }),
|
|
661
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
662
|
+
ui.Button,
|
|
663
|
+
{
|
|
664
|
+
size: "small",
|
|
665
|
+
isLoading: confirm.isPending,
|
|
666
|
+
onClick: () => confirm.mutate(draft),
|
|
667
|
+
style: { background: BRAND_BLUE },
|
|
668
|
+
children: row.status === "confirmed" ? "Save attestation" : "Confirm classification"
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
] })
|
|
672
|
+
] })
|
|
673
|
+
]
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function prefersReducedMotion() {
|
|
678
|
+
var _a;
|
|
679
|
+
return typeof window !== "undefined" && ((_a = window.matchMedia) == null ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)").matches) === true;
|
|
680
|
+
}
|
|
681
|
+
function CountUp({
|
|
682
|
+
value,
|
|
683
|
+
className,
|
|
684
|
+
style
|
|
685
|
+
}) {
|
|
686
|
+
const [display, setDisplay] = react.useState(value);
|
|
687
|
+
const prev = react.useRef(value);
|
|
688
|
+
react.useEffect(() => {
|
|
689
|
+
const from = prev.current;
|
|
690
|
+
const to = value;
|
|
691
|
+
prev.current = value;
|
|
692
|
+
if (from === to || prefersReducedMotion()) {
|
|
693
|
+
setDisplay(to);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const duration = 700;
|
|
697
|
+
const start = performance.now();
|
|
698
|
+
let raf = requestAnimationFrame(function tick(t) {
|
|
699
|
+
const p = Math.min(1, (t - start) / duration);
|
|
700
|
+
const eased = 1 - Math.pow(1 - p, 3);
|
|
701
|
+
setDisplay(Math.round(from + (to - from) * eased));
|
|
702
|
+
if (p < 1) {
|
|
703
|
+
raf = requestAnimationFrame(tick);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return () => cancelAnimationFrame(raf);
|
|
707
|
+
}, [value]);
|
|
708
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, style: { fontVariantNumeric: "tabular-nums", ...style }, children: display.toLocaleString() });
|
|
709
|
+
}
|
|
710
|
+
function Segmented({
|
|
711
|
+
options,
|
|
712
|
+
value,
|
|
713
|
+
onChange
|
|
714
|
+
}) {
|
|
715
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "inline-flex rounded-lg bg-ui-bg-base p-0.5 border border-ui-border-base", role: "tablist", children: options.map((o) => {
|
|
716
|
+
const active = o.value === value;
|
|
717
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
718
|
+
"button",
|
|
719
|
+
{
|
|
720
|
+
type: "button",
|
|
721
|
+
role: "tab",
|
|
722
|
+
"aria-selected": active,
|
|
723
|
+
onClick: () => onChange(o.value),
|
|
724
|
+
className: `rounded-md px-3 py-1 text-xs font-medium transition-colors ${active ? "" : "text-ui-fg-subtle hover:text-ui-fg-base"}`,
|
|
725
|
+
style: active ? { background: BRAND_BLUE, color: "#fff" } : void 0,
|
|
726
|
+
children: o.label
|
|
727
|
+
},
|
|
728
|
+
o.value
|
|
729
|
+
);
|
|
730
|
+
}) });
|
|
731
|
+
}
|
|
732
|
+
function BlocksChart({
|
|
733
|
+
data,
|
|
734
|
+
formatBucket,
|
|
735
|
+
ariaUnit
|
|
736
|
+
}) {
|
|
737
|
+
const reduce = prefersReducedMotion();
|
|
738
|
+
const max = Math.max(1, ...data.map((d) => d.count));
|
|
739
|
+
const labelIdx = [0, Math.floor((data.length - 1) / 2), data.length - 1];
|
|
740
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
741
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
742
|
+
"div",
|
|
743
|
+
{
|
|
744
|
+
className: "flex h-28 items-end gap-[3px]",
|
|
745
|
+
role: "img",
|
|
746
|
+
"aria-label": `Blocked orders per ${ariaUnit}. ${data.filter((d) => d.count > 0).map((d) => `${formatBucket(d.bucket)}: ${d.count}`).join(", ") || "none in range"}`,
|
|
747
|
+
children: data.map((d) => {
|
|
748
|
+
const h = d.count === 0 ? 3 : Math.max(6, Math.round(d.count / max * 100));
|
|
749
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
750
|
+
"div",
|
|
751
|
+
{
|
|
752
|
+
className: "group relative flex flex-1 flex-col justify-end",
|
|
753
|
+
title: `${formatBucket(d.bucket)} — ${d.count} stopped`,
|
|
754
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
755
|
+
"div",
|
|
756
|
+
{
|
|
757
|
+
style: {
|
|
758
|
+
height: `${h}%`,
|
|
759
|
+
minHeight: 3,
|
|
760
|
+
borderRadius: 3,
|
|
761
|
+
background: d.count > 0 ? BRAND_BLUE : "var(--border-base, #e5e7eb)",
|
|
762
|
+
opacity: d.count > 0 ? 1 : 0.5,
|
|
763
|
+
transition: reduce ? void 0 : "height 0.4s cubic-bezier(0.22,1,0.36,1)"
|
|
764
|
+
},
|
|
765
|
+
className: "group-hover:opacity-80"
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
},
|
|
769
|
+
d.bucket
|
|
770
|
+
);
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
),
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1.5 flex justify-between", children: labelIdx.map((i, k) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
775
|
+
ui.Text,
|
|
776
|
+
{
|
|
777
|
+
size: "xsmall",
|
|
778
|
+
leading: "compact",
|
|
779
|
+
className: "text-ui-fg-muted",
|
|
780
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
781
|
+
children: data[i] ? formatBucket(data[i].bucket) : ""
|
|
782
|
+
},
|
|
783
|
+
k
|
|
784
|
+
)) })
|
|
785
|
+
] });
|
|
786
|
+
}
|
|
787
|
+
function StatCard({
|
|
788
|
+
label,
|
|
789
|
+
value,
|
|
790
|
+
sub
|
|
791
|
+
}) {
|
|
792
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base bg-ui-bg-subtle px-4 py-3", children: [
|
|
793
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted uppercase tracking-wide", children: label }),
|
|
794
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
795
|
+
"div",
|
|
796
|
+
{
|
|
797
|
+
className: "mt-1 text-lg font-semibold text-ui-fg-base",
|
|
798
|
+
style: { fontVariantNumeric: "tabular-nums" },
|
|
799
|
+
children: value
|
|
800
|
+
}
|
|
801
|
+
),
|
|
802
|
+
sub && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: sub })
|
|
803
|
+
] });
|
|
804
|
+
}
|
|
805
|
+
const RANGE_META = {
|
|
806
|
+
day: { headline: "stopped today", caption: "Last 30 days", unit: "day" },
|
|
807
|
+
month: { headline: "stopped this month", caption: "Last 12 months", unit: "month" },
|
|
808
|
+
year: { headline: "stopped this year", caption: "By year", unit: "year" },
|
|
809
|
+
all: { headline: "stopped all-time", caption: "By year", unit: "year" }
|
|
810
|
+
};
|
|
811
|
+
function bucketFormatter(range) {
|
|
812
|
+
if (range === "day") {
|
|
813
|
+
return (b) => {
|
|
814
|
+
const [, m, d] = b.split("-");
|
|
815
|
+
return `${MONTHS[Number(m) - 1] ?? m} ${Number(d)}`;
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (range === "month" || range === "all") {
|
|
819
|
+
return (b) => {
|
|
820
|
+
if (b.length === 4) return b;
|
|
821
|
+
const [y, m] = b.split("-");
|
|
822
|
+
return `${MONTHS[Number(m) - 1] ?? m} '${y.slice(2)}`;
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return (b) => b;
|
|
826
|
+
}
|
|
827
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
828
|
+
function ImpactHero({ stats }) {
|
|
829
|
+
var _a;
|
|
830
|
+
const [range, setRange] = react.useState("month");
|
|
831
|
+
const value = range === "day" ? stats.today : range === "month" ? stats.month : range === "year" ? stats.year : stats.total_blocked;
|
|
832
|
+
const series = range === "day" ? stats.series_day : range === "month" ? stats.series_month : stats.series_year;
|
|
833
|
+
const meta = RANGE_META[range];
|
|
834
|
+
const top = (_a = stats.top_jurisdictions) == null ? void 0 : _a[0];
|
|
835
|
+
if (stats.total_blocked === 0) {
|
|
836
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-5", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
837
|
+
"div",
|
|
838
|
+
{
|
|
839
|
+
className: "flex items-center gap-4 rounded-2xl border border-ui-border-base p-5",
|
|
840
|
+
style: { background: BRAND_BLUE_SOFT },
|
|
841
|
+
children: [
|
|
842
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
843
|
+
"div",
|
|
844
|
+
{
|
|
845
|
+
className: "flex h-12 w-12 shrink-0 items-center justify-center rounded-full",
|
|
846
|
+
style: { background: "#fff" },
|
|
847
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.ShieldCheck, { style: { color: BRAND_BLUE } })
|
|
848
|
+
}
|
|
849
|
+
),
|
|
850
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
851
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-base", children: "You’re protected — no illegal orders yet" }),
|
|
852
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle mt-0.5", children: "Ordinant checks every checkout against current law. The moment an order can’t legally ship to its destination, it’s stopped and counted here." })
|
|
853
|
+
] })
|
|
854
|
+
]
|
|
855
|
+
}
|
|
856
|
+
) });
|
|
857
|
+
}
|
|
858
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-5", children: [
|
|
859
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl border border-ui-border-base p-5", style: { background: BRAND_BLUE_SOFT }, children: [
|
|
860
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
861
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-base", children: "Illegal orders stopped" }),
|
|
862
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
863
|
+
Segmented,
|
|
864
|
+
{
|
|
865
|
+
value: range,
|
|
866
|
+
onChange: setRange,
|
|
867
|
+
options: [
|
|
868
|
+
{ value: "day", label: "Day" },
|
|
869
|
+
{ value: "month", label: "Month" },
|
|
870
|
+
{ value: "year", label: "Year" },
|
|
871
|
+
{ value: "all", label: "All" }
|
|
872
|
+
]
|
|
873
|
+
}
|
|
874
|
+
)
|
|
875
|
+
] }),
|
|
876
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between", children: [
|
|
877
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0", children: [
|
|
878
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
879
|
+
CountUp,
|
|
880
|
+
{
|
|
881
|
+
value,
|
|
882
|
+
className: "text-5xl font-bold leading-none",
|
|
883
|
+
style: { color: BRAND_BLUE }
|
|
884
|
+
}
|
|
885
|
+
),
|
|
886
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-2", children: meta.headline })
|
|
887
|
+
] }),
|
|
888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 sm:max-w-[60%]", children: [
|
|
889
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
890
|
+
BlocksChart,
|
|
891
|
+
{
|
|
892
|
+
data: series,
|
|
893
|
+
ariaUnit: meta.unit,
|
|
894
|
+
formatBucket: bucketFormatter(range)
|
|
895
|
+
}
|
|
896
|
+
),
|
|
897
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted mt-1 text-right", children: meta.caption })
|
|
898
|
+
] })
|
|
899
|
+
] })
|
|
900
|
+
] }),
|
|
901
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
|
|
902
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "All-time stopped", value: stats.total_blocked.toLocaleString() }),
|
|
903
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
904
|
+
StatCard,
|
|
905
|
+
{
|
|
906
|
+
label: "Top blocked destination",
|
|
907
|
+
value: top ? top.state : "—",
|
|
908
|
+
sub: top ? `${top.count} order${top.count === 1 ? "" : "s"} stopped` : "none yet"
|
|
909
|
+
}
|
|
910
|
+
),
|
|
911
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "Stopped this year", value: stats.year.toLocaleString() })
|
|
912
|
+
] })
|
|
913
|
+
] });
|
|
914
|
+
}
|
|
915
|
+
const Code = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("code", { className: "rounded bg-ui-bg-subtle px-1 py-0.5", children });
|
|
916
|
+
function gateCopy(variant) {
|
|
917
|
+
switch (variant) {
|
|
918
|
+
case "unauthorized":
|
|
919
|
+
return {
|
|
920
|
+
heading: `${BRAND.name} key not recognized`,
|
|
921
|
+
body: `An API key is configured, but ${BRAND.name} didn’t accept it. Double-check the key, or get a fresh one.`,
|
|
922
|
+
hint: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
923
|
+
"Make sure ",
|
|
924
|
+
/* @__PURE__ */ jsxRuntime.jsx(Code, { children: "ORDINANT_API_KEY" }),
|
|
925
|
+
" matches a key issued by your",
|
|
926
|
+
" ",
|
|
927
|
+
BRAND.name,
|
|
928
|
+
" deployment, then restart."
|
|
929
|
+
] })
|
|
930
|
+
};
|
|
931
|
+
case "unreachable":
|
|
932
|
+
return {
|
|
933
|
+
heading: `Can’t reach ${BRAND.name}`,
|
|
934
|
+
body: `${BRAND.name} is configured, but the engine isn’t responding. Confirm your deployment is running and reachable, then retry.`,
|
|
935
|
+
hint: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
936
|
+
"Check that ",
|
|
937
|
+
/* @__PURE__ */ jsxRuntime.jsx(Code, { children: "ORDINANT_BASE_URL" }),
|
|
938
|
+
" points to a running",
|
|
939
|
+
" ",
|
|
940
|
+
BRAND.name,
|
|
941
|
+
" deployment."
|
|
942
|
+
] })
|
|
943
|
+
};
|
|
944
|
+
default:
|
|
945
|
+
return {
|
|
946
|
+
heading: `Activate ${BRAND.name}`,
|
|
947
|
+
body: `Your compliance engine isn’t connected yet. Get an API key to start gating checkout and protecting every order against firearms, ammo, and restricted-item law.`,
|
|
948
|
+
hint: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
949
|
+
"Already have one? Set ",
|
|
950
|
+
/* @__PURE__ */ jsxRuntime.jsx(Code, { children: "ORDINANT_API_KEY" }),
|
|
951
|
+
" and",
|
|
952
|
+
" ",
|
|
953
|
+
/* @__PURE__ */ jsxRuntime.jsx(Code, { children: "ORDINANT_BASE_URL" }),
|
|
954
|
+
" in your environment, then restart."
|
|
955
|
+
] })
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function GateOverlay({
|
|
960
|
+
variant = "not_activated",
|
|
961
|
+
onRetry
|
|
962
|
+
}) {
|
|
963
|
+
const copy = gateCopy(variant);
|
|
964
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
965
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
966
|
+
"div",
|
|
967
|
+
{
|
|
968
|
+
className: "absolute inset-0 z-40",
|
|
969
|
+
style: {
|
|
970
|
+
background: "rgba(15,23,42,0.45)",
|
|
971
|
+
backdropFilter: "blur(6px)",
|
|
972
|
+
WebkitBackdropFilter: "blur(6px)"
|
|
973
|
+
},
|
|
974
|
+
"aria-hidden": true
|
|
975
|
+
}
|
|
976
|
+
),
|
|
977
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none fixed inset-0 z-50 flex items-center justify-center p-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-auto w-[460px] max-w-[92vw] rounded-2xl bg-ui-bg-base p-8 text-center shadow-elevation-modal", children: [
|
|
978
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mx-auto flex w-fit", children: /* @__PURE__ */ jsxRuntime.jsx(BrandMark, { size: 56 }) }),
|
|
979
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", className: "mt-4", children: copy.heading }),
|
|
980
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle mt-2", children: copy.body }),
|
|
981
|
+
variant === "unreachable" && onRetry ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
982
|
+
"button",
|
|
983
|
+
{
|
|
984
|
+
type: "button",
|
|
985
|
+
onClick: onRetry,
|
|
986
|
+
className: "mt-6 inline-flex items-center gap-2 rounded-md px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90",
|
|
987
|
+
style: { background: BRAND_BLUE },
|
|
988
|
+
children: "Retry"
|
|
989
|
+
}
|
|
990
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
991
|
+
"a",
|
|
992
|
+
{
|
|
993
|
+
href: BRAND.website,
|
|
994
|
+
target: "_blank",
|
|
995
|
+
rel: "noreferrer",
|
|
996
|
+
className: "mt-6 inline-flex items-center gap-2 rounded-md px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90",
|
|
997
|
+
style: { background: BRAND_BLUE },
|
|
998
|
+
children: "Get your API key →"
|
|
999
|
+
}
|
|
1000
|
+
),
|
|
1001
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-muted mt-5", children: copy.hint })
|
|
1002
|
+
] }) })
|
|
1003
|
+
] });
|
|
1004
|
+
}
|
|
1005
|
+
const OVERVIEW_KEY = ["ordinant-overview"];
|
|
1006
|
+
const READY_THRESHOLD = 0.85;
|
|
1007
|
+
const PAGE_SIZE = 25;
|
|
1008
|
+
const GROUP_PAGE_SIZE = 12;
|
|
1009
|
+
const ACTIONABLE = ["needs_sku", "unclassified", "proposed"];
|
|
1010
|
+
const VALID_FILTERS = [
|
|
1011
|
+
"attention",
|
|
1012
|
+
"all",
|
|
1013
|
+
"needs_sku",
|
|
1014
|
+
"unclassified",
|
|
1015
|
+
"proposed",
|
|
1016
|
+
"confirmed"
|
|
1017
|
+
];
|
|
1018
|
+
const OrdinantPage = () => {
|
|
1019
|
+
var _a;
|
|
1020
|
+
const queryClient = reactQuery.useQueryClient();
|
|
1021
|
+
const [progress, setProgress] = react.useState(null);
|
|
1022
|
+
const [searchParams, setSearchParams] = reactRouterDom.useSearchParams();
|
|
1023
|
+
const rawView = searchParams.get("view");
|
|
1024
|
+
const filter = rawView && VALID_FILTERS.includes(rawView) ? rawView : "attention";
|
|
1025
|
+
const search = searchParams.get("q") ?? "";
|
|
1026
|
+
const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10) || 1);
|
|
1027
|
+
const patch = (updates, replace = false) => {
|
|
1028
|
+
const next = new URLSearchParams(searchParams);
|
|
1029
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
1030
|
+
if (v === null || v === "") {
|
|
1031
|
+
next.delete(k);
|
|
1032
|
+
} else {
|
|
1033
|
+
next.set(k, v);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
setSearchParams(next, { replace });
|
|
1037
|
+
};
|
|
1038
|
+
const setFilter = (f) => patch({ view: f, page: "1" });
|
|
1039
|
+
const onSearch = (q2) => patch({ q: q2, page: "1" }, true);
|
|
1040
|
+
const setPage = (p) => patch({ page: String(p) });
|
|
1041
|
+
const { data: status } = reactQuery.useQuery({
|
|
1042
|
+
queryKey: ["ordinant-status"],
|
|
1043
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/status")
|
|
1044
|
+
});
|
|
1045
|
+
const ready = (status == null ? void 0 : status.configured) === true && (status == null ? void 0 : status.engine_reachable) === true;
|
|
1046
|
+
const gated = status !== void 0 && !ready;
|
|
1047
|
+
const gateVariant = (status == null ? void 0 : status.reason) === "unauthorized" ? "unauthorized" : (status == null ? void 0 : status.reason) === "unreachable" ? "unreachable" : "not_activated";
|
|
1048
|
+
const { data: me } = reactQuery.useQuery({
|
|
1049
|
+
queryKey: ["ordinant-me"],
|
|
1050
|
+
queryFn: () => sdk.client.fetch("/admin/users/me")
|
|
1051
|
+
});
|
|
1052
|
+
const firstName = (_a = me == null ? void 0 : me.user) == null ? void 0 : _a.first_name;
|
|
1053
|
+
const { data: stats } = reactQuery.useQuery({
|
|
1054
|
+
queryKey: ["ordinant-stats"],
|
|
1055
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/stats"),
|
|
1056
|
+
enabled: ready
|
|
1057
|
+
});
|
|
1058
|
+
const {
|
|
1059
|
+
data,
|
|
1060
|
+
isFetching: overviewFetching,
|
|
1061
|
+
error
|
|
1062
|
+
} = reactQuery.useQuery({
|
|
1063
|
+
queryKey: OVERVIEW_KEY,
|
|
1064
|
+
queryFn: () => sdk.client.fetch("/admin/ordinant/overview"),
|
|
1065
|
+
enabled: ready
|
|
1066
|
+
});
|
|
1067
|
+
const invalidate = () => queryClient.invalidateQueries({ queryKey: OVERVIEW_KEY });
|
|
1068
|
+
const rows = (data == null ? void 0 : data.rows) ?? [];
|
|
1069
|
+
const summary = data == null ? void 0 : data.summary;
|
|
1070
|
+
const runClassifyAll = async (onProgress) => {
|
|
1071
|
+
let proposed = 0;
|
|
1072
|
+
let failed = 0;
|
|
1073
|
+
let total = 0;
|
|
1074
|
+
for (let i = 0; i < 500; i++) {
|
|
1075
|
+
const res = await sdk.client.fetch("/admin/ordinant/classify-all", { method: "POST" });
|
|
1076
|
+
if (i === 0) total = res.processed + res.remaining;
|
|
1077
|
+
proposed += res.proposed;
|
|
1078
|
+
failed += res.failed;
|
|
1079
|
+
onProgress == null ? void 0 : onProgress(Math.max(0, total - res.remaining), total);
|
|
1080
|
+
if (res.processed === 0 || res.remaining === 0 || res.proposed === 0) break;
|
|
1081
|
+
}
|
|
1082
|
+
return { proposed, failed };
|
|
1083
|
+
};
|
|
1084
|
+
const generateAll = reactQuery.useMutation({
|
|
1085
|
+
mutationFn: async () => {
|
|
1086
|
+
setProgress({
|
|
1087
|
+
title: "Generating SKUs & classifying",
|
|
1088
|
+
subtitle: "Assigning SKUs to your products…",
|
|
1089
|
+
done: 0,
|
|
1090
|
+
total: 0
|
|
1091
|
+
});
|
|
1092
|
+
const gen = await sdk.client.fetch(
|
|
1093
|
+
"/admin/ordinant/generate-all-skus",
|
|
1094
|
+
{ method: "POST" }
|
|
1095
|
+
);
|
|
1096
|
+
setProgress((p) => ({
|
|
1097
|
+
...p,
|
|
1098
|
+
subtitle: "Classifying your products…"
|
|
1099
|
+
}));
|
|
1100
|
+
const cls = await runClassifyAll(
|
|
1101
|
+
(done, total) => setProgress({
|
|
1102
|
+
title: "Generating SKUs & classifying",
|
|
1103
|
+
subtitle: "Classifying your products…",
|
|
1104
|
+
done,
|
|
1105
|
+
total
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
return { ...gen, ...cls };
|
|
1109
|
+
},
|
|
1110
|
+
onSuccess: ({ generated, proposed }) => {
|
|
1111
|
+
invalidate();
|
|
1112
|
+
ui.toast.success(
|
|
1113
|
+
`Generated ${generated} SKU${generated === 1 ? "" : "s"} and classified ${proposed}.`
|
|
1114
|
+
);
|
|
1115
|
+
},
|
|
1116
|
+
onError: (err) => ui.toast.error(`Failed: ${err.message}`),
|
|
1117
|
+
onSettled: () => setProgress(null)
|
|
1118
|
+
});
|
|
1119
|
+
const classifyAll = reactQuery.useMutation({
|
|
1120
|
+
mutationFn: () => {
|
|
1121
|
+
setProgress({
|
|
1122
|
+
title: "Classifying products",
|
|
1123
|
+
subtitle: "Reviewing your catalog…",
|
|
1124
|
+
done: 0,
|
|
1125
|
+
total: 0
|
|
1126
|
+
});
|
|
1127
|
+
return runClassifyAll(
|
|
1128
|
+
(done, total) => setProgress({
|
|
1129
|
+
title: "Classifying products",
|
|
1130
|
+
subtitle: "Reviewing your catalog…",
|
|
1131
|
+
done,
|
|
1132
|
+
total
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
1135
|
+
},
|
|
1136
|
+
onSuccess: ({ proposed, failed }) => {
|
|
1137
|
+
invalidate();
|
|
1138
|
+
ui.toast.success(
|
|
1139
|
+
`Classified ${proposed} variant${proposed === 1 ? "" : "s"}` + (failed > 0 ? ` (${failed} need attention)` : "")
|
|
1140
|
+
);
|
|
1141
|
+
},
|
|
1142
|
+
onError: (err) => ui.toast.error(`Classification failed: ${err.message}`),
|
|
1143
|
+
onSettled: () => setProgress(null)
|
|
1144
|
+
});
|
|
1145
|
+
const readyRows = react.useMemo(
|
|
1146
|
+
() => rows.filter(
|
|
1147
|
+
(r) => r.status === "proposed" && (r.confidence ?? 0) >= READY_THRESHOLD
|
|
1148
|
+
),
|
|
1149
|
+
[rows]
|
|
1150
|
+
);
|
|
1151
|
+
const bulkConfirm = reactQuery.useMutation({
|
|
1152
|
+
mutationFn: (skus) => sdk.client.fetch("/admin/ordinant/classifications/bulk-confirm", {
|
|
1153
|
+
method: "POST",
|
|
1154
|
+
body: { skus }
|
|
1155
|
+
}),
|
|
1156
|
+
onSuccess: () => {
|
|
1157
|
+
invalidate();
|
|
1158
|
+
setConfirmOpen(false);
|
|
1159
|
+
ui.toast.success("Confirmed — these variants can now sell.");
|
|
1160
|
+
},
|
|
1161
|
+
onError: (err) => ui.toast.error(`Confirmation failed: ${err.message}`)
|
|
1162
|
+
});
|
|
1163
|
+
const [confirmOpen, setConfirmOpen] = react.useState(false);
|
|
1164
|
+
const q = search.trim().toLowerCase();
|
|
1165
|
+
const filtered = react.useMemo(
|
|
1166
|
+
() => rows.filter((r) => {
|
|
1167
|
+
const inFilter = filter === "attention" ? ACTIONABLE.includes(r.status) : filter === "all" ? true : r.status === filter;
|
|
1168
|
+
if (!inFilter) {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
if (!q) {
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
return r.product_title.toLowerCase().includes(q) || r.sku.toLowerCase().includes(q);
|
|
1175
|
+
}),
|
|
1176
|
+
[rows, filter, q]
|
|
1177
|
+
);
|
|
1178
|
+
const tableMode = filter === "all" || filter === "confirmed";
|
|
1179
|
+
const groups = react.useMemo(
|
|
1180
|
+
() => tableMode ? [] : groupByProduct(filtered),
|
|
1181
|
+
[filtered, tableMode]
|
|
1182
|
+
);
|
|
1183
|
+
const totalItems = tableMode ? filtered.length : groups.length;
|
|
1184
|
+
const perPage = tableMode ? PAGE_SIZE : GROUP_PAGE_SIZE;
|
|
1185
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / perPage));
|
|
1186
|
+
const safePage = Math.min(page, totalPages);
|
|
1187
|
+
const start = (safePage - 1) * perPage;
|
|
1188
|
+
const pageRows = tableMode ? filtered.slice(start, start + perPage) : [];
|
|
1189
|
+
const pageGroups = tableMode ? [] : groups.slice(start, start + perPage);
|
|
1190
|
+
const attentionCount = ((summary == null ? void 0 : summary.needs_sku) ?? 0) + ((summary == null ? void 0 : summary.unclassified) ?? 0) + ((summary == null ? void 0 : summary.proposed) ?? 0);
|
|
1191
|
+
const readyByCategory = react.useMemo(() => {
|
|
1192
|
+
var _a2;
|
|
1193
|
+
const m = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const r of readyRows) {
|
|
1195
|
+
const cat = ((_a2 = r.attributes) == null ? void 0 : _a2["category"]) ?? "unspecified";
|
|
1196
|
+
m.set(cat, (m.get(cat) ?? 0) + 1);
|
|
1197
|
+
}
|
|
1198
|
+
return [...m.entries()].sort((a, b) => b[1] - a[1]);
|
|
1199
|
+
}, [readyRows]);
|
|
1200
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "relative divide-y p-0", children: [
|
|
1201
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4 px-6 py-5", children: [
|
|
1202
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1203
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Heading, { level: "h1", children: [
|
|
1204
|
+
"Welcome back",
|
|
1205
|
+
firstName ? `, ${firstName}` : ""
|
|
1206
|
+
] }),
|
|
1207
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle mt-1", children: "Here’s your compliance posture. Ordinant checks every order against current law and stops the ones that can’t legally ship." })
|
|
1208
|
+
] }),
|
|
1209
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 flex-col items-end gap-2", children: [
|
|
1210
|
+
/* @__PURE__ */ jsxRuntime.jsx(BrandWordmark, {}),
|
|
1211
|
+
/* @__PURE__ */ jsxRuntime.jsx(HealthPill, { status })
|
|
1212
|
+
] })
|
|
1213
|
+
] }),
|
|
1214
|
+
stats && /* @__PURE__ */ jsxRuntime.jsx(ImpactHero, { stats }),
|
|
1215
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-4 px-6 py-4", children: [
|
|
1216
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1217
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-base", children: "Catalog compliance" }),
|
|
1218
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: "Confirmed items are your attestation and can be sold." })
|
|
1219
|
+
] }),
|
|
1220
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1221
|
+
((summary == null ? void 0 : summary.needs_sku) ?? 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs(BrandButton, { onClick: () => generateAll.mutate(), loading: generateAll.isPending, children: [
|
|
1222
|
+
"Generate ",
|
|
1223
|
+
summary == null ? void 0 : summary.needs_sku,
|
|
1224
|
+
" SKUs"
|
|
1225
|
+
] }),
|
|
1226
|
+
((summary == null ? void 0 : summary.unclassified) ?? 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1227
|
+
ui.Button,
|
|
1228
|
+
{
|
|
1229
|
+
size: "small",
|
|
1230
|
+
variant: "secondary",
|
|
1231
|
+
onClick: () => classifyAll.mutate(),
|
|
1232
|
+
isLoading: classifyAll.isPending,
|
|
1233
|
+
children: [
|
|
1234
|
+
"Classify ",
|
|
1235
|
+
summary == null ? void 0 : summary.unclassified
|
|
1236
|
+
]
|
|
1237
|
+
}
|
|
1238
|
+
),
|
|
1239
|
+
readyRows.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "small", variant: "secondary", onClick: () => setConfirmOpen(true), children: [
|
|
1240
|
+
"Confirm ",
|
|
1241
|
+
readyRows.length,
|
|
1242
|
+
" ready"
|
|
1243
|
+
] })
|
|
1244
|
+
] })
|
|
1245
|
+
] }),
|
|
1246
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 divide-x sm:grid-cols-6", children: [
|
|
1247
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "Needs attention", value: attentionCount, active: filter === "attention", onClick: () => setFilter("attention") }),
|
|
1248
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "Needs SKU", value: summary == null ? void 0 : summary.needs_sku, active: filter === "needs_sku", onClick: () => setFilter("needs_sku") }),
|
|
1249
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "Unclassified", value: summary == null ? void 0 : summary.unclassified, active: filter === "unclassified", onClick: () => setFilter("unclassified") }),
|
|
1250
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "To review", value: summary == null ? void 0 : summary.proposed, active: filter === "proposed", onClick: () => setFilter("proposed") }),
|
|
1251
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "Confirmed", value: summary == null ? void 0 : summary.confirmed, active: filter === "confirmed", onClick: () => setFilter("confirmed") }),
|
|
1252
|
+
/* @__PURE__ */ jsxRuntime.jsx(FilterTile, { label: "All", value: summary == null ? void 0 : summary.total, active: filter === "all", onClick: () => setFilter("all") })
|
|
1253
|
+
] }),
|
|
1254
|
+
ready && !error && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 px-6 py-3", children: [
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-sm", children: [
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.MagnifyingGlass, { className: "pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-ui-fg-muted" }),
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1258
|
+
ui.Input,
|
|
1259
|
+
{
|
|
1260
|
+
size: "small",
|
|
1261
|
+
placeholder: "Search products or SKUs…",
|
|
1262
|
+
value: search,
|
|
1263
|
+
onChange: (e) => onSearch(e.target.value),
|
|
1264
|
+
className: "pl-8"
|
|
1265
|
+
}
|
|
1266
|
+
)
|
|
1267
|
+
] }),
|
|
1268
|
+
data && totalItems > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", leading: "compact", className: "shrink-0 text-ui-fg-subtle", children: [
|
|
1269
|
+
totalItems.toLocaleString(),
|
|
1270
|
+
" ",
|
|
1271
|
+
tableMode ? "item" : "product",
|
|
1272
|
+
totalItems === 1 ? "" : "s"
|
|
1273
|
+
] })
|
|
1274
|
+
] }),
|
|
1275
|
+
(!status || ready && overviewFetching && !data) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: status ? "Loading catalog…" : "Connecting to Ordinant…" }) }),
|
|
1276
|
+
error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-6", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-error", children: [
|
|
1277
|
+
"Could not reach Ordinant: ",
|
|
1278
|
+
error.message
|
|
1279
|
+
] }) }),
|
|
1280
|
+
ready && !error && data && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
1281
|
+
totalItems === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1282
|
+
CatalogEmpty,
|
|
1283
|
+
{
|
|
1284
|
+
filter,
|
|
1285
|
+
query: q,
|
|
1286
|
+
confirmed: (summary == null ? void 0 : summary.confirmed) ?? 0,
|
|
1287
|
+
onClear: () => onSearch("")
|
|
1288
|
+
}
|
|
1289
|
+
) : tableMode ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(CatalogTable, { rows: pageRows, onChange: invalidate }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col divide-y", children: pageGroups.map(([productId, group]) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1290
|
+
ProductGroup,
|
|
1291
|
+
{
|
|
1292
|
+
productId,
|
|
1293
|
+
title: group[0].product_title,
|
|
1294
|
+
rows: group,
|
|
1295
|
+
onChange: invalidate
|
|
1296
|
+
},
|
|
1297
|
+
productId
|
|
1298
|
+
)) }),
|
|
1299
|
+
totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsx(Pager, { page: safePage, totalPages, onPage: setPage })
|
|
1300
|
+
] }),
|
|
1301
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt, { open: confirmOpen, onOpenChange: setConfirmOpen, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
|
|
1302
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
|
|
1303
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Title, { children: [
|
|
1304
|
+
"Confirm ",
|
|
1305
|
+
readyRows.length,
|
|
1306
|
+
" classifications"
|
|
1307
|
+
] }),
|
|
1308
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: "You’re attesting that these variants are classified as proposed — your legal classification of record, which decides how they’re gated at checkout:" })
|
|
1309
|
+
] }),
|
|
1310
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-1 px-6 pb-2", children: readyByCategory.map(([cat, count]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
1311
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", children: cat.replace(/_/g, " ") }),
|
|
1312
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "2xsmall", children: count })
|
|
1313
|
+
] }, cat)) }),
|
|
1314
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
|
|
1315
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { disabled: bulkConfirm.isPending, children: "Cancel" }),
|
|
1316
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1317
|
+
ui.Button,
|
|
1318
|
+
{
|
|
1319
|
+
size: "small",
|
|
1320
|
+
onClick: () => bulkConfirm.mutate(readyRows.map((r) => r.sku)),
|
|
1321
|
+
isLoading: bulkConfirm.isPending,
|
|
1322
|
+
style: { background: BRAND_BLUE },
|
|
1323
|
+
children: [
|
|
1324
|
+
"Confirm ",
|
|
1325
|
+
readyRows.length
|
|
1326
|
+
]
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
] })
|
|
1330
|
+
] }) }),
|
|
1331
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1332
|
+
ProgressModal,
|
|
1333
|
+
{
|
|
1334
|
+
open: progress !== null,
|
|
1335
|
+
title: (progress == null ? void 0 : progress.title) ?? "",
|
|
1336
|
+
subtitle: progress == null ? void 0 : progress.subtitle,
|
|
1337
|
+
done: (progress == null ? void 0 : progress.done) ?? 0,
|
|
1338
|
+
total: (progress == null ? void 0 : progress.total) ?? 0
|
|
1339
|
+
}
|
|
1340
|
+
),
|
|
1341
|
+
gated && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1342
|
+
GateOverlay,
|
|
1343
|
+
{
|
|
1344
|
+
variant: gateVariant,
|
|
1345
|
+
onRetry: () => queryClient.invalidateQueries({ queryKey: ["ordinant-status"] })
|
|
1346
|
+
}
|
|
1347
|
+
)
|
|
1348
|
+
] });
|
|
1349
|
+
};
|
|
1350
|
+
const HealthPill = ({ status }) => {
|
|
1351
|
+
if (!status) {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
const ok = status.configured && status.engine_reachable;
|
|
1355
|
+
const label = ok ? `Engine connected${status.ruleset_version ? ` · ${status.ruleset_version}` : ""}` : status.configured ? "Engine unreachable" : "Not activated";
|
|
1356
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2 rounded-full border border-ui-border-base bg-ui-bg-subtle px-3 py-1", children: [
|
|
1357
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1358
|
+
"span",
|
|
1359
|
+
{
|
|
1360
|
+
className: "inline-block h-2 w-2 rounded-full",
|
|
1361
|
+
style: { background: ok ? BRAND_BLUE : "#DC2626" },
|
|
1362
|
+
"aria-hidden": true
|
|
1363
|
+
}
|
|
1364
|
+
),
|
|
1365
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle", children: label })
|
|
1366
|
+
] });
|
|
1367
|
+
};
|
|
1368
|
+
const CatalogEmpty = ({
|
|
1369
|
+
filter,
|
|
1370
|
+
query,
|
|
1371
|
+
confirmed,
|
|
1372
|
+
onClear
|
|
1373
|
+
}) => {
|
|
1374
|
+
if (query) {
|
|
1375
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-12 text-center", children: [
|
|
1376
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-subtle", children: [
|
|
1377
|
+
"No products match “",
|
|
1378
|
+
query,
|
|
1379
|
+
"”."
|
|
1380
|
+
] }),
|
|
1381
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1382
|
+
"button",
|
|
1383
|
+
{
|
|
1384
|
+
type: "button",
|
|
1385
|
+
onClick: onClear,
|
|
1386
|
+
className: "mt-2 text-sm font-medium",
|
|
1387
|
+
style: { color: BRAND_BLUE },
|
|
1388
|
+
children: "Clear search"
|
|
1389
|
+
}
|
|
1390
|
+
)
|
|
1391
|
+
] });
|
|
1392
|
+
}
|
|
1393
|
+
if (filter === "attention") {
|
|
1394
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3 px-6 py-14 text-center", children: [
|
|
1395
|
+
/* @__PURE__ */ jsxRuntime.jsx(BrandMark, { size: 56 }),
|
|
1396
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1397
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "plus", className: "text-ui-fg-base", children: "You’re all caught up" }),
|
|
1398
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle mt-1", children: [
|
|
1399
|
+
confirmed.toLocaleString(),
|
|
1400
|
+
" SKU",
|
|
1401
|
+
confirmed === 1 ? "" : "s",
|
|
1402
|
+
" classified and protected. Nothing needs your attention right now."
|
|
1403
|
+
] })
|
|
1404
|
+
] })
|
|
1405
|
+
] });
|
|
1406
|
+
}
|
|
1407
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-12 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Nothing in this view." }) });
|
|
1408
|
+
};
|
|
1409
|
+
const Pager = ({
|
|
1410
|
+
page,
|
|
1411
|
+
totalPages,
|
|
1412
|
+
onPage
|
|
1413
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between border-t border-ui-border-base px-6 py-3", children: [
|
|
1414
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle", children: [
|
|
1415
|
+
"Page ",
|
|
1416
|
+
page,
|
|
1417
|
+
" of ",
|
|
1418
|
+
totalPages
|
|
1419
|
+
] }),
|
|
1420
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
1421
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", disabled: page <= 1, onClick: () => onPage(page - 1), children: "Previous" }),
|
|
1422
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1423
|
+
ui.Button,
|
|
1424
|
+
{
|
|
1425
|
+
size: "small",
|
|
1426
|
+
variant: "secondary",
|
|
1427
|
+
disabled: page >= totalPages,
|
|
1428
|
+
onClick: () => onPage(page + 1),
|
|
1429
|
+
children: "Next"
|
|
1430
|
+
}
|
|
1431
|
+
)
|
|
1432
|
+
] })
|
|
1433
|
+
] });
|
|
1434
|
+
const ProductGroup = ({
|
|
1435
|
+
productId,
|
|
1436
|
+
title,
|
|
1437
|
+
rows,
|
|
1438
|
+
onChange
|
|
1439
|
+
}) => {
|
|
1440
|
+
const needsSku = rows.some((r) => r.status === "needs_sku");
|
|
1441
|
+
const generate = reactQuery.useMutation({
|
|
1442
|
+
mutationFn: () => sdk.client.fetch(`/admin/ordinant/products/${productId}/generate-skus`, {
|
|
1443
|
+
method: "POST"
|
|
1444
|
+
}),
|
|
1445
|
+
onSuccess: () => {
|
|
1446
|
+
onChange();
|
|
1447
|
+
ui.toast.success(`SKUs generated and classified for ${title}.`);
|
|
1448
|
+
},
|
|
1449
|
+
onError: (err) => ui.toast.error(`Could not generate SKUs: ${err.message}`)
|
|
1450
|
+
});
|
|
1451
|
+
const classify = reactQuery.useMutation({
|
|
1452
|
+
mutationFn: () => sdk.client.fetch(`/admin/ordinant/products/${productId}/classify`, {
|
|
1453
|
+
method: "POST"
|
|
1454
|
+
}),
|
|
1455
|
+
onSuccess: () => {
|
|
1456
|
+
onChange();
|
|
1457
|
+
ui.toast.success(`Classified ${title}.`);
|
|
1458
|
+
},
|
|
1459
|
+
onError: (err) => ui.toast.error(`Classification failed: ${err.message}`)
|
|
1460
|
+
});
|
|
1461
|
+
const hasUnclassified = rows.some((r) => r.status === "unclassified");
|
|
1462
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
1463
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [
|
|
1464
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "truncate", children: title }),
|
|
1465
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
1466
|
+
needsSku && /* @__PURE__ */ jsxRuntime.jsx(BrandButton, { onClick: () => generate.mutate(), loading: generate.isPending, children: "Generate SKU" }),
|
|
1467
|
+
hasUnclassified && !needsSku && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1468
|
+
ui.Button,
|
|
1469
|
+
{
|
|
1470
|
+
size: "small",
|
|
1471
|
+
variant: "secondary",
|
|
1472
|
+
onClick: () => classify.mutate(),
|
|
1473
|
+
isLoading: classify.isPending,
|
|
1474
|
+
children: "Classify"
|
|
1475
|
+
}
|
|
1476
|
+
)
|
|
1477
|
+
] })
|
|
1478
|
+
] }),
|
|
1479
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-2", children: rows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(VariantLine, { row, onChange }, row.variant_id)) })
|
|
1480
|
+
] });
|
|
1481
|
+
};
|
|
1482
|
+
const VariantLine = ({
|
|
1483
|
+
row,
|
|
1484
|
+
onChange
|
|
1485
|
+
}) => {
|
|
1486
|
+
const [editing, setEditing] = react.useState(false);
|
|
1487
|
+
const [draft, setDraft] = react.useState({});
|
|
1488
|
+
const confirm = reactQuery.useMutation({
|
|
1489
|
+
mutationFn: (attributes) => sdk.client.fetch(
|
|
1490
|
+
`/admin/ordinant/classifications/${encodeURIComponent(row.sku)}/confirm`,
|
|
1491
|
+
{ method: "POST", body: { attributes } }
|
|
1492
|
+
),
|
|
1493
|
+
onSuccess: () => {
|
|
1494
|
+
onChange();
|
|
1495
|
+
setEditing(false);
|
|
1496
|
+
ui.toast.success(`${row.sku} confirmed.`);
|
|
1497
|
+
},
|
|
1498
|
+
onError: (err) => ui.toast.error(`Confirmation failed: ${err.message}`)
|
|
1499
|
+
});
|
|
1500
|
+
const label = row.status === "needs_sku" ? row.variant_title ?? "variant" : row.sku;
|
|
1501
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-base px-3 py-2.5", children: [
|
|
1502
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
1503
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
|
|
1504
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", weight: "plus", className: "truncate", children: label }),
|
|
1505
|
+
row.variant_title && row.variant_title !== "Default variant" && row.status !== "needs_sku" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", leading: "compact", className: "text-ui-fg-subtle truncate", children: row.variant_title }),
|
|
1506
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatusBadge, { status: row.status, confidence: row.confidence })
|
|
1507
|
+
] }),
|
|
1508
|
+
row.status === "proposed" && !editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
1509
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1510
|
+
ui.Button,
|
|
1511
|
+
{
|
|
1512
|
+
size: "small",
|
|
1513
|
+
onClick: () => confirm.mutate(row.attributes ?? {}),
|
|
1514
|
+
isLoading: confirm.isPending,
|
|
1515
|
+
disabled: confirm.isPending,
|
|
1516
|
+
style: { background: BRAND_BLUE },
|
|
1517
|
+
children: "Confirm"
|
|
1518
|
+
}
|
|
1519
|
+
),
|
|
1520
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1521
|
+
ui.Button,
|
|
1522
|
+
{
|
|
1523
|
+
size: "small",
|
|
1524
|
+
variant: "secondary",
|
|
1525
|
+
onClick: () => {
|
|
1526
|
+
setDraft(row.attributes ?? {});
|
|
1527
|
+
setEditing(true);
|
|
1528
|
+
},
|
|
1529
|
+
children: "Edit"
|
|
1530
|
+
}
|
|
1531
|
+
)
|
|
1532
|
+
] }),
|
|
1533
|
+
row.status === "confirmed" && !editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1534
|
+
ui.Button,
|
|
1535
|
+
{
|
|
1536
|
+
size: "small",
|
|
1537
|
+
variant: "secondary",
|
|
1538
|
+
onClick: () => {
|
|
1539
|
+
setDraft(row.attributes ?? {});
|
|
1540
|
+
setEditing(true);
|
|
1541
|
+
},
|
|
1542
|
+
children: "Edit"
|
|
1543
|
+
}
|
|
1544
|
+
)
|
|
1545
|
+
] }),
|
|
1546
|
+
(row.status === "proposed" || row.status === "confirmed") && !editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2", children: [
|
|
1547
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReadableAttributes, { attributes: row.attributes }),
|
|
1548
|
+
row.notes && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-1", children: row.notes })
|
|
1549
|
+
] }),
|
|
1550
|
+
row.status === "unclassified" && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", leading: "compact", className: "text-ui-fg-subtle mt-1", children: "Has a SKU, awaiting classification — use “Classify” above." }),
|
|
1551
|
+
editing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 flex flex-col gap-3", children: [
|
|
1552
|
+
/* @__PURE__ */ jsxRuntime.jsx(AttributeEditor, { value: draft, onChange: setDraft }),
|
|
1553
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
1554
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1555
|
+
ui.Button,
|
|
1556
|
+
{
|
|
1557
|
+
size: "small",
|
|
1558
|
+
onClick: () => confirm.mutate(draft),
|
|
1559
|
+
isLoading: confirm.isPending,
|
|
1560
|
+
disabled: confirm.isPending,
|
|
1561
|
+
style: { background: BRAND_BLUE },
|
|
1562
|
+
children: "Confirm classification"
|
|
1563
|
+
}
|
|
1564
|
+
),
|
|
1565
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1566
|
+
ui.Button,
|
|
1567
|
+
{
|
|
1568
|
+
size: "small",
|
|
1569
|
+
variant: "secondary",
|
|
1570
|
+
onClick: () => setEditing(false),
|
|
1571
|
+
disabled: confirm.isPending,
|
|
1572
|
+
children: "Cancel"
|
|
1573
|
+
}
|
|
1574
|
+
)
|
|
1575
|
+
] })
|
|
1576
|
+
] })
|
|
1577
|
+
] });
|
|
1578
|
+
};
|
|
1579
|
+
function groupByProduct(rows) {
|
|
1580
|
+
const m = /* @__PURE__ */ new Map();
|
|
1581
|
+
for (const r of rows) {
|
|
1582
|
+
const arr = m.get(r.product_id) ?? [];
|
|
1583
|
+
arr.push(r);
|
|
1584
|
+
m.set(r.product_id, arr);
|
|
1585
|
+
}
|
|
1586
|
+
return [...m.entries()];
|
|
1587
|
+
}
|
|
1588
|
+
const config = adminSdk.defineRouteConfig({
|
|
1589
|
+
label: "Ordinant",
|
|
1590
|
+
// Swaps to the company logo automatically once BRAND.logo is set.
|
|
1591
|
+
icon: BrandIcon
|
|
1592
|
+
});
|
|
1593
|
+
const i18nTranslations0 = {};
|
|
1594
|
+
const widgetModule = { widgets: [
|
|
1595
|
+
{
|
|
1596
|
+
Component: OrdinantProductWidget,
|
|
1597
|
+
zone: ["product.details.after"],
|
|
1598
|
+
widgetId: "Widget-30be"
|
|
1599
|
+
}
|
|
1600
|
+
] };
|
|
1601
|
+
const routeModule = {
|
|
1602
|
+
routes: [
|
|
1603
|
+
{
|
|
1604
|
+
Component: OrdinantPage,
|
|
1605
|
+
path: "/ordinant"
|
|
1606
|
+
}
|
|
1607
|
+
]
|
|
1608
|
+
};
|
|
1609
|
+
const menuItemModule = {
|
|
1610
|
+
menuItems: [
|
|
1611
|
+
{
|
|
1612
|
+
label: config.label,
|
|
1613
|
+
icon: config.icon,
|
|
1614
|
+
path: "/ordinant",
|
|
1615
|
+
nested: void 0,
|
|
1616
|
+
rank: void 0,
|
|
1617
|
+
translationNs: void 0
|
|
1618
|
+
}
|
|
1619
|
+
]
|
|
1620
|
+
};
|
|
1621
|
+
const formModule = { customFields: {} };
|
|
1622
|
+
const displayModule = {
|
|
1623
|
+
displays: {}
|
|
1624
|
+
};
|
|
1625
|
+
const i18nModule = { resources: i18nTranslations0 };
|
|
1626
|
+
const layoutModule = { layouts: [] };
|
|
1627
|
+
const plugin = {
|
|
1628
|
+
widgetModule,
|
|
1629
|
+
routeModule,
|
|
1630
|
+
menuItemModule,
|
|
1631
|
+
formModule,
|
|
1632
|
+
displayModule,
|
|
1633
|
+
i18nModule,
|
|
1634
|
+
layoutModule
|
|
1635
|
+
};
|
|
1636
|
+
module.exports = plugin;
|