@tokenoftrust/storefront-runner 2.0.1 → 2.0.3
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/dev-plugins/tenant-hot-reload.mjs +19 -1
- package/apps/storefront/dev-plugins/tenant-validate-on-save.mjs +78 -0
- package/apps/storefront/src/middleware/index.ts +17 -1
- package/package.json +1 -1
- package/scripts/lib/tenant-dirs.mjs +80 -0
- package/scripts/tenant/lint-embeds.mjs +180 -0
- package/scripts/tenant/materialize-tenant-content.mjs +533 -0
- package/scripts/tenant/validate.mjs +837 -0
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
- package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
- package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
- package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2978
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
- package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
- package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
- package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
- package/apps/storefront/src/lib/publish/domainState.ts +0 -360
- package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
- package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
- package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
- package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
- package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
- package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
- package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
- package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
- package/apps/storefront/src/pages/admin/review-release.astro +0 -265
- package/apps/storefront/src/pages/admin/select.astro +0 -96
- package/apps/storefront/src/pages/admin.astro +0 -568
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
- package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
- package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
- package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
- package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
- package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
- package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
- package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
- package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
- package/apps/storefront/src/pages/cockpit.astro +0 -1548
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
- package/apps/storefront/src/pages/dashboard/index.astro +0 -160
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Customers tab (baseline §Customers), wired to the live customers view
|
|
4
|
-
* model (lib/admin/services/mock, from data-view-contracts) rather than
|
|
5
|
-
* static prototype data. Cross-links to a customer's orders use the shell's
|
|
6
|
-
* plain #orders hash route (no hard dependency on the orders-tab unit's
|
|
7
|
-
* component code).
|
|
8
|
-
*/
|
|
9
|
-
import { getCustomersViewModel } from "@/lib/admin/services/mock";
|
|
10
|
-
import { formatDate } from "@/lib/format";
|
|
11
|
-
import type { CustomerRow, VerificationState } from "@/lib/admin/viewmodels/customers";
|
|
12
|
-
import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
envelope: AdminTenantEnvelope;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const { envelope } = Astro.props;
|
|
19
|
-
const cspNonce = Astro.locals.cspNonce;
|
|
20
|
-
|
|
21
|
-
const vm = getCustomersViewModel(envelope);
|
|
22
|
-
|
|
23
|
-
const VERIFICATION_TONE: Record<VerificationState, string> = {
|
|
24
|
-
verified: "green",
|
|
25
|
-
pending: "amber",
|
|
26
|
-
blocked: "red",
|
|
27
|
-
not_required: "",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const VERIFICATION_LABEL: Record<VerificationState, string> = {
|
|
31
|
-
verified: "Verified",
|
|
32
|
-
pending: "Pending review",
|
|
33
|
-
blocked: "Blocked",
|
|
34
|
-
not_required: "Not required",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const SEGMENT_TONE: Record<CustomerRow["segment"], string> = {
|
|
38
|
-
vip: "violet",
|
|
39
|
-
returning: "teal",
|
|
40
|
-
new: "",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function viewKeysFor(row: CustomerRow): string[] {
|
|
44
|
-
const keys = ["all", row.segment];
|
|
45
|
-
if (row.verificationState === "blocked" || row.verificationState === "pending") keys.push("needs_review");
|
|
46
|
-
if (row.supportFlags.length > 0) keys.push("support_flagged");
|
|
47
|
-
return keys;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const totalValue = vm.directory.reduce((sum, c) => sum + c.lifetimeValue, 0);
|
|
51
|
-
const vipCount = vm.directory.filter((c) => c.segment === "vip").length;
|
|
52
|
-
|
|
53
|
-
const defaultRow = vm.directory.find((c) => c.id === vm.valuableButBlocked[0]?.customerId) ?? vm.directory[0] ?? null;
|
|
54
|
-
|
|
55
|
-
const SAVED_VIEWS: { key: string; label: string }[] = [
|
|
56
|
-
{ key: "all", label: "All customers" },
|
|
57
|
-
{ key: "vip", label: "VIP" },
|
|
58
|
-
{ key: "returning", label: "Returning" },
|
|
59
|
-
{ key: "new", label: "New" },
|
|
60
|
-
{ key: "needs_review", label: "Needs review" },
|
|
61
|
-
{ key: "support_flagged", label: "Support flags" },
|
|
62
|
-
];
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
<section id="customers" class="band customers-tab" aria-labelledby="admin-tab-customers customers-title" data-admin-panel data-agent-scope="customers" role="tabpanel" data-customers-target-tenant={vm.targetTenant}>
|
|
66
|
-
<div class="section-title">
|
|
67
|
-
<div>
|
|
68
|
-
<h2 id="customers-title">Customers</h2>
|
|
69
|
-
<p>Buyer value, purchase history, support context, and account access in one working surface.</p>
|
|
70
|
-
</div>
|
|
71
|
-
<div class="actions">
|
|
72
|
-
<button class="btn" type="button"><svg><use href="#i-export"></use></svg>Export</button>
|
|
73
|
-
<button class="btn primary" type="button"><svg><use href="#i-users"></use></svg>Create segment</button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div class="grid cols-4">
|
|
78
|
-
<div class="metric"><span>Customers</span><strong>{vm.directory.length}</strong><small>In this directory</small></div>
|
|
79
|
-
<div class="metric"><span>Lifetime value</span><strong>${totalValue.toFixed(2)}</strong><small>Across all known buyers</small></div>
|
|
80
|
-
<div class="metric"><span>VIP customers</span><strong>{vipCount}</strong><small>7+ orders</small></div>
|
|
81
|
-
<div class="metric"><span>Valuable but blocked</span><strong>{vm.valuableButBlocked.length}</strong><small>Recoverable with a direct action</small></div>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
{vm.valuableButBlocked.length > 0 && (
|
|
85
|
-
<div class="panel valuable-blocked-panel">
|
|
86
|
-
<div class="panel-head">
|
|
87
|
-
<h3><svg><use href="#i-ask"></use></svg>Valuable But Blocked</h3>
|
|
88
|
-
<span class="chip amber">Reach-out queue</span>
|
|
89
|
-
</div>
|
|
90
|
-
<div class="panel-body grid">
|
|
91
|
-
<div class="blocked-customer-grid">
|
|
92
|
-
{vm.valuableButBlocked.map((row) => (
|
|
93
|
-
<div class="blocked-customer">
|
|
94
|
-
<div>
|
|
95
|
-
<strong>{row.name}</strong>
|
|
96
|
-
<span class="subtle">${row.recoverableValue.toFixed(2)} recoverable value</span>
|
|
97
|
-
</div>
|
|
98
|
-
<div>
|
|
99
|
-
<span class="eyebrow">Blocker</span>
|
|
100
|
-
<strong>{row.blockerDetail}</strong>
|
|
101
|
-
</div>
|
|
102
|
-
<div>
|
|
103
|
-
<span class="eyebrow">Next action</span>
|
|
104
|
-
<strong>{row.action}</strong>
|
|
105
|
-
</div>
|
|
106
|
-
<button type="button" class="btn" data-customers-select-row={row.customerId}>View customer</button>
|
|
107
|
-
</div>
|
|
108
|
-
))}
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
)}
|
|
113
|
-
|
|
114
|
-
<div class="panel">
|
|
115
|
-
<div class="panel-head">
|
|
116
|
-
<h3><svg><use href="#i-users"></use></svg>Customer Directory</h3>
|
|
117
|
-
<span class="chip teal">{vm.directory.length} customers</span>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="panel-body grid">
|
|
120
|
-
<div class="tabs" role="tablist" aria-label="Customer saved views" data-customers-view-list>
|
|
121
|
-
{SAVED_VIEWS.map((view) => (
|
|
122
|
-
<button type="button" class:list={["tab", { active: view.key === "all" }]} data-customers-view-button data-view-key={view.key}>{view.label}</button>
|
|
123
|
-
))}
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="customer-toolbar" aria-label="Customer filters">
|
|
127
|
-
<label class="customer-search">
|
|
128
|
-
<span>Search</span>
|
|
129
|
-
<input type="search" placeholder="Search customers, email" aria-label="Search customers" data-customers-search />
|
|
130
|
-
</label>
|
|
131
|
-
<label>
|
|
132
|
-
<span>Verification</span>
|
|
133
|
-
<select aria-label="Filter by verification" data-customers-verification-select>
|
|
134
|
-
<option value="">Any status</option>
|
|
135
|
-
<option value="verified">Verified</option>
|
|
136
|
-
<option value="pending">Pending review</option>
|
|
137
|
-
<option value="blocked">Blocked</option>
|
|
138
|
-
</select>
|
|
139
|
-
</label>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
<div class="table-wrap">
|
|
143
|
-
<table class="customer-table">
|
|
144
|
-
<thead>
|
|
145
|
-
<tr>
|
|
146
|
-
<th>Customer</th>
|
|
147
|
-
<th>Customer value</th>
|
|
148
|
-
<th>Last purchase</th>
|
|
149
|
-
<th>Segment</th>
|
|
150
|
-
<th>Support</th>
|
|
151
|
-
<th>Verification</th>
|
|
152
|
-
<th class="action">Action</th>
|
|
153
|
-
</tr>
|
|
154
|
-
</thead>
|
|
155
|
-
<tbody data-customers-tbody>
|
|
156
|
-
{vm.directory.map((row) => (
|
|
157
|
-
<tr
|
|
158
|
-
class:list={[{ selected: row.id === defaultRow?.id }]}
|
|
159
|
-
data-customers-row
|
|
160
|
-
data-customer-id={row.id}
|
|
161
|
-
data-view-keys={viewKeysFor(row).join(" ")}
|
|
162
|
-
data-verification={row.verificationState}
|
|
163
|
-
data-search-haystack={`${row.name} ${row.email}`.toLowerCase()}
|
|
164
|
-
>
|
|
165
|
-
<td>
|
|
166
|
-
<div class="customer-cell">
|
|
167
|
-
<span class="customer-avatar" aria-hidden="true">{row.name.split(" ").map((p) => p[0]).join("").slice(0, 2).toUpperCase()}</span>
|
|
168
|
-
<span>
|
|
169
|
-
<strong>{row.name}</strong>
|
|
170
|
-
<span class="subtle">{row.email}</span>
|
|
171
|
-
</span>
|
|
172
|
-
</div>
|
|
173
|
-
</td>
|
|
174
|
-
<td>
|
|
175
|
-
<strong>${row.lifetimeValue.toFixed(2)}</strong>
|
|
176
|
-
<span class="subtle">{row.orderCount} order{row.orderCount === 1 ? "" : "s"}</span>
|
|
177
|
-
</td>
|
|
178
|
-
<td><strong>{formatDate(row.lastOrderAt ?? undefined) || "—"}</strong></td>
|
|
179
|
-
<td><span class:list={["chip", SEGMENT_TONE[row.segment]]}>{row.segment[0]!.toUpperCase() + row.segment.slice(1)}</span></td>
|
|
180
|
-
<td>{row.supportFlags.length > 0 ? <span class="chip blue">{row.supportFlags[0]}</span> : <span class="subtle">Clear</span>}</td>
|
|
181
|
-
<td><span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span></td>
|
|
182
|
-
<td class="action"><button type="button" class="btn" data-customers-select-row={row.id}>Open</button></td>
|
|
183
|
-
</tr>
|
|
184
|
-
))}
|
|
185
|
-
</tbody>
|
|
186
|
-
</table>
|
|
187
|
-
<p class="customers-empty-state" data-customers-empty hidden>No customers match the current filters.</p>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<div class="customer-detail-grid" data-customers-detail-root>
|
|
193
|
-
{vm.directory.map((row) => {
|
|
194
|
-
const detail = vm.detailsById[row.id]!;
|
|
195
|
-
return (
|
|
196
|
-
<div class="customer-detail-record" data-customers-detail-record={row.id} hidden={row.id !== defaultRow?.id}>
|
|
197
|
-
<div class="panel">
|
|
198
|
-
<div class="panel-head">
|
|
199
|
-
<h3><svg><use href="#i-card"></use></svg>{row.name}</h3>
|
|
200
|
-
<div class="actions">
|
|
201
|
-
<span class:list={["chip", SEGMENT_TONE[row.segment]]}>{row.segment[0]!.toUpperCase() + row.segment.slice(1)} buyer</span>
|
|
202
|
-
<span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span>
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
<div class="panel-body grid">
|
|
206
|
-
<div class="customer-profile-head">
|
|
207
|
-
<div class="customer-cell">
|
|
208
|
-
<span class="customer-avatar large" aria-hidden="true">{row.name.split(" ").map((p) => p[0]).join("").slice(0, 2).toUpperCase()}</span>
|
|
209
|
-
<span>
|
|
210
|
-
<strong>{row.email}</strong>
|
|
211
|
-
<span class="subtle">{row.orderCount} order{row.orderCount === 1 ? "" : "s"} - last purchase {formatDate(row.lastOrderAt ?? undefined) || "—"}</span>
|
|
212
|
-
</span>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<details class="detail-section" open>
|
|
217
|
-
<summary class="detail-section-head"><h4>Profile</h4><span class="chip">{row.orderCount} orders</span></summary>
|
|
218
|
-
<div class="profile-facts">
|
|
219
|
-
<div><span>Lifetime value</span><strong>${row.lifetimeValue.toFixed(2)}</strong></div>
|
|
220
|
-
<div><span>Order count</span><strong>{row.orderCount}</strong></div>
|
|
221
|
-
<div><span>Segment</span><strong>{row.segment[0]!.toUpperCase() + row.segment.slice(1)}</strong></div>
|
|
222
|
-
</div>
|
|
223
|
-
</details>
|
|
224
|
-
|
|
225
|
-
<details class="detail-section" open={detail.orders.length > 0}>
|
|
226
|
-
<summary class="detail-section-head"><h4>Orders</h4><span class="chip">{detail.orders.length}</span></summary>
|
|
227
|
-
<div class="table-wrap compact-table">
|
|
228
|
-
<table>
|
|
229
|
-
<thead><tr><th>Order</th><th>Date</th><th class="money">Total</th></tr></thead>
|
|
230
|
-
<tbody>
|
|
231
|
-
{detail.orders.map((order) => (
|
|
232
|
-
<tr>
|
|
233
|
-
<td><a class="btn" href="#orders">{order.orderId}</a></td>
|
|
234
|
-
<td><strong>{formatDate(order.placedAt)}</strong></td>
|
|
235
|
-
<td class="money"><strong>${order.total.toFixed(2)}</strong></td>
|
|
236
|
-
</tr>
|
|
237
|
-
))}
|
|
238
|
-
</tbody>
|
|
239
|
-
</table>
|
|
240
|
-
</div>
|
|
241
|
-
</details>
|
|
242
|
-
|
|
243
|
-
<details class="detail-section">
|
|
244
|
-
<summary class="detail-section-head"><h4>Activity</h4><span class="chip">{detail.activity.length}</span></summary>
|
|
245
|
-
<div class="customer-activity-list">
|
|
246
|
-
{detail.activity.map((event) => (
|
|
247
|
-
<div>
|
|
248
|
-
<time>{formatDate(event.at)}</time>
|
|
249
|
-
<div><b>{event.title}</b><p>{event.detail}</p></div>
|
|
250
|
-
</div>
|
|
251
|
-
))}
|
|
252
|
-
</div>
|
|
253
|
-
</details>
|
|
254
|
-
|
|
255
|
-
<details class="detail-section">
|
|
256
|
-
<summary class="detail-section-head"><h4>Verification</h4><span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{VERIFICATION_LABEL[row.verificationState]}</span></summary>
|
|
257
|
-
<div class:list={["check-row", { blocking: row.verificationState === "blocked" || row.verificationState === "pending" }]}>
|
|
258
|
-
<div><b>Identity and age</b><p>{row.verificationState === "verified" ? "Verification is current." : row.verificationState === "pending" ? "Verification needs a refresh before the next shipment." : "Verification is blocked — resolve before this customer can order again."}</p></div>
|
|
259
|
-
<strong>{VERIFICATION_LABEL[row.verificationState]}</strong>
|
|
260
|
-
<span class:list={["chip", VERIFICATION_TONE[row.verificationState]]}>{row.verificationState === "verified" ? "OK" : "Action"}</span>
|
|
261
|
-
</div>
|
|
262
|
-
</details>
|
|
263
|
-
|
|
264
|
-
<details class="detail-section">
|
|
265
|
-
<summary class="detail-section-head"><h4>Notes</h4><span class="chip">{detail.notes.length}</span></summary>
|
|
266
|
-
{detail.notes.length > 0 ? (
|
|
267
|
-
<ul class="customer-notes-list">
|
|
268
|
-
{detail.notes.map((note) => <li>{note}</li>)}
|
|
269
|
-
</ul>
|
|
270
|
-
) : (
|
|
271
|
-
<p class="subtle">No notes yet.</p>
|
|
272
|
-
)}
|
|
273
|
-
</details>
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
);
|
|
278
|
-
})}
|
|
279
|
-
</div>
|
|
280
|
-
</section>
|
|
281
|
-
|
|
282
|
-
<script is:inline nonce={cspNonce}>
|
|
283
|
-
(() => {
|
|
284
|
-
const root = document.querySelector('[data-customers-target-tenant]');
|
|
285
|
-
if (!root) return;
|
|
286
|
-
|
|
287
|
-
const viewButtons = Array.from(root.querySelectorAll("[data-customers-view-button]"));
|
|
288
|
-
const verificationSelect = root.querySelector("[data-customers-verification-select]");
|
|
289
|
-
const searchInput = root.querySelector("[data-customers-search]");
|
|
290
|
-
const rows = Array.from(root.querySelectorAll("[data-customers-row]"));
|
|
291
|
-
const emptyState = root.querySelector("[data-customers-empty]");
|
|
292
|
-
const detailRoot = root.querySelector("[data-customers-detail-root]");
|
|
293
|
-
const detailRecords = detailRoot ? Array.from(detailRoot.querySelectorAll("[data-customers-detail-record]")) : [];
|
|
294
|
-
|
|
295
|
-
let activeView = "all";
|
|
296
|
-
|
|
297
|
-
const applyFilters = () => {
|
|
298
|
-
const verification = verificationSelect ? verificationSelect.value : "";
|
|
299
|
-
const term = searchInput ? searchInput.value.trim().toLowerCase() : "";
|
|
300
|
-
let visibleCount = 0;
|
|
301
|
-
|
|
302
|
-
for (const row of rows) {
|
|
303
|
-
const viewKeys = (row.dataset.viewKeys ?? "").split(" ");
|
|
304
|
-
const matchesView = viewKeys.includes(activeView);
|
|
305
|
-
const matchesVerification = !verification || row.dataset.verification === verification;
|
|
306
|
-
const matchesSearch = !term || (row.dataset.searchHaystack ?? "").includes(term);
|
|
307
|
-
const visible = matchesView && matchesVerification && matchesSearch;
|
|
308
|
-
row.hidden = !visible;
|
|
309
|
-
if (visible) visibleCount += 1;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (emptyState) emptyState.hidden = visibleCount > 0;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
const selectCustomer = (customerId) => {
|
|
316
|
-
for (const row of rows) {
|
|
317
|
-
row.classList.toggle("selected", row.dataset.customerId === customerId);
|
|
318
|
-
}
|
|
319
|
-
for (const record of detailRecords) {
|
|
320
|
-
record.hidden = record.dataset.customersDetailRecord !== customerId;
|
|
321
|
-
}
|
|
322
|
-
document.querySelector(`[data-customers-detail-record="${customerId}"]`)?.scrollIntoView({ block: "nearest" });
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
viewButtons.forEach((button) => {
|
|
326
|
-
button.addEventListener("click", () => {
|
|
327
|
-
activeView = button.dataset.viewKey ?? "all";
|
|
328
|
-
viewButtons.forEach((b) => b.classList.toggle("active", b === button));
|
|
329
|
-
applyFilters();
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
verificationSelect?.addEventListener("change", applyFilters);
|
|
334
|
-
searchInput?.addEventListener("input", applyFilters);
|
|
335
|
-
|
|
336
|
-
root.querySelectorAll("[data-customers-select-row]").forEach((button) => {
|
|
337
|
-
button.addEventListener("click", () => {
|
|
338
|
-
const customerId = button.dataset.customersSelectRow;
|
|
339
|
-
if (customerId) selectCustomer(customerId);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
rows.forEach((row) => {
|
|
344
|
-
row.addEventListener("click", (event) => {
|
|
345
|
-
if (event.target instanceof Element && event.target.closest("[data-customers-select-row]")) return;
|
|
346
|
-
const customerId = row.dataset.customerId;
|
|
347
|
-
if (customerId) selectCustomer(customerId);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
applyFilters();
|
|
352
|
-
})();
|
|
353
|
-
</script>
|
|
354
|
-
|
|
355
|
-
<style>
|
|
356
|
-
.customers-tab .section-title .actions {
|
|
357
|
-
align-items: center;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
.valuable-blocked-panel {
|
|
361
|
-
border-color: #f1c883;
|
|
362
|
-
background: #fffdf8;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
.blocked-customer-grid {
|
|
366
|
-
display: grid;
|
|
367
|
-
grid-template-columns: minmax(0, 1fr);
|
|
368
|
-
gap: 10px;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.blocked-customer {
|
|
372
|
-
display: grid;
|
|
373
|
-
grid-template-columns: minmax(0, 1fr) minmax(120px, 0.72fr) minmax(130px, 0.86fr) auto;
|
|
374
|
-
gap: 10px;
|
|
375
|
-
align-items: center;
|
|
376
|
-
min-width: 0;
|
|
377
|
-
padding: 11px;
|
|
378
|
-
border: 1px solid var(--admin-line);
|
|
379
|
-
border-radius: var(--admin-radius-sm);
|
|
380
|
-
background: var(--admin-surface);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
.blocked-customer > div {
|
|
384
|
-
min-width: 0;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.blocked-customer strong {
|
|
388
|
-
display: block;
|
|
389
|
-
overflow-wrap: anywhere;
|
|
390
|
-
font-size: 13px;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
.blocked-customer .eyebrow {
|
|
394
|
-
display: block;
|
|
395
|
-
color: var(--admin-muted);
|
|
396
|
-
font-size: 11px;
|
|
397
|
-
font-weight: 760;
|
|
398
|
-
text-transform: uppercase;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.customer-toolbar {
|
|
402
|
-
display: grid;
|
|
403
|
-
grid-template-columns: minmax(220px, 1.4fr) minmax(150px, 0.8fr);
|
|
404
|
-
gap: 10px;
|
|
405
|
-
align-items: end;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.customer-toolbar label {
|
|
409
|
-
min-width: 0;
|
|
410
|
-
display: grid;
|
|
411
|
-
gap: 5px;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.customer-toolbar span,
|
|
415
|
-
.profile-facts span {
|
|
416
|
-
color: var(--admin-muted);
|
|
417
|
-
font-size: 11px;
|
|
418
|
-
font-weight: 760;
|
|
419
|
-
text-transform: uppercase;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
.customer-toolbar input,
|
|
423
|
-
.customer-toolbar select {
|
|
424
|
-
width: 100%;
|
|
425
|
-
min-height: 38px;
|
|
426
|
-
padding: 0 10px;
|
|
427
|
-
border: 1px solid var(--admin-line-strong);
|
|
428
|
-
border-radius: var(--admin-radius-sm);
|
|
429
|
-
background: var(--admin-surface);
|
|
430
|
-
color: var(--admin-ink);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
.customer-table {
|
|
434
|
-
min-width: 1000px;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.customer-table tbody tr {
|
|
438
|
-
cursor: pointer;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.customer-table tr.selected td {
|
|
442
|
-
background: #fffaf0;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
.customers-empty-state {
|
|
446
|
-
margin: 0;
|
|
447
|
-
padding: 24px 14px;
|
|
448
|
-
color: var(--admin-muted);
|
|
449
|
-
font-size: 13px;
|
|
450
|
-
text-align: center;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.customer-cell {
|
|
454
|
-
display: flex;
|
|
455
|
-
align-items: center;
|
|
456
|
-
gap: 10px;
|
|
457
|
-
min-width: 0;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.customer-avatar {
|
|
461
|
-
width: 36px;
|
|
462
|
-
height: 36px;
|
|
463
|
-
display: grid;
|
|
464
|
-
place-items: center;
|
|
465
|
-
border-radius: 50%;
|
|
466
|
-
background: var(--admin-surface-tint);
|
|
467
|
-
color: var(--admin-teal-dark);
|
|
468
|
-
font-size: 12px;
|
|
469
|
-
font-weight: 780;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
.customer-avatar.large {
|
|
473
|
-
width: 44px;
|
|
474
|
-
height: 44px;
|
|
475
|
-
font-size: 13px;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
.customer-detail-grid {
|
|
479
|
-
display: grid;
|
|
480
|
-
grid-template-columns: minmax(0, 1fr);
|
|
481
|
-
gap: 12px;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
.customer-profile-head {
|
|
485
|
-
display: flex;
|
|
486
|
-
justify-content: space-between;
|
|
487
|
-
gap: 12px;
|
|
488
|
-
align-items: center;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
.profile-facts {
|
|
492
|
-
display: grid;
|
|
493
|
-
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
494
|
-
gap: 10px;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
.profile-facts > div {
|
|
498
|
-
min-width: 0;
|
|
499
|
-
padding: 10px;
|
|
500
|
-
border: 1px solid var(--admin-line);
|
|
501
|
-
border-radius: var(--admin-radius-sm);
|
|
502
|
-
background: var(--admin-surface-soft);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
.profile-facts strong {
|
|
506
|
-
display: block;
|
|
507
|
-
margin-top: 4px;
|
|
508
|
-
font-size: 14px;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
.compact-table table {
|
|
512
|
-
min-width: 400px;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
.detail-section {
|
|
516
|
-
display: grid;
|
|
517
|
-
gap: 10px;
|
|
518
|
-
padding-top: 2px;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
.detail-section + .detail-section {
|
|
522
|
-
padding-top: 12px;
|
|
523
|
-
border-top: 1px solid var(--admin-line);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
.detail-section-head {
|
|
527
|
-
display: flex;
|
|
528
|
-
align-items: center;
|
|
529
|
-
justify-content: space-between;
|
|
530
|
-
gap: 10px;
|
|
531
|
-
cursor: pointer;
|
|
532
|
-
list-style: none;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
.detail-section-head::-webkit-details-marker {
|
|
536
|
-
display: none;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
.detail-section-head h4 {
|
|
540
|
-
margin: 0;
|
|
541
|
-
font-size: 13px;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.customer-activity-list {
|
|
545
|
-
display: grid;
|
|
546
|
-
gap: 10px;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
.customer-activity-list > div {
|
|
550
|
-
display: grid;
|
|
551
|
-
grid-template-columns: 90px minmax(0, 1fr);
|
|
552
|
-
gap: 10px;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
.customer-activity-list time {
|
|
556
|
-
color: var(--admin-muted);
|
|
557
|
-
font-size: 12px;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
.customer-activity-list p {
|
|
561
|
-
margin: 2px 0 0;
|
|
562
|
-
color: var(--admin-muted);
|
|
563
|
-
font-size: 12px;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
.customer-notes-list {
|
|
567
|
-
margin: 0;
|
|
568
|
-
padding-left: 18px;
|
|
569
|
-
font-size: 13px;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
@media (max-width: 1120px) {
|
|
573
|
-
.customer-toolbar {
|
|
574
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
@media (max-width: 760px) {
|
|
579
|
-
.blocked-customer {
|
|
580
|
-
grid-template-columns: 1fr;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
.customer-toolbar,
|
|
584
|
-
.profile-facts {
|
|
585
|
-
grid-template-columns: minmax(0, 1fr);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
.customer-profile-head {
|
|
589
|
-
align-items: flex-start;
|
|
590
|
-
flex-direction: column;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
</style>
|