@burdenoff/microfe-billing 2026.530.3 → 2026.530.4

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.
@@ -0,0 +1,168 @@
1
+ import { AccessDenied as e } from "../../../shared/components/AccessDenied.js";
2
+ import { PageHeader as t } from "../../../shared/components/PageHeader.js";
3
+ import "../../../shared/components/index.js";
4
+ import { useBillingPermissions as n } from "../../../hooks/useBillingPermissions.js";
5
+ import { useDeactivatePromotionMutation as r, useListPromotionsQuery as i, useUpdatePromotionMutation as a } from "../../../../generated/global-operations.js";
6
+ import { useBillingNavigate as o } from "../../../hooks/useBillingNavigate.js";
7
+ import { useState as s } from "react";
8
+ import { Calendar as c, Gift as l, Plus as u, Tag as d, ToggleLeft as f, ToggleRight as p, Users as m } from "lucide-react";
9
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
10
+ //#region src/billing/modules/promotions/pages/PromotionsListPage.tsx
11
+ var _ = () => {
12
+ let _ = o(), v = n(), [y, b] = s(!1), [x, S] = s(null), { data: C, loading: w, refetch: T } = i({ variables: { includeInactive: y } }), [E] = r(), [D] = a();
13
+ if (!v.canManagePlans) return /* @__PURE__ */ h(e, { message: "You don't have permission to manage promotions." });
14
+ let O = C?.listPromotions ?? [], k = async (e, t) => {
15
+ S(e);
16
+ try {
17
+ t ? await E({ variables: { id: e } }) : await D({ variables: {
18
+ id: e,
19
+ input: { isActive: !0 }
20
+ } }), await T();
21
+ } finally {
22
+ S(null);
23
+ }
24
+ }, A = /* @__PURE__ */ new Date(), j = (e) => e.isActive ? new Date(e.expiresAt) < A ? {
25
+ text: "Expired",
26
+ cls: "bg-destructive/10 text-destructive"
27
+ } : new Date(e.startsAt) > A ? {
28
+ text: "Upcoming",
29
+ cls: "bg-primary/10 text-primary"
30
+ } : {
31
+ text: "Live",
32
+ cls: "bg-green-500/10 text-green-600"
33
+ } : {
34
+ text: "Inactive",
35
+ cls: "bg-muted text-muted-foreground"
36
+ };
37
+ return /* @__PURE__ */ g("div", {
38
+ className: "max-w-5xl mx-auto p-4 sm:p-6 space-y-6",
39
+ children: [
40
+ /* @__PURE__ */ g("div", {
41
+ className: "flex items-center justify-between gap-4",
42
+ children: [/* @__PURE__ */ h(t, {
43
+ title: "Promotions",
44
+ description: "Time-bounded offers that grant bonus free renewal months to subscribers",
45
+ icon: /* @__PURE__ */ h(l, { className: "size-6" })
46
+ }), /* @__PURE__ */ g("button", {
47
+ onClick: () => _("/promotions/create"),
48
+ className: "flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 text-sm font-medium shrink-0",
49
+ children: [/* @__PURE__ */ h(u, { className: "size-4" }), "New Promotion"]
50
+ })]
51
+ }),
52
+ /* @__PURE__ */ h("div", {
53
+ className: "flex items-center gap-3",
54
+ children: /* @__PURE__ */ g("label", {
55
+ className: "flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none",
56
+ children: [/* @__PURE__ */ h("input", {
57
+ type: "checkbox",
58
+ checked: y,
59
+ onChange: (e) => b(e.target.checked),
60
+ className: "rounded border-border"
61
+ }), "Show inactive / expired"]
62
+ })
63
+ }),
64
+ w ? /* @__PURE__ */ h("div", {
65
+ className: "text-center py-16 text-muted-foreground",
66
+ children: "Loading promotions…"
67
+ }) : O.length === 0 ? /* @__PURE__ */ g("div", {
68
+ className: "text-center py-16 border border-dashed border-border rounded-xl",
69
+ children: [
70
+ /* @__PURE__ */ h(l, { className: "size-10 mx-auto mb-3 text-muted-foreground/50" }),
71
+ /* @__PURE__ */ h("p", {
72
+ className: "text-muted-foreground font-medium",
73
+ children: "No promotions yet"
74
+ }),
75
+ /* @__PURE__ */ h("p", {
76
+ className: "text-sm text-muted-foreground mt-1",
77
+ children: "Create one to offer bonus months to early subscribers"
78
+ }),
79
+ /* @__PURE__ */ h("button", {
80
+ onClick: () => _("/promotions/create"),
81
+ className: "mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm",
82
+ children: "Create Promotion"
83
+ })
84
+ ]
85
+ }) : /* @__PURE__ */ h("div", {
86
+ className: "space-y-3",
87
+ children: O.map((e) => {
88
+ let { text: t, cls: n } = j(e), r = x === e.id;
89
+ return /* @__PURE__ */ g("div", {
90
+ className: "bg-card border border-border rounded-xl p-5 flex flex-col sm:flex-row sm:items-center gap-4",
91
+ children: [/* @__PURE__ */ g("div", {
92
+ className: "flex-1 min-w-0",
93
+ children: [
94
+ /* @__PURE__ */ g("div", {
95
+ className: "flex items-center gap-2 mb-1",
96
+ children: [/* @__PURE__ */ h("span", {
97
+ className: "font-semibold text-foreground truncate",
98
+ children: e.name
99
+ }), /* @__PURE__ */ h("span", {
100
+ className: `text-xs font-medium px-2 py-0.5 rounded-full ${n}`,
101
+ children: t
102
+ })]
103
+ }),
104
+ e.description && /* @__PURE__ */ h("p", {
105
+ className: "text-sm text-muted-foreground mb-2 line-clamp-1",
106
+ children: e.description
107
+ }),
108
+ /* @__PURE__ */ g("div", {
109
+ className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground",
110
+ children: [
111
+ /* @__PURE__ */ g("span", {
112
+ className: "flex items-center gap-1",
113
+ children: [
114
+ /* @__PURE__ */ h(l, { className: "size-3" }),
115
+ /* @__PURE__ */ h("strong", {
116
+ className: "text-foreground",
117
+ children: e.bonusMonths
118
+ }),
119
+ " bonus month",
120
+ e.bonusMonths === 1 ? "" : "s"
121
+ ]
122
+ }),
123
+ /* @__PURE__ */ g("span", {
124
+ className: "flex items-center gap-1",
125
+ children: [
126
+ /* @__PURE__ */ h(c, { className: "size-3" }),
127
+ new Date(e.startsAt).toLocaleDateString(),
128
+ " –",
129
+ " ",
130
+ new Date(e.expiresAt).toLocaleDateString()
131
+ ]
132
+ }),
133
+ /* @__PURE__ */ g("span", {
134
+ className: "flex items-center gap-1",
135
+ children: [
136
+ /* @__PURE__ */ h(m, { className: "size-3" }),
137
+ e.currentRedemptions,
138
+ e.maxRedemptions ? ` / ${e.maxRedemptions}` : "",
139
+ " redemptions"
140
+ ]
141
+ }),
142
+ e.applicableProductIds.length > 0 && /* @__PURE__ */ g("span", {
143
+ className: "flex items-center gap-1",
144
+ children: [/* @__PURE__ */ h(d, { className: "size-3" }), e.applicableProductIds.join(", ")]
145
+ })
146
+ ]
147
+ })
148
+ ]
149
+ }), /* @__PURE__ */ h("div", {
150
+ className: "flex items-center gap-2 shrink-0",
151
+ children: /* @__PURE__ */ h("button", {
152
+ onClick: () => k(e.id, e.isActive),
153
+ disabled: r,
154
+ title: e.isActive ? "Deactivate" : "Reactivate",
155
+ className: `p-2 rounded-lg transition-colors ${e.isActive ? "text-green-600 hover:bg-green-50" : "text-muted-foreground hover:bg-muted"} disabled:opacity-50`,
156
+ children: e.isActive ? /* @__PURE__ */ h(p, { className: "size-5" }) : /* @__PURE__ */ h(f, { className: "size-5" })
157
+ })
158
+ })]
159
+ }, e.id);
160
+ })
161
+ })
162
+ ]
163
+ });
164
+ };
165
+ //#endregion
166
+ export { _ as PromotionsListPage };
167
+
168
+ //# sourceMappingURL=PromotionsListPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PromotionsListPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/PromotionsListPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { Plus, Tag, Calendar, Users, ToggleLeft, ToggleRight, Gift } from 'lucide-react';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListPromotionsQuery,\n useDeactivatePromotionMutation,\n useUpdatePromotionMutation,\n} from '../../../../generated/global-operations';\n\nexport const PromotionsListPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [showInactive, setShowInactive] = useState(false);\n const [actionId, setActionId] = useState<string | null>(null);\n\n const { data, loading, refetch } = useListPromotionsQuery({\n variables: { includeInactive: showInactive },\n });\n\n const [deactivatePromotion] = useDeactivatePromotionMutation();\n const [updatePromotion] = useUpdatePromotionMutation();\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to manage promotions.\" />;\n }\n\n const promotions = data?.listPromotions ?? [];\n\n const handleToggle = async (id: string, currentlyActive: boolean) => {\n setActionId(id);\n try {\n if (currentlyActive) {\n await deactivatePromotion({ variables: { id } });\n } else {\n await updatePromotion({ variables: { id, input: { isActive: true } } });\n }\n await refetch();\n } finally {\n setActionId(null);\n }\n };\n\n const now = new Date();\n const statusLabel = (p: { isActive: boolean; startsAt: string; expiresAt: string }) => {\n if (!p.isActive) return { text: 'Inactive', cls: 'bg-muted text-muted-foreground' };\n if (new Date(p.expiresAt) < now)\n return { text: 'Expired', cls: 'bg-destructive/10 text-destructive' };\n if (new Date(p.startsAt) > now) return { text: 'Upcoming', cls: 'bg-primary/10 text-primary' };\n return { text: 'Live', cls: 'bg-green-500/10 text-green-600' };\n };\n\n return (\n <div className=\"max-w-5xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center justify-between gap-4\">\n <PageHeader\n title=\"Promotions\"\n description=\"Time-bounded offers that grant bonus free renewal months to subscribers\"\n icon={<Gift className=\"size-6\" />}\n />\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 text-sm font-medium shrink-0\"\n >\n <Plus className=\"size-4\" />\n New Promotion\n </button>\n </div>\n\n {/* Filter */}\n <div className=\"flex items-center gap-3\">\n <label className=\"flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none\">\n <input\n type=\"checkbox\"\n checked={showInactive}\n onChange={(e) => setShowInactive(e.target.checked)}\n className=\"rounded border-border\"\n />\n Show inactive / expired\n </label>\n </div>\n\n {loading ? (\n <div className=\"text-center py-16 text-muted-foreground\">Loading promotions…</div>\n ) : promotions.length === 0 ? (\n <div className=\"text-center py-16 border border-dashed border-border rounded-xl\">\n <Gift className=\"size-10 mx-auto mb-3 text-muted-foreground/50\" />\n <p className=\"text-muted-foreground font-medium\">No promotions yet</p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n Create one to offer bonus months to early subscribers\n </p>\n <button\n onClick={() => navigateTo('/promotions/create')}\n className=\"mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm\"\n >\n Create Promotion\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n {promotions.map((p: (typeof promotions)[number]) => {\n const { text, cls } = statusLabel(p);\n const busy = actionId === p.id;\n return (\n <div\n key={p.id}\n className=\"bg-card border border-border rounded-xl p-5 flex flex-col sm:flex-row sm:items-center gap-4\"\n >\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 mb-1\">\n <span className=\"font-semibold text-foreground truncate\">{p.name}</span>\n <span className={`text-xs font-medium px-2 py-0.5 rounded-full ${cls}`}>\n {text}\n </span>\n </div>\n {p.description && (\n <p className=\"text-sm text-muted-foreground mb-2 line-clamp-1\">\n {p.description}\n </p>\n )}\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n <span className=\"flex items-center gap-1\">\n <Gift className=\"size-3\" />\n <strong className=\"text-foreground\">{p.bonusMonths}</strong> bonus month\n {p.bonusMonths !== 1 ? 's' : ''}\n </span>\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"size-3\" />\n {new Date(p.startsAt).toLocaleDateString()} –{' '}\n {new Date(p.expiresAt).toLocaleDateString()}\n </span>\n <span className=\"flex items-center gap-1\">\n <Users className=\"size-3\" />\n {p.currentRedemptions}\n {p.maxRedemptions ? ` / ${p.maxRedemptions}` : ''} redemptions\n </span>\n {p.applicableProductIds.length > 0 && (\n <span className=\"flex items-center gap-1\">\n <Tag className=\"size-3\" />\n {p.applicableProductIds.join(', ')}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"flex items-center gap-2 shrink-0\">\n <button\n onClick={() => handleToggle(p.id, p.isActive)}\n disabled={busy}\n title={p.isActive ? 'Deactivate' : 'Reactivate'}\n className={`p-2 rounded-lg transition-colors ${\n p.isActive\n ? 'text-green-600 hover:bg-green-50'\n : 'text-muted-foreground hover:bg-muted'\n } disabled:opacity-50`}\n >\n {p.isActive ? (\n <ToggleRight className=\"size-5\" />\n ) : (\n <ToggleLeft className=\"size-5\" />\n )}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import "./PromotionsListPage.js";
2
+ import "./CreatePromotionPage.js";