@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.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.
- package/apps/storefront/astro.config.mjs +59 -0
- package/apps/storefront/env.d.ts +24 -8
- package/apps/storefront/package.json +1 -1
- package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
- package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
- package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
- package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
- package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
- package/apps/storefront/src/config/devTenantSeed.ts +65 -16
- package/apps/storefront/src/config/resolver.ts +48 -1
- package/apps/storefront/src/layouts/Layout.astro +14 -12
- package/apps/storefront/src/lib/admin/README.md +2 -2
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
- package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
- package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
- package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
- package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
- package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
- package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
- package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
- package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
- package/apps/storefront/src/lib/edgeCache.ts +7 -6
- package/apps/storefront/src/lib/health.ts +53 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
- package/apps/storefront/src/lib/sections.ts +19 -23
- package/apps/storefront/src/lib/securityHeaders.ts +1 -1
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
- package/apps/storefront/src/middleware/index.ts +29 -12
- package/apps/storefront/src/pages/[...slug].astro +5 -6
- package/apps/storefront/src/pages/admin.astro +10 -3
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
- package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
- package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
- package/apps/storefront/src/pages/auth/login.astro +4 -1
- package/apps/storefront/src/pages/auth/magic.astro +263 -43
- package/apps/storefront/src/pages/capabilities.astro +1 -1
- package/apps/storefront/src/pages/cockpit.astro +2365 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
- package/apps/storefront/src/pages/index.astro +23 -29
- package/apps/storefront/src/pages/products/[handle].astro +50 -20
- package/package.json +6 -2
- package/packages/public-runtime/src/capabilities.ts +97 -9
- package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
- package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
- package/packages/public-runtime/src/checkout-style.ts +13 -13
- package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
- package/packages/public-runtime/src/csp.ts +11 -11
- package/packages/public-runtime/src/customization-scripts.ts +28 -0
- package/packages/public-runtime/src/extension-contract.ts +73 -0
- package/packages/public-runtime/src/index.ts +4 -2
- package/packages/public-runtime/src/product.ts +57 -0
- package/packages/public-runtime/src/raw-html-policy.ts +35 -0
- package/packages/public-runtime/src/tenant.ts +14 -11
- package/pnpm-workspace.yaml +1 -0
- package/scripts/dev/checkout-watch.mjs +9 -0
- package/scripts/dev/file-browser.mjs +5 -1
- package/scripts/dev/transient-files.mjs +34 -0
- package/scripts/tot-dev.mjs +16 -3
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
- package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
- package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
- package/apps/storefront/src/pages/api/dev/file.ts +0 -57
- package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
- package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
- package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
- package/apps/storefront/src/pages/api/dev/write.ts +0 -116
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Subscriptions tab - read-only v1. Catalog purchase options are the source of
|
|
4
|
+
* truth; lifecycle actions require a later subscription API and audit gates.
|
|
5
|
+
*/
|
|
6
|
+
import { getSubscriptionsViewModel } from "@/lib/admin/services/mock";
|
|
7
|
+
import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
|
|
8
|
+
import type {
|
|
9
|
+
SubscriptionAttentionRow,
|
|
10
|
+
SubscriptionProgramReadiness,
|
|
11
|
+
SubscriptionRenewalState,
|
|
12
|
+
SubscriptionSubscriberStatus,
|
|
13
|
+
} from "@/lib/admin/viewmodels/subscriptions";
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
envelope: AdminTenantEnvelope;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { envelope } = Astro.props;
|
|
20
|
+
const cspNonce = Astro.locals.cspNonce;
|
|
21
|
+
const vm = getSubscriptionsViewModel(envelope);
|
|
22
|
+
const defaultProgram = vm.programs.find((row) => row.needsAttentionCount > 0) ?? vm.programs[0] ?? null;
|
|
23
|
+
|
|
24
|
+
const READINESS_LABEL: Record<SubscriptionProgramReadiness, string> = {
|
|
25
|
+
ready: "Ready",
|
|
26
|
+
needs_setup: "Needs setup",
|
|
27
|
+
blocked: "Blocked",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const READINESS_TONE: Record<SubscriptionProgramReadiness, string> = {
|
|
31
|
+
ready: "green",
|
|
32
|
+
needs_setup: "amber",
|
|
33
|
+
blocked: "red",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const STATUS_LABEL: Record<SubscriptionSubscriberStatus, string> = {
|
|
37
|
+
active: "Active",
|
|
38
|
+
paused: "Paused",
|
|
39
|
+
payment_attention: "Payment attention",
|
|
40
|
+
verification_hold: "Verification hold",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const STATUS_TONE: Record<SubscriptionSubscriberStatus, string> = {
|
|
44
|
+
active: "green",
|
|
45
|
+
paused: "",
|
|
46
|
+
payment_attention: "amber",
|
|
47
|
+
verification_hold: "red",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const RENEWAL_TONE: Record<SubscriptionRenewalState, string> = {
|
|
51
|
+
scheduled: "green",
|
|
52
|
+
attention: "amber",
|
|
53
|
+
held: "red",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ATTENTION_KIND_LABEL: Record<SubscriptionAttentionRow["kind"], string> = {
|
|
57
|
+
setup_gap: "Setup gap",
|
|
58
|
+
verification_hold: "Verification hold",
|
|
59
|
+
payment_attention: "Payment attention",
|
|
60
|
+
product_blocker: "Product blocker",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function formatMoney(value: number): string {
|
|
64
|
+
return `$${value.toFixed(2)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatDate(iso: string): string {
|
|
68
|
+
return new Date(iso).toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
|
|
69
|
+
}
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
<section
|
|
73
|
+
id="subscriptions"
|
|
74
|
+
class="band subscriptions-tab"
|
|
75
|
+
aria-labelledby="admin-tab-subscriptions subscriptions-title"
|
|
76
|
+
data-admin-panel
|
|
77
|
+
data-agent-scope="subscriptions"
|
|
78
|
+
role="tabpanel"
|
|
79
|
+
data-subscriptions-target-tenant={vm.targetTenant}
|
|
80
|
+
data-admin-selected-id={defaultProgram?.id ?? ""}
|
|
81
|
+
>
|
|
82
|
+
<div class="section-title">
|
|
83
|
+
<div>
|
|
84
|
+
<h2 id="subscriptions-title">Subscriptions</h2>
|
|
85
|
+
<p>Recurring purchase options, subscriber readiness, renewal attention, and checkout payload evidence.</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="actions">
|
|
88
|
+
<button class="btn" type="button"><svg><use href="#i-export"></use></svg>Export</button>
|
|
89
|
+
<button class="btn" type="button"><svg><use href="#i-refresh"></use></svg>Refresh</button>
|
|
90
|
+
<button class="btn primary" type="button"><svg><use href="#i-check"></use></svg>Review setup</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="grid cols-4">
|
|
95
|
+
<div class="metric"><span>Subscription products</span><strong>{vm.summary.subscriptionProducts}</strong><small>Catalog purchase options</small></div>
|
|
96
|
+
<div class="metric"><span>Active subscribers</span><strong>{vm.summary.activeSubscribers}</strong><small>Across available programs</small></div>
|
|
97
|
+
<div class="metric"><span>Upcoming renewal value</span><strong>{formatMoney(vm.summary.upcomingRenewalValue)}</strong><small>Scheduled renewals</small></div>
|
|
98
|
+
<div class="metric"><span>Needs attention</span><strong>{vm.summary.needsAttention}</strong><small>Setup, payment, or verification</small></div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{vm.attention.length > 0 && (
|
|
102
|
+
<div class="panel subscriptions-attention-panel">
|
|
103
|
+
<div class="panel-head subscriptions-panel-head">
|
|
104
|
+
<h3><svg><use href="#i-alert"></use></svg>Attention Queue</h3>
|
|
105
|
+
<span class="chip amber">{vm.attention.length} item{vm.attention.length === 1 ? "" : "s"}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="panel-body">
|
|
108
|
+
<div class="subscriptions-attention-grid">
|
|
109
|
+
{vm.attention.slice(0, 4).map((row) => {
|
|
110
|
+
const program = vm.detailsById[row.programId];
|
|
111
|
+
return (
|
|
112
|
+
<div class:list={["subscriptions-attention-card", row.blocking ? "blocking" : ""]}>
|
|
113
|
+
<div>
|
|
114
|
+
<span class:list={["chip", row.tone]}>{ATTENTION_KIND_LABEL[row.kind]}</span>
|
|
115
|
+
<strong>{row.label}</strong>
|
|
116
|
+
<p>{row.detail}</p>
|
|
117
|
+
</div>
|
|
118
|
+
<button class="btn" type="button" data-subscriptions-select-row={row.programId}>
|
|
119
|
+
{program ? program.productName : "Open"}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
<div class="subscriptions-workspace">
|
|
130
|
+
<div class="panel subscriptions-list-panel">
|
|
131
|
+
<div class="panel-head subscriptions-panel-head">
|
|
132
|
+
<h3><svg><use href="#i-refresh"></use></svg>Programs</h3>
|
|
133
|
+
<span class="chip teal">{vm.programs.length} recurring option{vm.programs.length === 1 ? "" : "s"}</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="panel-body grid">
|
|
136
|
+
<div class="subscriptions-toolbar" aria-label="Subscription controls">
|
|
137
|
+
<label class="subscriptions-search">
|
|
138
|
+
<span>Search</span>
|
|
139
|
+
<input type="search" placeholder="Search products, options, frequency" aria-label="Search subscription programs" data-subscriptions-search />
|
|
140
|
+
</label>
|
|
141
|
+
<label>
|
|
142
|
+
<span>Readiness</span>
|
|
143
|
+
<select aria-label="Filter by readiness" data-subscriptions-readiness-select>
|
|
144
|
+
<option value="">Any readiness</option>
|
|
145
|
+
<option value="ready">Ready</option>
|
|
146
|
+
<option value="needs_setup">Needs setup</option>
|
|
147
|
+
<option value="blocked">Blocked</option>
|
|
148
|
+
</select>
|
|
149
|
+
</label>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="table-wrap">
|
|
153
|
+
<table class="subscriptions-table">
|
|
154
|
+
<thead>
|
|
155
|
+
<tr>
|
|
156
|
+
<th>Program</th>
|
|
157
|
+
<th>Frequency</th>
|
|
158
|
+
<th>Savings</th>
|
|
159
|
+
<th>Readiness</th>
|
|
160
|
+
<th>Subscribers</th>
|
|
161
|
+
<th>Renewal value</th>
|
|
162
|
+
<th class="action">Action</th>
|
|
163
|
+
</tr>
|
|
164
|
+
</thead>
|
|
165
|
+
<tbody data-subscriptions-tbody>
|
|
166
|
+
{vm.programs.map((row) => (
|
|
167
|
+
<tr
|
|
168
|
+
class:list={[{ selected: row.id === defaultProgram?.id }]}
|
|
169
|
+
data-subscriptions-row
|
|
170
|
+
data-program-id={row.id}
|
|
171
|
+
data-readiness={row.readiness}
|
|
172
|
+
data-search-haystack={`${row.productName} ${row.optionLabel} ${row.frequency}`.toLowerCase()}
|
|
173
|
+
>
|
|
174
|
+
<td><strong>{row.productName}</strong><span class="subtle">{row.optionLabel}</span></td>
|
|
175
|
+
<td><strong>{row.frequency}</strong></td>
|
|
176
|
+
<td><strong>{row.savingsLabel ?? "None"}</strong>{row.priceAdjustmentLabel && <span class="subtle">{row.priceAdjustmentLabel}</span>}</td>
|
|
177
|
+
<td>
|
|
178
|
+
<span class:list={["chip", READINESS_TONE[row.readiness]]}>{READINESS_LABEL[row.readiness]}</span>
|
|
179
|
+
{row.needsAttentionCount > 0 && <span class="subtle">{row.needsAttentionCount} attention</span>}
|
|
180
|
+
</td>
|
|
181
|
+
<td><strong>{row.subscriberCount}</strong></td>
|
|
182
|
+
<td><strong>{formatMoney(row.upcomingRenewalValue)}</strong></td>
|
|
183
|
+
<td class="action"><button type="button" class="btn" data-subscriptions-select-row={row.id}>Open</button></td>
|
|
184
|
+
</tr>
|
|
185
|
+
))}
|
|
186
|
+
</tbody>
|
|
187
|
+
</table>
|
|
188
|
+
<p class="subscriptions-empty-state" data-subscriptions-empty hidden>No subscription programs match the current filters.</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<aside class="panel subscriptions-detail-panel" aria-labelledby="subscriptions-detail-title" data-subscriptions-detail-root>
|
|
194
|
+
{vm.programs.map((row) => {
|
|
195
|
+
const detail = vm.detailsById[row.id]!;
|
|
196
|
+
return (
|
|
197
|
+
<div class="subscriptions-detail-record" data-subscriptions-detail-record={row.id} hidden={row.id !== defaultProgram?.id}>
|
|
198
|
+
<div class="panel-head subscriptions-panel-head">
|
|
199
|
+
<h3 id={row.id === defaultProgram?.id ? "subscriptions-detail-title" : undefined}><svg><use href="#i-file"></use></svg>{row.optionLabel}</h3>
|
|
200
|
+
<span class:list={["chip", READINESS_TONE[row.readiness]]}>{READINESS_LABEL[row.readiness]}</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="panel-body grid">
|
|
203
|
+
<div class="subscriptions-detail-meta">
|
|
204
|
+
<div><span class="eyebrow">Product</span><strong>{row.productName}</strong></div>
|
|
205
|
+
<div><span class="eyebrow">Frequency</span><strong>{row.frequency}</strong></div>
|
|
206
|
+
<div><span class="eyebrow">Subscribers</span><strong>{row.subscriberCount}</strong></div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{detail.attention.length > 0 && (
|
|
210
|
+
<div class="subscriptions-check-list">
|
|
211
|
+
{detail.attention.map((attention) => (
|
|
212
|
+
<div class:list={["subscriptions-check-row", attention.blocking ? "blocking" : ""]}>
|
|
213
|
+
<div><b>{attention.label}</b><p>{attention.detail}</p></div>
|
|
214
|
+
<span class:list={["chip", attention.tone]}>{ATTENTION_KIND_LABEL[attention.kind]}</span>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
<details class="detail-section" open>
|
|
221
|
+
<summary class="detail-section-head"><h4>Checkout payload preview</h4><span class="chip">{detail.checkoutFields.length}</span></summary>
|
|
222
|
+
<div class="mini-table" role="table" aria-label="Checkout payload preview">
|
|
223
|
+
{detail.checkoutFields.map((field) => (
|
|
224
|
+
<div class="mini-row payload-row" role="row">
|
|
225
|
+
<div role="cell"><span class="eyebrow">Field</span><strong>{field.name}</strong></div>
|
|
226
|
+
<div role="cell"><span class="eyebrow">Value</span><strong>{field.value}</strong></div>
|
|
227
|
+
</div>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</details>
|
|
231
|
+
|
|
232
|
+
<details class="detail-section" open={detail.subscribers.length > 0}>
|
|
233
|
+
<summary class="detail-section-head"><h4>Linked subscribers</h4><span class="chip">{detail.subscribers.length}</span></summary>
|
|
234
|
+
{detail.subscribers.length > 0 ? (
|
|
235
|
+
<div class="mini-table" role="table" aria-label="Linked subscribers">
|
|
236
|
+
{detail.subscribers.map((subscriber) => (
|
|
237
|
+
<div class="mini-row subscriber-row" role="row">
|
|
238
|
+
<div role="cell"><strong>{subscriber.customerName}</strong><span class="subtle">{subscriber.email}</span></div>
|
|
239
|
+
<div role="cell"><span class:list={["chip", STATUS_TONE[subscriber.status]]}>{STATUS_LABEL[subscriber.status]}</span></div>
|
|
240
|
+
<div role="cell"><span class="eyebrow">Next renewal</span><strong>{formatDate(subscriber.nextRenewalAt)}</strong></div>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
) : (
|
|
245
|
+
<p class="subscriptions-note">No active subscriber records are linked to this program.</p>
|
|
246
|
+
)}
|
|
247
|
+
</details>
|
|
248
|
+
|
|
249
|
+
<details class="detail-section" open={detail.upcomingRenewals.length > 0}>
|
|
250
|
+
<summary class="detail-section-head"><h4>Upcoming renewals</h4><span class="chip">{detail.upcomingRenewals.length}</span></summary>
|
|
251
|
+
<div class="subscriptions-renewal-list">
|
|
252
|
+
{detail.upcomingRenewals.map((renewal) => (
|
|
253
|
+
<div>
|
|
254
|
+
<time>{formatDate(renewal.at)}</time>
|
|
255
|
+
<div>
|
|
256
|
+
<b>{renewal.customerName}</b>
|
|
257
|
+
<p>{formatMoney(renewal.amount)} renewal</p>
|
|
258
|
+
</div>
|
|
259
|
+
<span class:list={["chip", RENEWAL_TONE[renewal.state]]}>{renewal.state}</span>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
</details>
|
|
264
|
+
|
|
265
|
+
<details class="detail-section">
|
|
266
|
+
<summary class="detail-section-head"><h4>Evidence timeline</h4><span class="chip">{detail.evidenceTimeline.length}</span></summary>
|
|
267
|
+
<div class="subscriptions-activity-list">
|
|
268
|
+
{detail.evidenceTimeline.map((event) => (
|
|
269
|
+
<div>
|
|
270
|
+
<time>{formatDate(event.at)}</time>
|
|
271
|
+
<div><b>{event.title}</b><p>{event.detail}</p></div>
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</details>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
})}
|
|
280
|
+
</aside>
|
|
281
|
+
</div>
|
|
282
|
+
</section>
|
|
283
|
+
|
|
284
|
+
<script is:inline nonce={cspNonce}>
|
|
285
|
+
(() => {
|
|
286
|
+
const root = document.querySelector("[data-subscriptions-target-tenant]");
|
|
287
|
+
if (!root) return;
|
|
288
|
+
const rows = Array.from(root.querySelectorAll("[data-subscriptions-row]"));
|
|
289
|
+
const detailRecords = Array.from(root.querySelectorAll("[data-subscriptions-detail-record]"));
|
|
290
|
+
const emptyState = root.querySelector("[data-subscriptions-empty]");
|
|
291
|
+
const searchInput = root.querySelector("[data-subscriptions-search]");
|
|
292
|
+
const readinessSelect = root.querySelector("[data-subscriptions-readiness-select]");
|
|
293
|
+
|
|
294
|
+
const selectProgram = (programId) => {
|
|
295
|
+
root.dataset.adminSelectedId = programId;
|
|
296
|
+
rows.forEach((row) => row.classList.toggle("selected", row.dataset.programId === programId));
|
|
297
|
+
detailRecords.forEach((record) => {
|
|
298
|
+
record.hidden = record.dataset.subscriptionsDetailRecord !== programId;
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const applyFilters = () => {
|
|
303
|
+
const query = String(searchInput?.value ?? "").trim().toLowerCase();
|
|
304
|
+
const readiness = String(readinessSelect?.value ?? "");
|
|
305
|
+
let visible = 0;
|
|
306
|
+
rows.forEach((row) => {
|
|
307
|
+
const matchesSearch = !query || String(row.dataset.searchHaystack ?? "").includes(query);
|
|
308
|
+
const matchesReadiness = !readiness || row.dataset.readiness === readiness;
|
|
309
|
+
const show = matchesSearch && matchesReadiness;
|
|
310
|
+
row.hidden = !show;
|
|
311
|
+
if (show) visible += 1;
|
|
312
|
+
});
|
|
313
|
+
if (emptyState) emptyState.hidden = visible !== 0;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
root.querySelectorAll("[data-subscriptions-select-row]").forEach((button) => {
|
|
317
|
+
button.addEventListener("click", () => {
|
|
318
|
+
const programId = button.dataset.subscriptionsSelectRow;
|
|
319
|
+
if (programId) selectProgram(programId);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
rows.forEach((row) => {
|
|
324
|
+
row.addEventListener("click", (event) => {
|
|
325
|
+
if (event.target instanceof Element && event.target.closest("[data-subscriptions-select-row]")) return;
|
|
326
|
+
const programId = row.dataset.programId;
|
|
327
|
+
if (programId) selectProgram(programId);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
searchInput?.addEventListener("input", applyFilters);
|
|
332
|
+
readinessSelect?.addEventListener("change", applyFilters);
|
|
333
|
+
applyFilters();
|
|
334
|
+
})();
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<style>
|
|
338
|
+
.subscriptions-tab {
|
|
339
|
+
--subscriptions-sideout-width: minmax(370px, 0.42fr);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.subscriptions-panel-head {
|
|
343
|
+
align-items: center;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.subscriptions-attention-grid {
|
|
347
|
+
display: grid;
|
|
348
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
349
|
+
gap: 10px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.subscriptions-attention-card {
|
|
353
|
+
display: flex;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
gap: 12px;
|
|
356
|
+
min-width: 0;
|
|
357
|
+
padding: 12px;
|
|
358
|
+
border: 1px solid var(--admin-line);
|
|
359
|
+
border-radius: var(--admin-radius);
|
|
360
|
+
background: var(--admin-surface-soft);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.subscriptions-attention-card.blocking {
|
|
364
|
+
border-color: #f2c778;
|
|
365
|
+
background: #fff8ea;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.subscriptions-attention-card strong,
|
|
369
|
+
.subscriptions-attention-card p {
|
|
370
|
+
display: block;
|
|
371
|
+
margin: 6px 0 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.subscriptions-attention-card p,
|
|
375
|
+
.subscriptions-note,
|
|
376
|
+
.subscriptions-activity-list p,
|
|
377
|
+
.subscriptions-renewal-list p,
|
|
378
|
+
.subscriptions-check-row p {
|
|
379
|
+
color: var(--admin-muted);
|
|
380
|
+
font-size: 12px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.subscriptions-workspace {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: minmax(0, 1fr) var(--subscriptions-sideout-width);
|
|
386
|
+
gap: 14px;
|
|
387
|
+
align-items: start;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.subscriptions-detail-panel {
|
|
391
|
+
position: sticky;
|
|
392
|
+
top: 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.subscriptions-toolbar {
|
|
396
|
+
display: grid;
|
|
397
|
+
grid-template-columns: minmax(220px, 1fr) minmax(150px, 0.28fr);
|
|
398
|
+
gap: 10px;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.subscriptions-toolbar label {
|
|
402
|
+
display: grid;
|
|
403
|
+
gap: 5px;
|
|
404
|
+
min-width: 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.subscriptions-toolbar span,
|
|
408
|
+
.eyebrow {
|
|
409
|
+
color: var(--admin-muted);
|
|
410
|
+
font-size: 11px;
|
|
411
|
+
font-weight: 760;
|
|
412
|
+
text-transform: uppercase;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.subscriptions-toolbar input,
|
|
416
|
+
.subscriptions-toolbar select {
|
|
417
|
+
min-width: 0;
|
|
418
|
+
width: 100%;
|
|
419
|
+
min-height: 38px;
|
|
420
|
+
padding: 0 10px;
|
|
421
|
+
border: 1px solid var(--admin-line-strong);
|
|
422
|
+
border-radius: var(--admin-radius-sm);
|
|
423
|
+
background: var(--admin-surface);
|
|
424
|
+
color: var(--admin-ink);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.subscriptions-table {
|
|
428
|
+
min-width: 980px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.subscriptions-table tbody tr {
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.subscriptions-table tr.selected td {
|
|
436
|
+
background: #eef9f6;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.subscriptions-empty-state {
|
|
440
|
+
margin: 0;
|
|
441
|
+
padding: 24px 14px;
|
|
442
|
+
color: var(--admin-muted);
|
|
443
|
+
font-size: 13px;
|
|
444
|
+
text-align: center;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.subscriptions-detail-meta {
|
|
448
|
+
display: grid;
|
|
449
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
450
|
+
gap: 10px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.subscriptions-detail-meta > div {
|
|
454
|
+
min-width: 0;
|
|
455
|
+
padding-bottom: 10px;
|
|
456
|
+
border-bottom: 1px solid var(--admin-line);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.subscriptions-detail-meta strong,
|
|
460
|
+
.mini-row strong {
|
|
461
|
+
display: block;
|
|
462
|
+
margin-top: 3px;
|
|
463
|
+
overflow-wrap: anywhere;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.subscriptions-check-list,
|
|
467
|
+
.subscriptions-activity-list,
|
|
468
|
+
.subscriptions-renewal-list {
|
|
469
|
+
display: grid;
|
|
470
|
+
gap: 10px;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.subscriptions-check-row {
|
|
474
|
+
display: grid;
|
|
475
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
476
|
+
gap: 12px;
|
|
477
|
+
align-items: center;
|
|
478
|
+
padding: 11px 12px;
|
|
479
|
+
border: 1px solid var(--admin-line);
|
|
480
|
+
border-radius: var(--admin-radius);
|
|
481
|
+
background: var(--admin-surface-soft);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.subscriptions-check-row.blocking {
|
|
485
|
+
border-color: #f2c778;
|
|
486
|
+
background: #fff8ea;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.subscriptions-check-row b,
|
|
490
|
+
.subscriptions-renewal-list b,
|
|
491
|
+
.subscriptions-activity-list b {
|
|
492
|
+
display: block;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.detail-section {
|
|
496
|
+
display: grid;
|
|
497
|
+
gap: 10px;
|
|
498
|
+
padding-top: 2px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.detail-section + .detail-section {
|
|
502
|
+
padding-top: 12px;
|
|
503
|
+
border-top: 1px solid var(--admin-line);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.detail-section-head {
|
|
507
|
+
display: flex;
|
|
508
|
+
align-items: center;
|
|
509
|
+
justify-content: space-between;
|
|
510
|
+
gap: 10px;
|
|
511
|
+
cursor: pointer;
|
|
512
|
+
list-style: none;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.detail-section-head::-webkit-details-marker {
|
|
516
|
+
display: none;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.detail-section-head h4 {
|
|
520
|
+
margin: 0;
|
|
521
|
+
font-size: 13px;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.mini-table {
|
|
525
|
+
display: grid;
|
|
526
|
+
margin-top: 10px;
|
|
527
|
+
border: 1px solid var(--admin-line);
|
|
528
|
+
border-radius: var(--admin-radius);
|
|
529
|
+
overflow: hidden;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.mini-row {
|
|
533
|
+
display: grid;
|
|
534
|
+
grid-template-columns: minmax(130px, 1.25fr) minmax(120px, 1fr) 110px;
|
|
535
|
+
gap: 10px;
|
|
536
|
+
align-items: center;
|
|
537
|
+
min-height: 58px;
|
|
538
|
+
padding: 9px 10px;
|
|
539
|
+
border-bottom: 1px solid var(--admin-line);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.mini-row:last-child {
|
|
543
|
+
border-bottom: 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.payload-row {
|
|
547
|
+
grid-template-columns: minmax(130px, 0.8fr) minmax(0, 1fr);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.subscriptions-renewal-list > div {
|
|
551
|
+
display: grid;
|
|
552
|
+
grid-template-columns: 92px minmax(0, 1fr) auto;
|
|
553
|
+
gap: 10px;
|
|
554
|
+
align-items: center;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.subscriptions-activity-list > div {
|
|
558
|
+
display: grid;
|
|
559
|
+
grid-template-columns: 92px minmax(0, 1fr);
|
|
560
|
+
gap: 10px;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.subscriptions-renewal-list time,
|
|
564
|
+
.subscriptions-activity-list time {
|
|
565
|
+
color: var(--admin-muted);
|
|
566
|
+
font-size: 12px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@media (max-width: 1180px) {
|
|
570
|
+
.subscriptions-workspace {
|
|
571
|
+
grid-template-columns: minmax(0, 1fr);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.subscriptions-detail-panel {
|
|
575
|
+
position: static;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
@media (max-width: 920px) {
|
|
580
|
+
.subscriptions-tab .section-title,
|
|
581
|
+
.subscriptions-tab .panel-head {
|
|
582
|
+
align-items: flex-start;
|
|
583
|
+
flex-direction: column;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.subscriptions-tab .grid.cols-4,
|
|
587
|
+
.subscriptions-attention-grid {
|
|
588
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
@media (max-width: 640px) {
|
|
593
|
+
.subscriptions-tab .grid.cols-4,
|
|
594
|
+
.subscriptions-attention-grid,
|
|
595
|
+
.subscriptions-toolbar,
|
|
596
|
+
.subscriptions-detail-meta,
|
|
597
|
+
.mini-row,
|
|
598
|
+
.payload-row,
|
|
599
|
+
.subscriptions-renewal-list > div,
|
|
600
|
+
.subscriptions-activity-list > div,
|
|
601
|
+
.subscriptions-check-row {
|
|
602
|
+
grid-template-columns: 1fr;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.subscriptions-tab .actions {
|
|
606
|
+
width: 100%;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.subscriptions-tab .actions .btn {
|
|
610
|
+
flex: 1 1 150px;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
</style>
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
parseOrderFilters,
|
|
25
25
|
type OrdersLoadStatus,
|
|
26
26
|
} from "@/lib/admin/services/orders";
|
|
27
|
-
import {
|
|
27
|
+
import { createCheckoutOrderClient, orderApiBaseUrl } from "@/lib/admin/orderClient";
|
|
28
28
|
import { readEnv } from "@/lib/env";
|
|
29
29
|
import { ORDER_SHIP_STATE_LABEL, type OrderRow } from "@/lib/admin/viewmodels/orders";
|
|
30
30
|
import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
|
|
@@ -38,7 +38,7 @@ interface Props {
|
|
|
38
38
|
const { envelope } = Astro.props;
|
|
39
39
|
const cspNonce = Astro.locals.cspNonce;
|
|
40
40
|
|
|
41
|
-
// Real orders (U3): fetch this tenant's orders
|
|
41
|
+
// Real orders (U3): fetch this tenant's checkout orders via U2's client
|
|
42
42
|
// and map to the view model. The per-tenant bearer is a server-side Worker secret;
|
|
43
43
|
// absent (or any upstream error) → an empty-but-valid model so the tab still
|
|
44
44
|
// renders (rich empty/error UX is U5). Live data is gated on the ToT-core orders
|
|
@@ -51,7 +51,7 @@ const orderFilters = parseOrderFilters(Astro.url.searchParams);
|
|
|
51
51
|
const ordersResult = orderBearer
|
|
52
52
|
? await getOrdersViewModelResult({
|
|
53
53
|
targetTenant: envelope.targetTenant,
|
|
54
|
-
client:
|
|
54
|
+
client: createCheckoutOrderClient({
|
|
55
55
|
baseUrl: orderApiBaseUrl(Astro.locals.customizationEnv),
|
|
56
56
|
appDomain: envelope.targetTenant,
|
|
57
57
|
bearer: orderBearer,
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* lives here now, as explicit props — the caller (Layout.astro) resolves
|
|
11
11
|
* session/feature/theme state and passes it down; this component stays pure.
|
|
12
12
|
*
|
|
13
|
-
* COMMERCE BOUNDARY: the cart is a LINK only
|
|
14
|
-
*
|
|
15
|
-
* layer hydrates later.
|
|
13
|
+
* COMMERCE BOUNDARY: the cart is a LINK only — no cart state, no add-to-cart.
|
|
14
|
+
* The count is a static affordance the managed cart can hydrate later.
|
|
16
15
|
*/
|
|
17
16
|
import type { ChromeNavItem, ChromeLogo, ChromeCta } from "@/lib/chrome/model";
|
|
18
17
|
import MegaMenu from "@/components/chrome/MegaMenu.astro";
|
|
@@ -37,7 +36,7 @@ interface Props {
|
|
|
37
36
|
savedItemsHref?: string;
|
|
38
37
|
/** Marketing conversion CTAs (non-commerce tenants), shown alongside search/saved. */
|
|
39
38
|
headerCtas?: ChromeCta[];
|
|
40
|
-
/** Cart link target
|
|
39
|
+
/** Cart link target. Omit to hide the cart affordance. */
|
|
41
40
|
cartHref?: string;
|
|
42
41
|
cartCount?: number;
|
|
43
42
|
/** A brand-fixed "dark" header bar (e.g. a navy bar with a light logo) vs. the default light bar. */
|