medusa-analytics 0.0.13 → 0.0.14

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.
@@ -48,7 +48,7 @@ const ORDER_CHART_STATUSES = [
48
48
  { key: "cancelled", label: "Cancelled", color: STATUS_COLORS.cancelled },
49
49
  { key: "delivered", label: "Delivered", color: STATUS_COLORS.delivered }
50
50
  ];
51
- function formatCurrency(value) {
51
+ function formatCurrency$1(value) {
52
52
  return new Intl.NumberFormat("en-IN", {
53
53
  style: "currency",
54
54
  currency: "INR",
@@ -56,7 +56,7 @@ function formatCurrency(value) {
56
56
  maximumFractionDigits: 0
57
57
  }).format(value);
58
58
  }
59
- const SUMMARY_PERIODS = [
59
+ const SUMMARY_PERIODS$1 = [
60
60
  { value: "all", label: "All time" },
61
61
  { value: "0", label: "Today's orders" },
62
62
  { value: "7", label: "Last 7 days" },
@@ -150,7 +150,7 @@ function OrdersDashboard() {
150
150
  value: filter,
151
151
  onChange: (e) => setFilter(e.target.value),
152
152
  className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
153
- children: SUMMARY_PERIODS.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p.value, children: p.label }, p.value))
153
+ children: SUMMARY_PERIODS$1.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p.value, children: p.label }, p.value))
154
154
  }
155
155
  )
156
156
  ] }),
@@ -173,11 +173,11 @@ function OrdersDashboard() {
173
173
  ] }),
174
174
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
175
175
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Total revenue" }),
176
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency(data.totalRevenue) })
176
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency$1(data.totalRevenue) })
177
177
  ] }),
178
178
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
179
179
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Average order value" }),
180
- /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency(data.aov) })
180
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency$1(data.aov) })
181
181
  ] }),
182
182
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
183
183
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Orders today" }),
@@ -386,7 +386,7 @@ const COUNT_DAY_PRESETS = [
386
386
  { value: "30", label: "Last 30 days" },
387
387
  { value: "90", label: "Last 90 days" }
388
388
  ];
389
- const GRAPH_PERIODS = [
389
+ const GRAPH_PERIODS$1 = [
390
390
  { value: "one_week", label: "One week" },
391
391
  { value: "one_month", label: "One month" },
392
392
  { value: "one_year", label: "One year" }
@@ -527,7 +527,7 @@ function CustomersDashboard() {
527
527
  value: graphPeriod,
528
528
  onChange: (e) => setGraphPeriod(e.target.value),
529
529
  className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
530
- children: GRAPH_PERIODS.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p.value, children: p.label }, p.value))
530
+ children: GRAPH_PERIODS$1.map((p) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: p.value, children: p.label }, p.value))
531
531
  }
532
532
  )
533
533
  ] })
@@ -604,9 +604,593 @@ function CustomersDashboard() {
604
604
  ] })
605
605
  ] });
606
606
  }
