@legenki/print2medusa 0.8.1 → 0.9.2

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.
@@ -307,6 +307,347 @@ const PrintfulSyncWidget = () => {
307
307
  adminSdk.defineWidgetConfig({
308
308
  zone: "product.list.before"
309
309
  });
310
+ const CLASS_LABEL = {
311
+ apparel: "Apparel",
312
+ embroidery: "Embroidered",
313
+ print_media: "Print media"
314
+ };
315
+ const CLASS_COLOUR = {
316
+ apparel: "blue",
317
+ embroidery: "orange",
318
+ print_media: "green"
319
+ };
320
+ const STATUS_COLOUR = {
321
+ success: "green",
322
+ running: "orange",
323
+ partial: "orange",
324
+ failed: "red"
325
+ };
326
+ const looksStuck = (row, staleMinutes) => {
327
+ if (!row || row.status !== "running") {
328
+ return false;
329
+ }
330
+ const beat = row.heartbeat_at ?? row.started_at;
331
+ if (!beat) {
332
+ return true;
333
+ }
334
+ return Date.now() - new Date(beat).getTime() > staleMinutes * 6e4;
335
+ };
336
+ const minutesSince = (iso) => {
337
+ if (!iso) {
338
+ return null;
339
+ }
340
+ return Math.floor((Date.now() - new Date(iso).getTime()) / 6e4);
341
+ };
342
+ const PrintfulPage = () => {
343
+ var _a, _b, _c;
344
+ const [syncs, setSyncs] = react.useState([]);
345
+ const [health, setHealth] = react.useState(null);
346
+ const [stats, setStats] = react.useState(null);
347
+ const [designs, setDesigns] = react.useState([]);
348
+ const [bundles, setBundles] = react.useState([]);
349
+ const [promptProducts, setPromptProducts] = react.useState([]);
350
+ const [styles, setStyles] = react.useState([]);
351
+ const [style, setStyle] = react.useState("editorial");
352
+ const [artwork, setArtwork] = react.useState("");
353
+ const [loading, setLoading] = react.useState(true);
354
+ const [clearing, setClearing] = react.useState(false);
355
+ const load = react.useCallback(async () => {
356
+ var _a2;
357
+ const [h, w, s, d, b] = await Promise.allSettled([
358
+ fetch("/admin/printful/history", { credentials: "include" }),
359
+ fetch("/admin/printful/health", { credentials: "include" }),
360
+ fetch("/admin/printful/statistics", { credentials: "include" }),
361
+ fetch("/admin/printful/design", { credentials: "include" }),
362
+ fetch("/admin/printful/bundles", { credentials: "include" })
363
+ ]);
364
+ if (h.status === "fulfilled" && h.value.ok) {
365
+ const body = await h.value.json();
366
+ setSyncs(body.syncs ?? []);
367
+ }
368
+ if (w.status === "fulfilled" && w.value.ok) {
369
+ setHealth(await w.value.json());
370
+ }
371
+ if (s.status === "fulfilled" && s.value.ok) {
372
+ const body = await s.value.json();
373
+ setStats(((_a2 = body.statistics) == null ? void 0 : _a2[0]) ?? null);
374
+ }
375
+ if (d.status === "fulfilled" && d.value.ok) {
376
+ const body = await d.value.json();
377
+ setDesigns(body.designs ?? []);
378
+ }
379
+ if (b.status === "fulfilled" && b.value.ok) {
380
+ const body = await b.value.json();
381
+ setBundles(body.bundles ?? []);
382
+ }
383
+ setLoading(false);
384
+ }, []);
385
+ const loadPrompts = react.useCallback(async () => {
386
+ const params = new URLSearchParams({ style });
387
+ if (artwork.trim()) {
388
+ params.set("artwork", artwork.trim());
389
+ }
390
+ try {
391
+ const res = await fetch(`/admin/printful/prompts?${params}`, {
392
+ credentials: "include"
393
+ });
394
+ if (!res.ok) {
395
+ return;
396
+ }
397
+ const body = await res.json();
398
+ setPromptProducts(body.products ?? []);
399
+ setStyles(body.styles ?? []);
400
+ } catch {
401
+ }
402
+ }, [style, artwork]);
403
+ react.useEffect(() => {
404
+ void load();
405
+ }, [load]);
406
+ react.useEffect(() => {
407
+ void loadPrompts();
408
+ }, [loadPrompts]);
409
+ const latest = syncs[0];
410
+ const running = (latest == null ? void 0 : latest.status) === "running";
411
+ react.useEffect(() => {
412
+ if (!running) {
413
+ return;
414
+ }
415
+ const t = setInterval(() => void load(), 3e3);
416
+ return () => clearInterval(t);
417
+ }, [running, load]);
418
+ const clearStuck = async () => {
419
+ setClearing(true);
420
+ try {
421
+ const res = await fetch("/admin/printful/sync/clear", {
422
+ method: "POST",
423
+ credentials: "include",
424
+ headers: { "content-type": "application/json" },
425
+ body: JSON.stringify({ confirm: "clear-stuck-sync" })
426
+ });
427
+ const body = await res.json().catch(() => ({}));
428
+ if (res.ok) {
429
+ ui.toast.success("Cleared the stuck sync", {
430
+ description: body.error_message
431
+ });
432
+ } else if (res.status === 409) {
433
+ ui.toast.info("The sync is still alive", {
434
+ description: "It checked in while you were looking. Nothing cleared."
435
+ });
436
+ } else {
437
+ ui.toast.error("Could not clear the sync", { description: body.message });
438
+ }
439
+ await load();
440
+ } catch (err) {
441
+ ui.toast.error("Could not clear the sync", {
442
+ description: err instanceof Error ? err.message : String(err)
443
+ });
444
+ } finally {
445
+ setClearing(false);
446
+ }
447
+ };
448
+ const stuck = looksStuck(latest, (health == null ? void 0 : health.window) ?? 60);
449
+ const silentFor = minutesSince((latest == null ? void 0 : latest.heartbeat_at) ?? (latest == null ? void 0 : latest.started_at));
450
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
451
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
452
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Printful" }),
453
+ /* @__PURE__ */ jsxRuntime.jsx(
454
+ ui.Button,
455
+ {
456
+ size: "small",
457
+ variant: "secondary",
458
+ onClick: () => void load(),
459
+ disabled: loading,
460
+ children: "Refresh"
461
+ }
462
+ )
463
+ ] }),
464
+ stuck && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
465
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "This sync looks stuck" }),
466
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
467
+ "It has not checked in",
468
+ silentFor != null ? ` for ${silentFor} minutes` : "",
469
+ ". Until it is cleared or reclaimed, no other sync can start."
470
+ ] }),
471
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt, { children: [
472
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "danger", disabled: clearing, children: "Clear it" }) }),
473
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Content, { children: [
474
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Header, { children: [
475
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Title, { children: "Clear the stuck sync?" }),
476
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Description, { children: "This marks the run as failed so a new sync can start. If it turns out to be alive, nothing is cleared. Products already imported are not touched." })
477
+ ] }),
478
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Prompt.Footer, { children: [
479
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Cancel, { children: "Cancel" }),
480
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Prompt.Action, { onClick: () => void clearStuck(), children: "Clear" })
481
+ ] })
482
+ ] })
483
+ ] })
484
+ ] }),
485
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
486
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Sales" }),
487
+ stats ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
488
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
489
+ "Paid orders: ",
490
+ ((_a = stats.total_paid_orders) == null ? void 0 : _a.value) ?? "—"
491
+ ] }),
492
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
493
+ "Profit: ",
494
+ ((_b = stats.profit) == null ? void 0 : _b.value) ?? "—",
495
+ " ",
496
+ ((_c = stats.currency) == null ? void 0 : _c.toUpperCase()) ?? ""
497
+ ] })
498
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "Printful reported no statistics." })
499
+ ] }),
500
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
501
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Webhooks" }),
502
+ health && !health.ever_received && // The state a misconfigured webhook leaves behind, and the reason
503
+ // this panel exists: a store can look fine and never hear back.
504
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: health.secret_set ? "A secret is configured, but nothing has arrived yet." : "No webhook secret is configured." }),
505
+ health && health.ever_received && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
506
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
507
+ "Last event:",
508
+ " ",
509
+ health.last_event_at ? new Date(health.last_event_at).toLocaleString() : "—"
510
+ ] }),
511
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
512
+ health.processed,
513
+ " processed · ",
514
+ health.deferred,
515
+ " waiting ·",
516
+ " ",
517
+ health.failed,
518
+ " failed"
519
+ ] })
520
+ ] })
521
+ ] }),
522
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
523
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Design parameters" }),
524
+ designs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No products carry design parameters yet. Run a sync." }) : designs.map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
525
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
526
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: d.title }),
527
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: CLASS_COLOUR[d.product_class] ?? "grey", children: CLASS_LABEL[d.product_class] ?? d.product_class })
528
+ ] }),
529
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
530
+ d.technique ?? "—",
531
+ d.brand ? ` · ${d.brand}${d.model ? ` ${d.model}` : ""}` : "",
532
+ d.material ? ` · ${d.material}` : ""
533
+ ] }),
534
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
535
+ "Design goes on: ",
536
+ d.core_placements.join(", ") || "—",
537
+ d.placements.length > d.core_placements.length ? ` (${d.placements.length - d.core_placements.length} more available)` : ""
538
+ ] }),
539
+ d.colors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-wrap", children: [
540
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Base colours:" }),
541
+ d.colors.slice(0, 12).map((c) => /* @__PURE__ */ jsxRuntime.jsx(
542
+ "span",
543
+ {
544
+ title: `${c.name}${c.hex ? ` ${c.hex}` : ""}`,
545
+ className: "inline-block h-4 w-4 rounded border border-ui-border-base",
546
+ style: c.hex ? { backgroundColor: c.hex } : void 0
547
+ },
548
+ c.name
549
+ )),
550
+ d.colors.length > 12 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
551
+ "+",
552
+ d.colors.length - 12
553
+ ] })
554
+ ] }),
555
+ d.sizes.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
556
+ d.product_class === "print_media" ? "Sizes" : "Sizes",
557
+ ":",
558
+ " ",
559
+ d.sizes.slice(0, 8).join(", "),
560
+ d.sizes.length > 8 ? ` +${d.sizes.length - 8}` : ""
561
+ ] })
562
+ ] }, d.product_id))
563
+ ] }),
564
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
565
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Bundles" }),
566
+ bundles.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No bundles yet. Add printful_bundle_members to a variant's metadata to make it a bundle." }) : bundles.map((b) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
567
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
568
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: b.title }),
569
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: b.available ? "green" : "red", children: b.available ? "all members in stock" : "member unavailable" }),
570
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: b.status === "published" ? "green" : "grey", children: b.status }),
571
+ b.missing_count > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "orange", children: [
572
+ b.missing_count,
573
+ " member",
574
+ b.missing_count === 1 ? "" : "s",
575
+ " not found"
576
+ ] })
577
+ ] }),
578
+ b.members.map((m) => /* @__PURE__ */ jsxRuntime.jsxs(
579
+ ui.Text,
580
+ {
581
+ size: "small",
582
+ className: "text-ui-fg-subtle pl-3",
583
+ children: [
584
+ m.quantity,
585
+ "× ",
586
+ m.product_title ?? "—",
587
+ m.title ? ` · ${m.title}` : "",
588
+ m.status === "missing" ? " — variant not found" : m.status === "active" || m.status === "unknown" ? "" : ` — ${m.status.replace(/_/g, " ")}`
589
+ ]
590
+ },
591
+ m.variant_id
592
+ ))
593
+ ] }, b.variant_id))
594
+ ] }),
595
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-3", children: [
596
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Mockup prompts" }),
597
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: "Paste these into an image model. Printful’s own generator renders a product on a plain background — right for a catalogue thumbnail, not for an editorial mockup." }),
598
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
599
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Select, { value: style, onValueChange: setStyle, children: [
600
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Trigger, { className: "w-40", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Value, { placeholder: "Style" }) }),
601
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Content, { children: styles.map((s) => /* @__PURE__ */ jsxRuntime.jsx(ui.Select.Item, { value: s.id, children: s.label }, s.id)) })
602
+ ] }),
603
+ /* @__PURE__ */ jsxRuntime.jsx(
604
+ ui.Input,
605
+ {
606
+ className: "flex-1 min-w-64",
607
+ placeholder: "What is the artwork? (optional)",
608
+ value: artwork,
609
+ onChange: (e) => setArtwork(e.target.value)
610
+ }
611
+ )
612
+ ] }),
613
+ promptProducts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No prompts yet. Run a sync so products carry design parameters." }) : promptProducts.map((p) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
614
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: p.title }),
615
+ p.prompts.map((prompt, i) => /* @__PURE__ */ jsxRuntime.jsxs(
616
+ "div",
617
+ {
618
+ className: "flex items-start gap-2 rounded-md border border-ui-border-base p-2",
619
+ children: [
620
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 flex-1", children: [
621
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: prompt.label }),
622
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: prompt.text })
623
+ ] }),
624
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Copy, { content: prompt.text })
625
+ ]
626
+ },
627
+ `${p.product_id}-${i}`
628
+ ))
629
+ ] }, p.product_id))
630
+ ] }),
631
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-2", children: [
632
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: "Recent syncs" }),
633
+ syncs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: loading ? "Loading…" : "No syncs yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
634
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
635
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Started" }),
636
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
637
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Result" })
638
+ ] }) }),
639
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: syncs.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
640
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: row.started_at ? new Date(row.started_at).toLocaleString() : "—" }),
641
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: STATUS_COLOUR[row.status] ?? "grey", children: row.status }) }),
642
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: row.status === "running" ? `${row.products_processed ?? 0} of ${row.products_total ?? "?"}` : `${row.products_created ?? 0} created · ${row.products_updated ?? 0} updated · ${row.products_failed ?? 0} failed` })
643
+ ] }, row.id)) })
644
+ ] })
645
+ ] })
646
+ ] });
647
+ };
648
+ const config = adminSdk.defineRouteConfig({
649
+ label: "Printful"
650
+ });
310
651
  const i18nTranslations0 = {};
311
652
  const widgetModule = { widgets: [
312
653
  {
@@ -321,10 +662,25 @@ const widgetModule = { widgets: [
321
662
  }
322
663
  ] };
323
664
  const routeModule = {
324
- routes: []
665
+ routes: [
666
+ {
667
+ Component: PrintfulPage,
668
+ path: "/printful",
669
+ handle: { label: config.label, translationNs: config.translationNs }
670
+ }
671
+ ]
325
672
  };
326
673
  const menuItemModule = {
327
- menuItems: []
674
+ menuItems: [
675
+ {
676
+ label: config.label,
677
+ icon: void 0,
678
+ path: "/printful",
679
+ nested: void 0,
680
+ rank: void 0,
681
+ translationNs: void 0
682
+ }
683
+ ]
328
684
  };
329
685
  const formModule = { customFields: {} };
330
686
  const displayModule = {