@fluid-app/portal-sdk 0.1.405 → 0.1.407

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.
@@ -202,6 +202,7 @@ function mapToPortalTenantOrder(raw) {
202
202
  token: raw.token ?? "",
203
203
  order_number: typeof rawOrderNumber === "string" && rawOrderNumber.length > 0 ? rawOrderNumber : null,
204
204
  status: raw.status ?? "pending",
205
+ fulfillment_status: typeof raw.fulfillment_status === "string" ? raw.fulfillment_status : "unfulfilled",
205
206
  subtotal,
206
207
  subtotal_in_currency: raw.subtotal_in_currency ?? subtotal,
207
208
  discount,
@@ -358,127 +359,226 @@ function OrdersTranslationBridge({ children }) {
358
359
  });
359
360
  }
360
361
  //#endregion
361
- //#region src/screens/orders/portal-orders-list-helpers.ts
362
- const STATUS_LOOKUP = {
363
- fulfilled: {
364
- kind: "fulfilled",
365
- labelKey: "status_fulfilled"
366
- },
367
- completed: {
368
- kind: "fulfilled",
369
- labelKey: "status_fulfilled"
370
- },
371
- partially_fulfilled: {
372
- kind: "preparing",
373
- labelKey: "status_preparing"
374
- },
375
- in_progress: {
376
- kind: "preparing",
377
- labelKey: "status_preparing"
378
- },
379
- on_hold: {
380
- kind: "preparing",
381
- labelKey: "status_preparing"
382
- },
383
- scheduled: {
384
- kind: "preparing",
385
- labelKey: "status_preparing"
386
- },
387
- preparing: {
388
- kind: "preparing",
389
- labelKey: "status_preparing"
390
- },
391
- shipped: {
392
- kind: "shipped",
393
- labelKey: "status_shipped"
394
- },
395
- cancelled: { kind: "cancelled" },
396
- returned: { kind: "cancelled" },
397
- refunded: { kind: "cancelled" },
398
- pending: {
399
- kind: "pending",
400
- labelKey: "status_processing"
401
- },
402
- unfulfilled: {
403
- kind: "pending",
404
- labelKey: "status_processing"
405
- },
406
- processing: {
407
- kind: "pending",
408
- labelKey: "status_processing"
409
- }
362
+ //#region ../../orders/ui/src/components/status-badge.tsx
363
+ const colorStyles = {
364
+ green: "bg-green-500/15 text-foreground",
365
+ yellow: "bg-yellow-500/15 text-foreground",
366
+ red: "bg-red-500/15 text-foreground",
367
+ blue: "bg-blue-500/15 text-foreground",
368
+ orange: "bg-orange-500/15 text-foreground",
369
+ gray: "bg-foreground/10 text-foreground"
410
370
  };
411
- /**
412
- * Map a raw status string (typically `fulfillment_status`) from the BFF
413
- * onto our internal status vocabulary (`StatusKind`). Aliases are folded
414
- * together (`unfulfilled` → `pending`, `in_progress`/`scheduled` →
415
- * `preparing`, etc.). Unknown statuses pass through with `kind: "pending"`
416
- * and the raw label capitalized — never synthesize a colored pill that
417
- * pretends to be real status data.
418
- */
419
- function mapStatus(orderStatus, t) {
420
- const entry = STATUS_LOOKUP[(orderStatus || "").toLowerCase()];
421
- const rawLabel = orderStatus ? orderStatus.charAt(0).toUpperCase() + orderStatus.slice(1) : "—";
422
- if (!entry) return {
423
- kind: "pending",
424
- label: rawLabel
425
- };
426
- return {
427
- kind: entry.kind,
428
- label: entry.labelKey ? t(entry.labelKey) : rawLabel
429
- };
430
- }
431
- const STATUS_STYLES = {
432
- fulfilled: {
433
- bg: "bg-emerald-50 dark:bg-emerald-950/40",
434
- text: "text-emerald-700 dark:text-emerald-400",
435
- dot: "bg-emerald-500"
436
- },
437
- shipped: {
438
- bg: "bg-sky-50 dark:bg-sky-950/40",
439
- text: "text-sky-700 dark:text-sky-400",
440
- dot: "bg-sky-500"
441
- },
442
- preparing: {
443
- bg: "bg-amber-50 dark:bg-amber-950/40",
444
- text: "text-amber-700 dark:text-amber-400",
445
- dot: "bg-amber-500"
446
- },
447
- pending: {
448
- bg: "bg-muted",
449
- text: "text-muted-foreground",
450
- dot: "bg-muted-foreground"
451
- },
452
- cancelled: {
453
- bg: "bg-destructive/10",
454
- text: "text-destructive",
455
- dot: "bg-destructive"
456
- }
371
+ const dotColorStyles = {
372
+ green: "bg-green-500",
373
+ yellow: "bg-yellow-500",
374
+ red: "bg-red-500",
375
+ blue: "bg-blue-500",
376
+ orange: "bg-orange-500",
377
+ gray: "bg-gray-500"
457
378
  };
458
- const CANCELLED_FAMILY = new Set([
379
+ const sizeStyles = {
380
+ xs: "px-1.5 py-0.5 text-[10px]",
381
+ sm: "px-2 py-0.5 text-xs",
382
+ md: "px-2.5 py-1 text-sm"
383
+ };
384
+ function StatusBadge({ color, dot, size = "md", className, children }) {
385
+ return /* @__PURE__ */ jsxs("span", {
386
+ className: cn("inline-flex items-center gap-1 rounded-full font-medium whitespace-nowrap capitalize", colorStyles[color], sizeStyles[size], className),
387
+ children: [dot && /* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", dotColorStyles[color]) }), children]
388
+ });
389
+ }
390
+ //#endregion
391
+ //#region ../../orders/ui/src/lib/format.ts
392
+ function startCase(str) {
393
+ if (!str) return "";
394
+ return str.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
395
+ }
396
+ //#endregion
397
+ //#region ../../orders/ui/src/lib/translate-status.ts
398
+ /** Status values that have a `status_*` translation key in the orders dictionary. */
399
+ const translatableStatuses = new Set([
459
400
  "cancelled",
401
+ "completed",
402
+ "fulfilled",
403
+ "pending",
404
+ "preparing",
405
+ "processing",
460
406
  "returned",
461
- "refunded"
407
+ "shipped"
462
408
  ]);
463
409
  /**
410
+ * Translate an order status using the orders `t()` function.
411
+ * Falls back to `startCase` for statuses without a dedicated translation key.
412
+ */
413
+ function translateStatus(t, status) {
414
+ if (translatableStatuses.has(status)) return t(`status_${status}`);
415
+ return startCase(status);
416
+ }
417
+ //#endregion
418
+ //#region ../../orders/ui/src/lib/effective-order-date.ts
419
+ function effectiveOrderDate(order) {
420
+ return order.sale_date ?? order.created_at;
421
+ }
422
+ //#endregion
423
+ //#region ../../orders/ui/src/lib/fulfillment-status.ts
424
+ /**
425
+ * Canonical fulfillment-status → badge color mapping.
426
+ *
427
+ * This is a deliberate mirror of fluid-admin's fulfillment color config
428
+ * (`COLOR_CONFIG.fulfillment` + `getDefaultColorForUnknownStatus` in
429
+ * `apps/fluid-admin/components/Orders/utils/formatters.ts`) so the
430
+ * customer-facing portal renders order status tags with the SAME label
431
+ * and color as the admin — admin is the source of truth (CURRENT-1496).
432
+ *
433
+ * The `Commerce::Order#fulfillment_status` enum values are:
434
+ * `unfulfilled, in_progress, on_hold, partially_fulfilled, scheduled,
435
+ * fulfilled`.
436
+ */
437
+ const FULFILLMENT_STATUS_COLORS = {
438
+ fulfilled: "gray",
439
+ unfulfilled: "yellow",
440
+ in_progress: "yellow",
441
+ on_hold: "orange",
442
+ partially_fulfilled: "yellow",
443
+ scheduled: "blue",
444
+ processing: "yellow"
445
+ };
446
+ /**
447
+ * Cancelled-family lifecycle values. These come from `order_status` (not
448
+ * `fulfillment_status`), and the portal orders list surfaces them on the
449
+ * same pill. Admin renders these red, so we match.
450
+ */
451
+ const CANCELLED_FAMILY_COLORS = {
452
+ cancelled: "red",
453
+ returned: "red",
454
+ refunded: "red"
455
+ };
456
+ /**
457
+ * Fallback for statuses not in the explicit maps. Mirrors admin's
458
+ * `getDefaultColorForUnknownStatus` for the fulfillment type so unknown
459
+ * values still read consistently with admin rather than defaulting to a
460
+ * meaningless grey.
461
+ */
462
+ function defaultColor(status) {
463
+ if (status.includes("fulfilled") || status.includes("complete")) return "gray";
464
+ if (status.includes("hold")) return "orange";
465
+ return "yellow";
466
+ }
467
+ /**
468
+ * Cancelled-family lifecycle values, derived from the color map so there is
469
+ * a single source of truth. Membership is checked case-insensitively.
470
+ */
471
+ const CANCELLED_FAMILY = new Set(Object.keys(CANCELLED_FAMILY_COLORS));
472
+ /**
473
+ * Resolve which status string should drive an order's pill. Cancelled-family
474
+ * lifecycle values (`cancelled` / `returned` / `refunded`) come from the
475
+ * order's lifecycle status; everything else reads off `fulfillment_status`
476
+ * (the source of truth per `Commerce::Order#fulfillment_status`).
477
+ *
478
+ * Both the orders list and the order detail page use this so a cancelled
479
+ * order renders red "Cancelled" instead of a misleading yellow "Unfulfilled"
480
+ * — `fulfillment_status` has no cancelled-family value, so a cancelled order
481
+ * typically carries `fulfillment_status: "unfulfilled"` (CURRENT-1496).
482
+ *
483
+ * lifecycleStatus - the order's lifecycle status (`order_status` on a list
484
+ * order, `status` on a portal-tenant detail order).
485
+ * fulfillmentStatus - the order's `fulfillment_status`.
486
+ */
487
+ function resolveOrderStatus(lifecycleStatus, fulfillmentStatus) {
488
+ if (lifecycleStatus && CANCELLED_FAMILY.has(lifecycleStatus.toLowerCase())) return lifecycleStatus;
489
+ return fulfillmentStatus ?? lifecycleStatus ?? "";
490
+ }
491
+ /**
492
+ * Resolve the badge color for an order status tag, matching admin exactly
493
+ * for every `fulfillment_status` enum value and the cancelled family.
494
+ */
495
+ function fulfillmentStatusColor(status) {
496
+ if (!status) return "gray";
497
+ const normalized = status.toLowerCase();
498
+ return FULFILLMENT_STATUS_COLORS[normalized] ?? CANCELLED_FAMILY_COLORS[normalized] ?? defaultColor(normalized);
499
+ }
500
+ //#endregion
501
+ //#region ../../orders/ui/src/components/fulfillment-status-badge.tsx
502
+ /**
503
+ * Renders an order status tag whose label and color match the fluid-admin
504
+ * fulfillment pill exactly (CURRENT-1496). Admin is the source of truth:
505
+ * `fulfilled` is grey, `unfulfilled`/`in_progress`/`partially_fulfilled`
506
+ * are yellow, `on_hold` is orange, and `scheduled` is blue. Labels come
507
+ * from `translateStatus`, which yields the same start-cased text admin
508
+ * shows (`getStatusDisplayText`) while preserving the translated
509
+ * `status_fulfilled` copy where available.
510
+ */
511
+ function FulfillmentStatusBadge({ status, className }) {
512
+ const { t } = useOrdersTranslation();
513
+ return /* @__PURE__ */ jsx(StatusBadge, {
514
+ color: fulfillmentStatusColor(status),
515
+ dot: true,
516
+ size: "sm",
517
+ className,
518
+ children: translateStatus(t, status)
519
+ });
520
+ }
521
+ //#endregion
522
+ //#region ../../orders/ui/src/components/static-map.tsx
523
+ function StaticMap({ apiKey, address, options, alt, className }) {
524
+ const { t } = useOrdersTranslation();
525
+ const [errored, setErrored] = useState(false);
526
+ const url = buildStaticMapUrl(apiKey, address, options);
527
+ if (!url || errored) return null;
528
+ return /* @__PURE__ */ jsx("img", {
529
+ src: url,
530
+ alt: alt ?? t("shipping_address_map"),
531
+ width: options?.width ?? 600,
532
+ height: options?.height ?? 300,
533
+ className: className ?? "border-border w-full rounded border object-cover shadow-xs",
534
+ onError: () => setErrored(true)
535
+ });
536
+ }
537
+ //#endregion
538
+ //#region src/screens/orders/portal-orders-list-helpers.ts
539
+ const STATUS_KIND_LOOKUP = {
540
+ fulfilled: "fulfilled",
541
+ completed: "fulfilled",
542
+ shipped: "shipped",
543
+ partially_fulfilled: "preparing",
544
+ in_progress: "preparing",
545
+ on_hold: "preparing",
546
+ scheduled: "preparing",
547
+ preparing: "preparing",
548
+ cancelled: "cancelled",
549
+ returned: "cancelled",
550
+ refunded: "cancelled",
551
+ pending: "pending",
552
+ unfulfilled: "pending",
553
+ processing: "pending"
554
+ };
555
+ /**
556
+ * Classify a raw status string onto the internal `StatusKind` vocabulary
557
+ * used to drive the filter tabs. This is intentionally coarser than the
558
+ * rendered pill — the pill's label and color come from
559
+ * `FulfillmentStatusBadge`. Unknown statuses fall back to `pending`.
560
+ */
561
+ function statusKind(orderStatus) {
562
+ return STATUS_KIND_LOOKUP[(orderStatus || "").toLowerCase()] ?? "pending";
563
+ }
564
+ /**
464
565
  * Pick the right status string to drive the pill / filter for a list order.
465
566
  * Cancelled-family lifecycle states come from `order_status`; everything
466
567
  * else is read off `fulfillment_status` (the BFF source-of-truth per the
467
- * `Commerce::Order#fulfillment_status` enum).
568
+ * `Commerce::Order#fulfillment_status` enum). Delegates to the shared
569
+ * `resolveOrderStatus` so the list and the order detail page stay in lockstep.
468
570
  */
469
571
  function statusForOrder(order) {
470
- if (CANCELLED_FAMILY.has(order.order_status)) return order.order_status;
471
- return order.fulfillment_status ?? order.order_status;
572
+ return resolveOrderStatus(order.order_status, order.fulfillment_status);
472
573
  }
473
574
  function filterPredicate(filter, order) {
474
575
  if (filter === "all") return true;
475
576
  if (filter === "subscription") return order.subscription != null;
476
- const status = mapStatus(statusForOrder(order), identity).kind;
477
- if (filter === "fulfilled") return status === "fulfilled" || status === "shipped";
478
- if (filter === "preparing") return status === "preparing" || status === "pending";
577
+ const kind = statusKind(statusForOrder(order));
578
+ if (filter === "fulfilled") return kind === "fulfilled" || kind === "shipped";
579
+ if (filter === "preparing") return kind === "preparing" || kind === "pending";
479
580
  return true;
480
581
  }
481
- const identity = ((k) => k);
482
582
  function countByFilter(list) {
483
583
  return {
484
584
  all: list.length,
@@ -727,7 +827,6 @@ function FilterPills({ active, counts, onChange, t }) {
727
827
  });
728
828
  }
729
829
  function OrderRow({ order, onClick, t }) {
730
- const status = mapStatus(statusForOrder(order), t);
731
830
  const dateLabel = formatFriendlyDate(orderDisplayDate(order), t);
732
831
  const total = formatOrderTotal(order);
733
832
  const titleText = order.first_item?.title || `${t("order_number")}${order.order_number}`;
@@ -751,7 +850,7 @@ function OrderRow({ order, onClick, t }) {
751
850
  className: "text-muted-foreground font-medium",
752
851
  children: titleSuffix
753
852
  })]
754
- }), /* @__PURE__ */ jsx(StatusPill, { status })]
853
+ }), /* @__PURE__ */ jsx(FulfillmentStatusBadge, { status: statusForOrder(order) })]
755
854
  }),