607
+ const SUMMARY_PERIODS = [
608
+ { value: "all", label: "All time" },
609
+ { value: "0", label: "Today" },
610
+ { value: "7", label: "Last 7 days" },
611
+ { value: "30", label: "Last 30 days" },
612
+ { value: "90", label: "Last 90 days" }
613
+ ];
614
+ const GRAPH_PERIODS = [
615
+ { value: "one_week", label: "One week" },
616
+ { value: "one_month", label: "One month" },
617
+ { value: "one_year", label: "One year" }
618
+ ];
619
+ const TOP_SELLER_PERIODS = [
620
+ { value: "week", label: "Week" },
621
+ { value: "month", label: "Month" },
622
+ { value: "year", label: "Year" },
623
+ { value: "all", label: "All time" }
624
+ ];
625
+ const SALES_COLOR = "var(--medusa-color-ui-fg-interactive)";
626
+ const VIEWS_COLOR = "#8B5CF6";
627
+ const REVENUE_COLOR = "#F59E0B";
628
+ function formatCurrency(value) {
629
+ return new Intl.NumberFormat("en-IN", {
630
+ style: "currency",
631
+ currency: "INR",
632
+ minimumFractionDigits: 0,
633
+ maximumFractionDigits: 0
634
+ }).format(value);
635
+ }
636
+ function formatRatio(value) {
637
+ if (value === null || Number.isNaN(value)) return "N/A";
638
+ return `${value.toFixed(2)}x`;
639
+ }
640
+ function truncateLabel(value, maxLength = 22) {
641
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
642
+ }
643
+ function buildAnalyticsUrl(path, params) {
644
+ const search = new URLSearchParams();
645
+ for (const [key, value] of Object.entries(params)) {
646
+ if (value) {
647
+ search.set(key, value);
648
+ }
649
+ }
650
+ const query = search.toString();
651
+ return query ? `${path}?${query}` : path;
652
+ }
653
+ function TrendTooltip({
654
+ active,
655
+ payload,
656
+ label
657
+ }) {
658
+ var _a, _b, _c, _d;
659
+ if (!active || !(payload == null ? void 0 : payload.length)) return null;
660
+ const row = (_a = payload[0]) == null ? void 0 : _a.payload;
661
+ const displayLabel = (row == null ? void 0 : row.label) ?? (label ? new Date(label).toLocaleDateString() : "");
662
+ const unitsSold = ((_b = payload.find((entry) => entry.name === "Units sold")) == null ? void 0 : _b.value) ?? 0;
663
+ const views = ((_c = payload.find((entry) => entry.name === "Views")) == null ? void 0 : _c.value) ?? 0;
664
+ const revenue = ((_d = payload.find((entry) => entry.name === "Revenue")) == null ? void 0 : _d.value) ?? 0;
665
+ return /* @__PURE__ */ jsxRuntime.jsxs(
666
+ "div",
667
+ {
668
+ style: {
669
+ backgroundColor: "rgb(255, 255, 255)",
670
+ color: "#111827",
671
+ border: "1px solid rgb(229, 231, 235)",
672
+ borderRadius: "8px",
673
+ padding: "12px 16px",
674
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.08)",
675
+ fontSize: "14px",
676
+ lineHeight: 1.5
677
+ },
678
+ children: [
679
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, marginBottom: "6px" }, children: displayLabel }),
680
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
681
+ "Units sold: ",
682
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: Math.floor(unitsSold).toLocaleString() })
683
+ ] }),
684
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
685
+ "Views: ",
686
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: Math.floor(views).toLocaleString() })
687
+ ] }),
688
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
689
+ "Revenue: ",
690
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: formatCurrency(Number(revenue)) })
691
+ ] })
692
+ ]
693
+ }
694
+ );
695
+ }
696
+ function ProductBarTooltip({
697
+ active,
698
+ payload
699
+ }) {
700
+ var _a;
701
+ if (!active || !(payload == null ? void 0 : payload.length)) return null;
702
+ const row = (_a = payload[0]) == null ? void 0 : _a.payload;
703
+ if (!row) return null;
704
+ const unitsSold = "units_sold" in row ? row.units_sold : 0;
705
+ const totalViews = "total_views" in row ? row.total_views : 0;
706
+ const revenue = "revenue" in row ? row.revenue : 0;
707
+ return /* @__PURE__ */ jsxRuntime.jsxs(
708
+ "div",
709
+ {
710
+ style: {
711
+ backgroundColor: "rgb(255, 255, 255)",
712
+ color: "#111827",
713
+ border: "1px solid rgb(229, 231, 235)",
714
+ borderRadius: "8px",
715
+ padding: "12px 16px",
716
+ boxShadow: "0 4px 14px rgba(0, 0, 0, 0.08)",
717
+ fontSize: "14px",
718
+ lineHeight: 1.5
719
+ },
720
+ children: [
721
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 600, marginBottom: "6px" }, children: row.product_title }),
722
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
723
+ "Units sold: ",
724
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: Math.floor(unitsSold).toLocaleString() })
725
+ ] }),
726
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
727
+ "Views: ",
728
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: Math.floor(totalViews).toLocaleString() })
729
+ ] }),
730
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
731
+ "Revenue: ",
732
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: formatCurrency(Number(revenue)) })
733
+ ] })
734
+ ]
735
+ }
736
+ );
737
+ }
738
+ function ProductsDashboard() {
739
+ var _a, _b, _c;
740
+ const [summaryDays, setSummaryDays] = react.useState("all");
741
+ const [graphPeriod, setGraphPeriod] = react.useState("one_week");
742
+ const [topSellerPeriod, setTopSellerPeriod] = react.useState("week");
743
+ const [salesChannelId, setSalesChannelId] = react.useState("all");
744
+ const [salesChannels, setSalesChannels] = react.useState([]);
745
+ const [filtersLoading, setFiltersLoading] = react.useState(true);
746
+ const [filtersError, setFiltersError] = react.useState(null);
747
+ const [summary, setSummary] = react.useState(null);
748
+ const [summaryLoading, setSummaryLoading] = react.useState(true);
749
+ const [summaryError, setSummaryError] = react.useState(null);
750
+ const [overTime, setOverTime] = react.useState(null);
751
+ const [overTimeLoading, setOverTimeLoading] = react.useState(true);
752
+ const [overTimeError, setOverTimeError] = react.useState(null);
753
+ const [topSellers, setTopSellers] = react.useState(null);
754
+ const [topSellersLoading, setTopSellersLoading] = react.useState(true);
755
+ const [topSellersError, setTopSellersError] = react.useState(null);
756
+ const [performance, setPerformance] = react.useState(null);
757
+ const [performanceLoading, setPerformanceLoading] = react.useState(true);
758
+ const [performanceError, setPerformanceError] = react.useState(null);
759
+ react.useEffect(() => {
760
+ let cancelled = false;
761
+ setFiltersLoading(true);
762
+ setFiltersError(null);
763
+ fetch("/admin/analytics/products-filters").then((res) => {
764
+ if (!res.ok) throw new Error(res.statusText);
765
+ return res.json();
766
+ }).then((body) => {
767
+ if (!cancelled) {
768
+ setSalesChannels(body.salesChannels ?? []);
769
+ }
770
+ }).catch((error) => {
771
+ if (!cancelled) {
772
+ setFiltersError(error instanceof Error ? error.message : String(error));
773
+ }
774
+ }).finally(() => {
775
+ if (!cancelled) setFiltersLoading(false);
776
+ });
777
+ return () => {
778
+ cancelled = true;
779
+ };
780
+ }, []);
781
+ react.useEffect(() => {
782
+ let cancelled = false;
783
+ setSummaryLoading(true);
784
+ setSummaryError(null);
785
+ fetch(
786
+ buildAnalyticsUrl("/admin/analytics/products-summary", {
787
+ days: summaryDays,
788
+ sales_channel_id: salesChannelId !== "all" ? salesChannelId : null
789
+ })
790
+ ).then((res) => {
791
+ if (!res.ok) throw new Error(res.statusText);
792
+ return res.json();
793
+ }).then((body) => {
794
+ if (!cancelled) setSummary(body);
795
+ }).catch((error) => {
796
+ if (!cancelled) {
797
+ setSummaryError(error instanceof Error ? error.message : String(error));
798
+ }
799
+ }).finally(() => {
800
+ if (!cancelled) setSummaryLoading(false);
801
+ });
802
+ return () => {
803
+ cancelled = true;
804
+ };
805
+ }, [salesChannelId, summaryDays]);
806
+ react.useEffect(() => {
807
+ let cancelled = false;
808
+ setOverTimeLoading(true);
809
+ setOverTimeError(null);
810
+ fetch(
811
+ buildAnalyticsUrl("/admin/analytics/products-over-time", {
812
+ period: graphPeriod,
813
+ sales_channel_id: salesChannelId !== "all" ? salesChannelId : null
814
+ })
815
+ ).then((res) => {
816
+ if (!res.ok) throw new Error(res.statusText);
817
+ return res.json();
818
+ }).then((body) => {
819
+ if (!cancelled) setOverTime(body);
820
+ }).catch((error) => {
821
+ if (!cancelled) {
822
+ setOverTimeError(error instanceof Error ? error.message : String(error));
823
+ }
824
+ }).finally(() => {
825
+ if (!cancelled) setOverTimeLoading(false);
826
+ });
827
+ return () => {
828
+ cancelled = true;
829
+ };
830
+ }, [graphPeriod, salesChannelId]);
831
+ react.useEffect(() => {
832
+ let cancelled = false;
833
+ setTopSellersLoading(true);
834
+ setTopSellersError(null);
835
+ fetch(
836
+ buildAnalyticsUrl("/admin/analytics/products-top-sellers", {
837
+ period: topSellerPeriod,
838
+ sales_channel_id: salesChannelId !== "all" ? salesChannelId : null
839
+ })
840
+ ).then((res) => {
841
+ if (!res.ok) throw new Error(res.statusText);
842
+ return res.json();
843
+ }).then((body) => {
844
+ if (!cancelled) setTopSellers(body);
845
+ }).catch((error) => {
846
+ if (!cancelled) {
847
+ setTopSellersError(error instanceof Error ? error.message : String(error));
848
+ }
849
+ }).finally(() => {
850
+ if (!cancelled) setTopSellersLoading(false);
851
+ });
852
+ return () => {
853
+ cancelled = true;
854
+ };
855
+ }, [salesChannelId, topSellerPeriod]);
856
+ react.useEffect(() => {
857
+ let cancelled = false;
858
+ setPerformanceLoading(true);
859
+ setPerformanceError(null);
860
+ fetch(
861
+ buildAnalyticsUrl("/admin/analytics/products-performance", {
862
+ days: summaryDays,
863
+ sales_channel_id: salesChannelId !== "all" ? salesChannelId : null
864
+ })
865
+ ).then((res) => {
866
+ if (!res.ok) throw new Error(res.statusText);
867
+ return res.json();
868
+ }).then((body) => {
869
+ if (!cancelled) setPerformance(body);
870
+ }).catch((error) => {
871
+ if (!cancelled) {
872
+ setPerformanceError(error instanceof Error ? error.message : String(error));
873
+ }
874
+ }).finally(() => {
875
+ if (!cancelled) setPerformanceLoading(false);
876
+ });
877
+ return () => {
878
+ cancelled = true;
879
+ };
880
+ }, [salesChannelId, summaryDays]);
881
+ const topSellerChartData = react.useMemo(
882
+ () => ((topSellers == null ? void 0 : topSellers.products) ?? []).slice(0, 7).map((row) => ({
883
+ ...row,
884
+ product_title: truncateLabel(row.product_title, 24)
885
+ })).reverse(),
886
+ [topSellers]
887
+ );
888
+ const topViewedChartData = react.useMemo(
889
+ () => ((performance == null ? void 0 : performance.topViewedProducts) ?? []).slice(0, 7).map((row) => ({
890
+ ...row,
891
+ product_title: truncateLabel(row.product_title, 24)
892
+ })).reverse(),
893
+ [performance]
894
+ );
895
+ if (summaryLoading) {
896
+ return /* @__PURE__ */ jsxRuntime.jsx(
897
+ "div",
898
+ {
899
+ className: "flex items-center justify-center min-h-[320px]",
900
+ role: "status",
901
+ "aria-label": "Loading product analytics",
902
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Loading…" })
903
+ }
904
+ );
905
+ }
906
+ if (summaryError || !summary) {
907
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger", children: summaryError ?? "Failed to load product analytics" }) });
908
+ }
909
+ const viewsConnected = summary.productViewsConnected || (overTime == null ? void 0 : overTime.productViewsConnected) === true || (topSellers == null ? void 0 : topSellers.productViewsConnected) === true || (performance == null ? void 0 : performance.productViewsConnected) === true;
910
+ const selectedChannelLabel = salesChannelId === "all" ? "All channels" : ((_a = salesChannels.find((channel) => channel.id === salesChannelId)) == null ? void 0 : _a.name) ?? "Selected channel";
911
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-8", children: [
912
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [
913
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
914
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Products" }),
915
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mt-1", children: "Track units sold, revenue, best sellers, and product visit behavior." })
916
+ ] }),
917
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
918
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
919
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "products-channel-filter", className: "text-ui-fg-muted text-sm", children: "Channel" }),
920
+ /* @__PURE__ */ jsxRuntime.jsxs(
921
+ "select",
922
+ {
923
+ id: "products-channel-filter",
924
+ value: salesChannelId,
925
+ onChange: (event) => setSalesChannelId(event.target.value),
926
+ disabled: filtersLoading,
927
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
928
+ children: [
929
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All channels" }),
930
+ salesChannels.map((channel) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: channel.id, children: channel.name }, channel.id))
931
+ ]
932
+ }
933
+ )
934
+ ] }),
935
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
936
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "products-summary-period", className: "text-ui-fg-muted text-sm", children: "Period" }),
937
+ /* @__PURE__ */ jsxRuntime.jsx(
938
+ "select",
939
+ {
940
+ id: "products-summary-period",
941
+ value: summaryDays,
942
+ onChange: (event) => setSummaryDays(event.target.value),
943
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
944
+ children: SUMMARY_PERIODS.map((period) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: period.value, children: period.label }, period.value))
945
+ }
946
+ )
947
+ ] }),
948
+ summaryDays !== "all" && /* @__PURE__ */ jsxRuntime.jsx(
949
+ "button",
950
+ {
951
+ type: "button",
952
+ onClick: () => setSummaryDays("all"),
953
+ className: "text-xs text-ui-fg-muted hover:text-ui-fg-base transition-colors",
954
+ "aria-label": "Show all products analytics",
955
+ children: "Clear filter"
956
+ }
957
+ ),
958
+ salesChannelId !== "all" && /* @__PURE__ */ jsxRuntime.jsx(
959
+ "button",
960
+ {
961
+ type: "button",
962
+ onClick: () => setSalesChannelId("all"),
963
+ className: "text-xs text-ui-fg-muted hover:text-ui-fg-base transition-colors",
964
+ "aria-label": "Show all sales channels",
965
+ children: "Clear channel"
966
+ }
967
+ )
968
+ ] })
969
+ ] }),
970
+ filtersError && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger text-sm", children: filtersError }) }),
971
+ !viewsConnected && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Product views are unavailable until the `medusa-product-helper` tracking module is registered and storefront page views are being recorded." }) }),
972
+ salesChannelId !== "all" && viewsConnected && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-ui-fg-muted text-sm", children: [
973
+ "Showing product analytics for ",
974
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: selectedChannelLabel }),
975
+ ". Product views are scoped to products assigned to this sales channel."
976
+ ] }) }),
977
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4", children: [
978
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
979
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Units sold" }),
980
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: summary.unitsSold.toLocaleString() })
981
+ ] }),
982
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
983
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Product revenue" }),
984
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatCurrency(summary.productRevenue) })
985
+ ] }),
986
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
987
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Orders with product sales" }),
988
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: summary.ordersWithProducts.toLocaleString() })
989
+ ] }),
990
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
991
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Active products sold" }),
992
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: summary.activeProductsSold.toLocaleString() })
993
+ ] }),
994
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
995
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "Product views" }),
996
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: summary.totalProductViews.toLocaleString() })
997
+ ] }),
998
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-4", children: [
999
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm", children: "View / order ratio" }),
1000
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: formatRatio(summary.viewToOrderRatio) })
1001
+ ] })
1002
+ ] }),
1003
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
1004
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 mb-4", children: [
1005
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1006
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Sales and views over time" }),
1007
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mt-1", children: "Units sold and revenue are always shown. Product views appear when tracking data is available." })
1008
+ ] }),
1009
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1010
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Label, { htmlFor: "products-graph-period", className: "text-ui-fg-muted text-sm", children: "Range" }),
1011
+ /* @__PURE__ */ jsxRuntime.jsx(
1012
+ "select",
1013
+ {
1014
+ id: "products-graph-period",
1015
+ value: graphPeriod,
1016
+ onChange: (event) => setGraphPeriod(event.target.value),
1017
+ className: "h-9 rounded-md border border-ui-border-base bg-transparent px-3 text-sm text-ui-fg-base outline-none transition focus:ring-2 focus:ring-ui-fg-interactive",
1018
+ children: GRAPH_PERIODS.map((period) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: period.value, children: period.label }, period.value))
1019
+ }
1020
+ )
1021
+ ] })
1022
+ ] }),
1023
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-80", children: overTimeLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Loading chart…" }) }) : overTimeError || !overTime ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger", children: overTimeError ?? "Failed to load product trend" }) }) : /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.LineChart, { data: overTime.series, children: [
1024
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", vertical: false }),
1025
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { dataKey: "label" }),
1026
+ /* @__PURE__ */ jsxRuntime.jsx(
1027
+ recharts.YAxis,
1028
+ {
1029
+ yAxisId: "counts",
1030
+ allowDecimals: false,
1031
+ tickFormatter: (value) => Math.floor(Number(value)).toLocaleString()
1032
+ }
1033
+ ),
1034
+ /* @__PURE__ */ jsxRuntime.jsx(
1035
+ recharts.YAxis,
1036
+ {
1037
+ yAxisId: "revenue",
1038
+ orientation: "right",
1039
+ tickFormatter: (value) => formatCurrency(Number(value))
1040
+ }
1041
+ ),
1042
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(TrendTooltip, {}) }),
1043
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Legend, {}),
1044
+ /* @__PURE__ */ jsxRuntime.jsx(
1045
+ recharts.Line,
1046
+ {
1047
+ yAxisId: "counts",
1048
+ type: "monotone",
1049
+ dataKey: "units_sold",
1050
+ name: "Units sold",
1051
+ stroke: SALES_COLOR,
1052
+ strokeWidth: 2,
1053
+ dot: false
1054
+ }
1055
+ ),
1056
+ /* @__PURE__ */ jsxRuntime.jsx(
1057
+ recharts.Line,
1058
+ {
1059
+ yAxisId: "counts",
1060
+ type: "monotone",
1061
+ dataKey: "views",
1062
+ name: "Views",
1063
+ stroke: VIEWS_COLOR,
1064
+ strokeWidth: 2,
1065
+ dot: false
1066
+ }
1067
+ ),
1068
+ /* @__PURE__ */ jsxRuntime.jsx(
1069
+ recharts.Line,
1070
+ {
1071
+ yAxisId: "revenue",
1072
+ type: "monotone",
1073
+ dataKey: "revenue",
1074
+ name: "Revenue",
1075
+ stroke: REVENUE_COLOR,
1076
+ strokeWidth: 2,
1077
+ dot: false
1078
+ }
1079
+ )
1080
+ ] }) }) })
1081
+ ] }),
1082
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 xl:grid-cols-2 gap-4", children: [
1083
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
1084
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4 mb-4", children: [
1085
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1086
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", children: "Best sellers" }),
1087
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mt-1", children: "Ranked by units sold." })
1088
+ ] }),
1089
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-2", children: TOP_SELLER_PERIODS.map((period) => /* @__PURE__ */ jsxRuntime.jsx(
1090
+ ui.Button,
1091
+ {
1092
+ variant: topSellerPeriod === period.value ? "secondary" : "transparent",
1093
+ size: "small",
1094
+ onClick: () => setTopSellerPeriod(period.value),
1095
+ children: period.label
1096
+ },
1097
+ period.value
1098
+ )) })
1099
+ ] }),
1100
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-80", children: topSellersLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Loading best sellers…" }) }) : topSellersError || !topSellers ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger", children: topSellersError ?? "Failed to load best sellers" }) }) : topSellerChartData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "No product sales found for this range." }) }) : /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.BarChart, { data: topSellerChartData, layout: "vertical", margin: { left: 8, right: 8 }, children: [
1101
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", horizontal: false }),
1102
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { type: "number", allowDecimals: false }),
1103
+ /* @__PURE__ */ jsxRuntime.jsx(
1104
+ recharts.YAxis,
1105
+ {
1106
+ type: "category",
1107
+ dataKey: "product_title",
1108
+ width: 160,
1109
+ tickLine: false
1110
+ }
1111
+ ),
1112
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ProductBarTooltip, {}) }),
1113
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Bar, { dataKey: "units_sold", fill: SALES_COLOR, radius: [0, 6, 6, 0] })
1114
+ ] }) }) })
1115
+ ] }),
1116
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
1117
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-4", children: "Top seller breakdown" }),
1118
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1119
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1120
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
1121
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Units" }),
1122
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Orders" }),
1123
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Revenue" })
1124
+ ] }) }),
1125
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
1126
+ ((topSellers == null ? void 0 : topSellers.products) ?? []).slice(0, 8).map((product) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1127
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.product_title }),
1128
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.units_sold.toLocaleString() }),
1129
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.order_count.toLocaleString() }),
1130
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatCurrency(product.revenue) })
1131
+ ] }, product.product_id)),
1132
+ !topSellersLoading && (((_b = topSellers == null ? void 0 : topSellers.products) == null ? void 0 : _b.length) ?? 0) === 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: "No best seller data yet." }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
1135
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
1136
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {})
1137
+ ] })
1138
+ ] })
1139
+ ] })
1140
+ ] })
1141
+ ] }),
1142
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 xl:grid-cols-2 gap-4", children: [
1143
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
1144
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-1", children: "Most viewed products" }),
1145
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted text-sm mb-4", children: "View activity is sourced from the existing product-helper tracking module." }),
1146
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-80", children: performanceLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Loading product views…" }) }) : performanceError || !performance ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-danger", children: performanceError ?? "Failed to load product performance" }) }) : !performance.productViewsConnected ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "Product view tracking is not available in this environment." }) }) : topViewedChartData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-muted", children: "No product views found for this range." }) }) : /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsxs(recharts.BarChart, { data: topViewedChartData, layout: "vertical", margin: { left: 8, right: 8 }, children: [
1147
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeDasharray: "3 3", horizontal: false }),
1148
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.XAxis, { type: "number", allowDecimals: false }),
1149
+ /* @__PURE__ */ jsxRuntime.jsx(
1150
+ recharts.YAxis,
1151
+ {
1152
+ type: "category",
1153
+ dataKey: "product_title",
1154
+ width: 160,
1155
+ tickLine: false
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ProductBarTooltip, {}) }),
1159
+ /* @__PURE__ */ jsxRuntime.jsx(recharts.Bar, { dataKey: "total_views", fill: VIEWS_COLOR, radius: [0, 6, 6, 0] })
1160
+ ] }) }) })
1161
+ ] }),
1162
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6", children: [
1163
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "mb-4", children: "View-to-sales opportunities" }),
1164
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
1165
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1166
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Product" }),
1167
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Views" }),
1168
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Units" }),
1169
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Views / unit" })
1170
+ ] }) }),
1171
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
1172
+ ((performance == null ? void 0 : performance.viewOpportunities) ?? []).slice(0, 8).map((product) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1173
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.product_title }),
1174
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.total_views.toLocaleString() }),
1175
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: product.units_sold.toLocaleString() }),
1176
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: formatRatio(product.views_per_unit) })
1177
+ ] }, product.product_id)),
1178
+ !performanceLoading && !performanceError && (((_c = performance == null ? void 0 : performance.viewOpportunities) == null ? void 0 : _c.length) ?? 0) === 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
1179
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: "No product view opportunities yet." }),
1180
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
1181
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {}),
1182
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, {})
1183
+ ] })
1184
+ ] })
1185
+ ] })
1186
+ ] })
1187
+ ] })
1188
+ ] });
1189
+ }
607
1190
  const ANALYTICS_MODULES = [
608
1191
  { id: "orders", label: "Orders" },
609
- { id: "customers", label: "Customers" }
1192
+ { id: "customers", label: "Customers" },
1193
+ { id: "products", label: "Products" }
610
1194
  ];
611
1195
  const AnalyticsPage = () => {
612
1196
  const [activeModule, setActiveModule] = react.useState("orders");
@@ -623,7 +1207,8 @@ const AnalyticsPage = () => {
623
1207
  m.id
624
1208
  )) }),
625
1209
  activeModule === "orders" && /* @__PURE__ */ jsxRuntime.jsx(OrdersDashboard, {}),
626
- activeModule === "customers" && /* @__PURE__ */ jsxRuntime.jsx(CustomersDashboard, {})
1210
+ activeModule === "customers" && /* @__PURE__ */ jsxRuntime.jsx(CustomersDashboard, {}),
1211
+ activeModule === "products" && /* @__PURE__ */ jsxRuntime.jsx(ProductsDashboard, {})
627
1212
  ] }) }) }) });
628
1213
  };
629
1214
  const config = adminSdk.defineRouteConfig({