756
855
  /* @__PURE__ */ jsxs("div", {
757
856
  className: "text-muted-foreground mt-1 truncate text-xs",
@@ -807,13 +906,6 @@ function SubscriptionBadge({ t }) {
807
906
  })
808
907
  });
809
908
  }
810
- function StatusPill({ status }) {
811
- const styles = STATUS_STYLES[status.kind];
812
- return /* @__PURE__ */ jsxs("span", {
813
- className: `inline-flex shrink-0 items-center gap-1 rounded-md px-2 py-0.5 text-xs font-semibold ${styles.bg} ${styles.text}`,
814
- children: [/* @__PURE__ */ jsx("span", { className: `size-1.5 rounded-full ${styles.dot}` }), status.label]
815
- });
816
- }
817
909
  function OrdersListSkeleton() {
818
910
  return /* @__PURE__ */ jsx(Fragment$1, { children: Array(4).fill(0).map((_, i) => /* @__PURE__ */ jsxs("div", {
819
911
  className: `flex items-center gap-3 px-4 py-3.5 ${i < 3 ? "border-border border-b" : ""}`,
@@ -956,107 +1048,6 @@ function MaskedPaymentLabel({ label, className }) {
956
1048
  });
957
1049
  }
958
1050
  //#endregion
959
- //#region ../../orders/ui/src/components/status-badge.tsx
960
- const colorStyles = {
961
- green: "bg-green-500/15 text-foreground",
962
- yellow: "bg-yellow-500/15 text-foreground",
963
- red: "bg-red-500/15 text-foreground",
964
- blue: "bg-blue-500/15 text-foreground",
965
- gray: "bg-foreground/10 text-foreground"
966
- };
967
- const dotColorStyles = {
968
- green: "bg-green-500",
969
- yellow: "bg-yellow-500",
970
- red: "bg-red-500",
971
- blue: "bg-blue-500",
972
- gray: "bg-gray-500"
973
- };
974
- const sizeStyles = {
975
- xs: "px-1.5 py-0.5 text-[10px]",
976
- sm: "px-2 py-0.5 text-xs",
977
- md: "px-2.5 py-1 text-sm"
978
- };
979
- function StatusBadge({ color, dot, size = "md", className, children }) {
980
- return /* @__PURE__ */ jsxs("span", {
981
- className: cn("inline-flex items-center gap-1 rounded-full font-medium whitespace-nowrap capitalize", colorStyles[color], sizeStyles[size], className),
982
- children: [dot && /* @__PURE__ */ jsx("span", { className: cn("h-1.5 w-1.5 rounded-full", dotColorStyles[color]) }), children]
983
- });
984
- }
985
- //#endregion
986
- //#region ../../orders/ui/src/lib/format.ts
987
- function startCase(str) {
988
- if (!str) return "";
989
- return str.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
990
- }
991
- //#endregion
992
- //#region ../../orders/ui/src/lib/translate-status.ts
993
- /** Status values that have a `status_*` translation key in the orders dictionary. */
994
- const translatableStatuses = new Set([
995
- "cancelled",
996
- "completed",
997
- "fulfilled",
998
- "pending",
999
- "preparing",
1000
- "processing",
1001
- "returned",
1002
- "shipped"
1003
- ]);
1004
- /**
1005
- * Translate an order status using the orders `t()` function.
1006
- * Falls back to `startCase` for statuses without a dedicated translation key.
1007
- */
1008
- function translateStatus(t, status) {
1009
- if (translatableStatuses.has(status)) return t(`status_${status}`);
1010
- return startCase(status);
1011
- }
1012
- //#endregion
1013
- //#region ../../orders/ui/src/components/order-status-badge.tsx
1014
- const statusColorMap = {
1015
- paid: "green",
1016
- fulfilled: "green",
1017
- delivered: "green",
1018
- complete: "green",
1019
- pending: "yellow",
1020
- unfulfilled: "yellow",
1021
- partially_fulfilled: "yellow",
1022
- processing: "yellow",
1023
- refunded: "red",
1024
- cancelled: "red",
1025
- failed: "red",
1026
- voided: "red"
1027
- };
1028
- function OrderStatusBadge({ status, className }) {
1029
- const { t } = useOrdersTranslation();
1030
- return /* @__PURE__ */ jsx(StatusBadge, {
1031
- color: statusColorMap[status] ?? "gray",
1032
- dot: true,
1033
- size: "sm",
1034
- className,
1035
- children: translateStatus(t, status)
1036
- });
1037
- }
1038
- //#endregion
1039
- //#region ../../orders/ui/src/components/static-map.tsx
1040
- function StaticMap({ apiKey, address, options, alt, className }) {
1041
- const { t } = useOrdersTranslation();
1042
- const [errored, setErrored] = useState(false);
1043
- const url = buildStaticMapUrl(apiKey, address, options);
1044
- if (!url || errored) return null;
1045
- return /* @__PURE__ */ jsx("img", {
1046
- src: url,
1047
- alt: alt ?? t("shipping_address_map"),
1048
- width: options?.width ?? 600,
1049
- height: options?.height ?? 300,
1050
- className: className ?? "border-border w-full rounded border object-cover shadow-xs",
1051
- onError: () => setErrored(true)
1052
- });
1053
- }
1054
- //#endregion
1055
- //#region ../../orders/ui/src/lib/effective-order-date.ts
1056
- function effectiveOrderDate(order) {
1057
- return order.sale_date ?? order.created_at;
1058
- }
1059
- //#endregion
1060
1051
  //#region ../../orders/ui/src/components/portal-order-detail.tsx
1061
1052
  /** Capitalize the first letter for display in headings / sentence-leading copy. */
1062
1053
  const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1);
@@ -1440,7 +1431,7 @@ function DetailsSection({ order, googleMapsApiKey }) {
1440
1431
  children: [/* @__PURE__ */ jsx("span", {
1441
1432
  className: "text-muted-foreground text-sm",
1442
1433
  children: t("status_label")
1443
- }), /* @__PURE__ */ jsx(OrderStatusBadge, { status: order.status })]
1434
+ }), /* @__PURE__ */ jsx(FulfillmentStatusBadge, { status: resolveOrderStatus(order.status, order.fulfillment_status) })]
1444
1435
  })
1445
1436
  })
1446
1437
  }),
@@ -1652,4 +1643,4 @@ const ordersScreenPropertySchema = {
1652
1643
  //#endregion
1653
1644
  export { ordersScreenPropertySchema as n, OrdersScreen as t };
1654
1645
 
1655
- //# sourceMappingURL=OrdersScreen-BUTD4UlS.mjs.map
1646
+ //# sourceMappingURL=OrdersScreen-Cy2M0ZKk.mjs.map