@blamejs/blamejs-shop 0.3.53 → 0.3.55

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/lib/admin.js CHANGED
@@ -181,6 +181,16 @@ function _htmlEscape(s) {
181
181
  return b.template.escapeHtml(String(s));
182
182
  }
183
183
 
184
+ // Wrap a built data-table string in a horizontal-scroll container so a
185
+ // narrow viewport scrolls the table rather than the whole page (.table-wrap
186
+ // is defined in admin.css — WCAG 1.4.10 reflow). Layout-only tables
187
+ // (order-totals) are NOT wrapped — they never overflow. `tableHtml` is
188
+ // already-escaped markup assembled by the caller; this helper interpolates
189
+ // no untrusted data, so it is XSS-neutral.
190
+ function _tableWrap(tableHtml) {
191
+ return "<div class=\"table-wrap\">" + tableHtml + "</div>";
192
+ }
193
+
184
194
  // Strict integer coercion for money / count form fields. Refuses
185
195
  // "50abc" / "" / floats, unlike parseInt's loose prefix match (which
186
196
  // silently turns "50abc" into 50). Accepts an already-numeric value
@@ -515,6 +525,8 @@ function mount(router, deps) {
515
525
  var complianceExport = deps.complianceExport || null; // DSR queue (export/erasure) disabled when absent
516
526
  var orderExchanges = deps.orderExchanges || null; // exchange queue + FSM-action console disabled when absent
517
527
  var orderRatings = deps.orderRatings || null; // per-order rating moderation queue (flag/clear + public reply) disabled when absent
528
+ var clickAndCollect = deps.clickAndCollect || null; // pickup-locations CRUD + pickup queue console disabled when absent
529
+ var giftOptions = deps.giftOptions || null; // gift-wrap catalog console disabled when absent
518
530
  var searchRanking = deps.searchRanking || null; // search-ranking weight-set + pin console disabled when absent
519
531
  var trustBadges = deps.trustBadges || null; // trust-badge authoring console disabled when absent
520
532
  var preorder = deps.preorder || null; // pre-order campaign console (define/launch/close) disabled when absent
@@ -531,7 +543,7 @@ function mount(router, deps) {
531
543
  // `reports` is always present in the nav (read-only sales summary needs no
532
544
  // extra dep); its route mounts unconditionally and renders an unconfigured
533
545
  // notice when the salesReports primitive isn't wired.
534
- var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, preorder: !!deps.preorder, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, customerSegments: !!customerSegments, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, blog: !!deps.blog, knowledgeBase: !!deps.knowledgeBase, customerSurveys: !!deps.customerSurveys, storefrontPages: !!deps.storefrontPages, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, autoDiscount: !!deps.autoDiscount, discountAllocation: !!deps.discountAllocation, quantityDiscounts: !!deps.quantityDiscounts, loyalty: !!deps.loyalty, pickLists: !!pickLists, salesTaxFilings: !!salesTaxFilings, shippingLabels: !!shippingLabels, supportTickets: !!supportTickets, complianceExport: !!complianceExport, orderExchanges: !!orderExchanges, orderRatings: !!orderRatings, searchRanking: !!searchRanking, trustBadges: !!trustBadges, orderExport: !!orderExport, auditLog: auditLog };
546
+ var navAvailable = { returns: !!returns, reviews: !!reviews, productQa: !!productQa, subscriptions: !!deps.subscriptions, preorder: !!deps.preorder, webhooks: !!deps.webhooks, collections: !!deps.collections, customers: !!deps.customers, customerSegments: !!customerSegments, giftcards: !!deps.giftcards, announcementBar: !!deps.announcementBar, blog: !!deps.blog, knowledgeBase: !!deps.knowledgeBase, customerSurveys: !!deps.customerSurveys, storefrontPages: !!deps.storefrontPages, businessHours: !!deps.businessHours, taxRates: !!deps.taxRates, shippingZones: !!deps.shippingZones, autoDiscount: !!deps.autoDiscount, discountAllocation: !!deps.discountAllocation, quantityDiscounts: !!deps.quantityDiscounts, loyalty: !!deps.loyalty, pickLists: !!pickLists, salesTaxFilings: !!salesTaxFilings, shippingLabels: !!shippingLabels, supportTickets: !!supportTickets, complianceExport: !!complianceExport, orderExchanges: !!orderExchanges, orderRatings: !!orderRatings, clickAndCollect: !!clickAndCollect, giftOptions: !!giftOptions, searchRanking: !!searchRanking, trustBadges: !!trustBadges, orderExport: !!orderExport, auditLog: auditLog };
535
547
 
536
548
  try { b.audit.registerNamespace(AUDIT_NAMESPACE); } catch (_e) { /* idempotent */ }
537
549
 
@@ -4596,6 +4608,318 @@ function mount(router, deps) {
4596
4608
  ));
4597
4609
  }
4598
4610
 
4611
+ // ---- click-and-collect (pickup locations + the pickup queue) --------
4612
+ //
4613
+ // Two operator surfaces: the pickup-location CRUD (define / archive) and
4614
+ // the front-counter pickup queue per location (the FSM: ready / picked-up
4615
+ // / no-show). The primitive throws PLAIN TypeError (no `.code`) on every
4616
+ // refusal EXCEPT markPickedUp's internal `fsm/illegal-transition` handling,
4617
+ // so the action wrapper maps TypeError → 400/?err=1 — but RESOLVES EXISTENCE
4618
+ // FIRST (getScheduleByOrder / getLocation null → 404) so a not-found isn't
4619
+ // mislabeled a bad-shape 400.
4620
+ if (clickAndCollect) {
4621
+ // Parse the operator's hours textarea (one "Mon 09:00-17:00" line per
4622
+ // weekday) into the hours_json object the primitive validates. An empty
4623
+ // textarea yields {} (the primitive only requires a JSON-serialisable
4624
+ // object). Unparseable lines are skipped so a typo doesn't 500 the save.
4625
+ function _parseHoursJson(raw) {
4626
+ var hours = {};
4627
+ if (typeof raw !== "string" || !raw.trim()) return hours;
4628
+ raw.split(/\r?\n/).forEach(function (line) {
4629
+ var m = line.trim().match(/^([A-Za-z]{3,})\s+(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})$/);
4630
+ if (m) hours[m[1].toLowerCase().slice(0, 3)] = { open: m[2], close: m[3] };
4631
+ });
4632
+ return hours;
4633
+ }
4634
+
4635
+ // GET /admin/pickup-locations — the active-location list (the primitive
4636
+ // has no "list all incl archived" verb; archived rows drop out of
4637
+ // availableLocations, which is the operator's working set — re-open an
4638
+ // archived-locations list if operators ask for it).
4639
+ router.get("/admin/pickup-locations", _pageOrApi(true,
4640
+ R(async function (req, res) {
4641
+ var rows = await clickAndCollect.availableLocations({ limit: 200 });
4642
+ _json(res, 200, { rows: rows });
4643
+ }),
4644
+ async function (req, res) {
4645
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
4646
+ var rows = await clickAndCollect.availableLocations({ limit: 200 });
4647
+ _sendHtml(res, 200, renderAdminPickupLocations({
4648
+ shop_name: deps.shop_name, nav_available: navAvailable, locations: rows,
4649
+ saved: url && url.searchParams.get("saved"),
4650
+ notice: url && url.searchParams.get("err") ? "That location couldn't be saved — check the fields and try again." : null,
4651
+ }));
4652
+ },
4653
+ ));
4654
+
4655
+ // POST /admin/pickup-locations — definePickupLocation upsert. A bad
4656
+ // shape (missing name / bad capacity / bad address) is a TypeError → 400.
4657
+ router.post("/admin/pickup-locations", _pageOrApi(false,
4658
+ W("pickup.location.define", async function (req, res) {
4659
+ var body = req.body || {};
4660
+ var loc;
4661
+ try { loc = await _definePickupFromBody(body); }
4662
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4663
+ _json(res, 201, loc);
4664
+ return loc;
4665
+ }),
4666
+ async function (req, res) {
4667
+ try { await _definePickupFromBody(req.body || {}); }
4668
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/pickup-locations?err=1"); throw e; }
4669
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".pickup.location.define", outcome: "success", metadata: { code: (req.body || {}).code } });
4670
+ _redirect(res, "/admin/pickup-locations?saved=1");
4671
+ },
4672
+ ));
4673
+
4674
+ function _definePickupFromBody(body) {
4675
+ return clickAndCollect.definePickupLocation({
4676
+ code: body.code,
4677
+ name: body.name,
4678
+ address: { line1: body.line1, city: body.city, country: body.country },
4679
+ hours_json: _parseHoursJson(body.hours),
4680
+ capacity_per_hour: parseInt(body.capacity_per_hour, 10),
4681
+ lead_time_hours: parseInt(body.lead_time_hours, 10),
4682
+ active: !(body.active === "0" || body.active === false),
4683
+ });
4684
+ }
4685
+
4686
+ // POST /admin/pickup-locations/:code/archive — soft-delete. Resolve
4687
+ // existence first (getLocation null → 404) so a typo'd code is a 404, not
4688
+ // a 400/500. The archive verb itself only throws on a malformed code.
4689
+ router.post("/admin/pickup-locations/:code/archive", _pageOrApi(false,
4690
+ W("pickup.location.archive", async function (req, res) {
4691
+ var code = req.params.code;
4692
+ var existing;
4693
+ try { existing = await clickAndCollect.getLocation(code); }
4694
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4695
+ if (!existing) return _problem(res, 404, "pickup-location-not-found");
4696
+ var row = await clickAndCollect.archiveLocation(code);
4697
+ _json(res, 200, row);
4698
+ return row;
4699
+ }),
4700
+ async function (req, res) {
4701
+ var code = req.params.code;
4702
+ var existing;
4703
+ try { existing = await clickAndCollect.getLocation(code); }
4704
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/pickup-locations?err=1"); throw e; }
4705
+ if (!existing) return _redirect(res, "/admin/pickup-locations?err=1");
4706
+ await clickAndCollect.archiveLocation(code);
4707
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".pickup.location.archive", outcome: "success", metadata: { code: code } });
4708
+ _redirect(res, "/admin/pickup-locations?saved=1");
4709
+ },
4710
+ ));
4711
+
4712
+ // GET /admin/pickups — the per-location pickup queue. The primitive has
4713
+ // no all-locations queue verb, so the screen requires a ?location=
4714
+ // selector defaulting to the first active location. A wide window
4715
+ // (now-7d … now+30d) bounds the read.
4716
+ router.get("/admin/pickups", _pageOrApi(true,
4717
+ R(async function (req, res) {
4718
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
4719
+ var locs = await clickAndCollect.availableLocations({ limit: 200 });
4720
+ var selected = (url && url.searchParams.get("location")) || (locs[0] && locs[0].code) || null;
4721
+ var status = (url && url.searchParams.get("status")) || null;
4722
+ var rows = selected ? await _pickupQueue(selected, status) : [];
4723
+ _json(res, 200, { rows: rows, location: selected, status: status });
4724
+ }),
4725
+ async function (req, res) {
4726
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
4727
+ var locs = await clickAndCollect.availableLocations({ limit: 200 });
4728
+ var selected = (url && url.searchParams.get("location")) || (locs[0] && locs[0].code) || null;
4729
+ var status = (url && url.searchParams.get("status")) || null;
4730
+ var rows = [];
4731
+ if (selected) {
4732
+ try { rows = await _pickupQueue(selected, status); }
4733
+ catch (e) { if (!(e instanceof TypeError)) throw e; rows = []; }
4734
+ }
4735
+ _sendHtml(res, 200, renderAdminPickups({
4736
+ shop_name: deps.shop_name, nav_available: navAvailable,
4737
+ locations: locs, selected: selected, status: status, pickups: rows,
4738
+ moved: url && url.searchParams.get("moved"),
4739
+ notice: url && url.searchParams.get("err") ? "That pickup action couldn't be completed." : null,
4740
+ }));
4741
+ },
4742
+ ));
4743
+
4744
+ function _pickupQueue(locationCode, status) {
4745
+ var now = Date.now();
4746
+ var opts2 = {
4747
+ location_code: locationCode,
4748
+ from: now - b.constants.TIME.days(7),
4749
+ to: now + b.constants.TIME.days(30),
4750
+ };
4751
+ if (status && PICKUP_QUEUE_STATUSES.indexOf(status) !== -1) opts2.status = status;
4752
+ return clickAndCollect.pickupsForLocation(opts2);
4753
+ }
4754
+
4755
+ // The three FSM action routes. Each RESOLVES EXISTENCE FIRST
4756
+ // (getScheduleByOrder null → 404) so a not-found order isn't mislabeled a
4757
+ // 400; a real refusal (wrong status / bad shape) is a TypeError → 400.
4758
+ function _pickupAction(verb, auditEvent, opFn) {
4759
+ router.post("/admin/pickups/:order_id/" + verb, _pageOrApi(false,
4760
+ W("pickup." + auditEvent, async function (req, res) {
4761
+ var orderId = req.params.order_id;
4762
+ var existing;
4763
+ try { existing = await clickAndCollect.getScheduleByOrder(orderId); }
4764
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4765
+ if (!existing) return _problem(res, 404, "pickup-schedule-not-found");
4766
+ var row;
4767
+ try { row = await opFn(orderId, req.body || {}); }
4768
+ catch (e) {
4769
+ if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message);
4770
+ if (e && e.code === "fsm/illegal-transition") return _problem(res, 409, "conflict", e.message);
4771
+ throw e;
4772
+ }
4773
+ _json(res, 200, row);
4774
+ return row;
4775
+ }),
4776
+ async function (req, res) {
4777
+ var orderId = req.params.order_id;
4778
+ var existing;
4779
+ try { existing = await clickAndCollect.getScheduleByOrder(orderId); }
4780
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/pickups?err=1"); throw e; }
4781
+ if (!existing) return _redirect(res, "/admin/pickups?err=1");
4782
+ var backLoc = "&location=" + encodeURIComponent(existing.location_code);
4783
+ try { await opFn(orderId, req.body || {}); }
4784
+ catch (e) {
4785
+ if (e instanceof TypeError || (e && e.code === "fsm/illegal-transition")) {
4786
+ return _redirect(res, "/admin/pickups?err=1" + backLoc);
4787
+ }
4788
+ throw e;
4789
+ }
4790
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".pickup." + auditEvent, outcome: "success", metadata: { order_id: orderId } });
4791
+ _redirect(res, "/admin/pickups?moved=1" + backLoc);
4792
+ },
4793
+ ));
4794
+ }
4795
+ _pickupAction("ready", "ready", function (orderId) {
4796
+ return clickAndCollect.markReadyForPickup({ order_id: orderId });
4797
+ });
4798
+ _pickupAction("picked-up", "picked_up", function (orderId, body) {
4799
+ return clickAndCollect.markPickedUp({
4800
+ order_id: orderId, picked_up_at: Date.now(),
4801
+ signature: body.signature || undefined,
4802
+ customer_id_proof_kind: body.proof_kind || undefined,
4803
+ });
4804
+ });
4805
+ _pickupAction("no-show", "no_show", function (orderId, body) {
4806
+ return clickAndCollect.markNoShow({ order_id: orderId, reason: body.reason });
4807
+ });
4808
+ }
4809
+
4810
+ // ---- gift wraps -----------------------------------------------------
4811
+ //
4812
+ // The operator-defined gift-wrap catalog: define / update / archive a wrap
4813
+ // option whose wrap_sku is a real catalog variant (so the fee flows through
4814
+ // inventory + pricing). defineWrap / updateWrap / archiveWrap throw PLAIN
4815
+ // TypeError on a bad shape / unknown sku / not-found → 400; update/archive
4816
+ // resolve existence first (getWrap null → 404) for parity.
4817
+ if (giftOptions) {
4818
+ router.get("/admin/gift-wraps", _pageOrApi(true,
4819
+ R(async function (req, res) {
4820
+ var rows = await giftOptions.listWraps({});
4821
+ _json(res, 200, { rows: rows });
4822
+ }),
4823
+ async function (req, res) {
4824
+ var url = req.url ? new URL(req.url, "http://localhost") : null;
4825
+ var rows = await giftOptions.listWraps({});
4826
+ _sendHtml(res, 200, renderAdminGiftWraps({
4827
+ shop_name: deps.shop_name, nav_available: navAvailable, wraps: rows,
4828
+ saved: url && url.searchParams.get("saved"),
4829
+ notice: url && url.searchParams.get("err") ? "That wrap couldn't be saved — the SKU must be a real catalog variant." : null,
4830
+ }));
4831
+ },
4832
+ ));
4833
+
4834
+ router.post("/admin/gift-wraps", _pageOrApi(false,
4835
+ W("gift.wrap.define", async function (req, res) {
4836
+ var wrap;
4837
+ try { wrap = await _defineWrapFromBody(req.body || {}); }
4838
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4839
+ _json(res, 201, wrap);
4840
+ return wrap;
4841
+ }),
4842
+ async function (req, res) {
4843
+ try { await _defineWrapFromBody(req.body || {}); }
4844
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/gift-wraps?err=1"); throw e; }
4845
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift.wrap.define", outcome: "success", metadata: { wrap_sku: (req.body || {}).wrap_sku } });
4846
+ _redirect(res, "/admin/gift-wraps?saved=1");
4847
+ },
4848
+ ));
4849
+
4850
+ function _defineWrapFromBody(body) {
4851
+ return giftOptions.defineWrap({
4852
+ wrap_sku: body.wrap_sku,
4853
+ title: body.title,
4854
+ fee_minor: parseInt(body.fee_minor, 10),
4855
+ image_url: body.image_url ? body.image_url : undefined,
4856
+ max_per_order: body.max_per_order ? parseInt(body.max_per_order, 10) : undefined,
4857
+ active: !(body.active === "0" || body.active === false),
4858
+ });
4859
+ }
4860
+
4861
+ router.post("/admin/gift-wraps/:wrap_sku/update", _pageOrApi(false,
4862
+ W("gift.wrap.update", async function (req, res) {
4863
+ var sku = req.params.wrap_sku;
4864
+ var existing;
4865
+ try { existing = await giftOptions.getWrap(sku); }
4866
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4867
+ if (!existing) return _problem(res, 404, "gift-wrap-not-found");
4868
+ var wrap;
4869
+ try { wrap = await giftOptions.updateWrap(sku, _wrapPatchFromBody(req.body || {})); }
4870
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4871
+ _json(res, 200, wrap);
4872
+ return wrap;
4873
+ }),
4874
+ async function (req, res) {
4875
+ var sku = req.params.wrap_sku;
4876
+ var existing;
4877
+ try { existing = await giftOptions.getWrap(sku); }
4878
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/gift-wraps?err=1"); throw e; }
4879
+ if (!existing) return _redirect(res, "/admin/gift-wraps?err=1");
4880
+ try { await giftOptions.updateWrap(sku, _wrapPatchFromBody(req.body || {})); }
4881
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/gift-wraps?err=1"); throw e; }
4882
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift.wrap.update", outcome: "success", metadata: { wrap_sku: sku } });
4883
+ _redirect(res, "/admin/gift-wraps?saved=1");
4884
+ },
4885
+ ));
4886
+
4887
+ // Build the updateWrap patch from the edit form — only the columns the
4888
+ // operator actually changed (a blank field is omitted, never sent as "").
4889
+ function _wrapPatchFromBody(body) {
4890
+ var patch = {};
4891
+ if (body.title != null && body.title !== "") patch.title = body.title;
4892
+ if (body.fee_minor != null && body.fee_minor !== "") patch.fee_minor = parseInt(body.fee_minor, 10);
4893
+ if (body.image_url != null && body.image_url !== "") patch.image_url = body.image_url;
4894
+ if (body.max_per_order != null && body.max_per_order !== "") patch.max_per_order = parseInt(body.max_per_order, 10);
4895
+ if (body.active != null) patch.active = !(body.active === "0" || body.active === false);
4896
+ return patch;
4897
+ }
4898
+
4899
+ router.post("/admin/gift-wraps/:wrap_sku/archive", _pageOrApi(false,
4900
+ W("gift.wrap.archive", async function (req, res) {
4901
+ var sku = req.params.wrap_sku;
4902
+ var existing;
4903
+ try { existing = await giftOptions.getWrap(sku); }
4904
+ catch (e) { if (e instanceof TypeError) return _problem(res, 400, "bad-request", e.message); throw e; }
4905
+ if (!existing) return _problem(res, 404, "gift-wrap-not-found");
4906
+ var wrap = await giftOptions.archiveWrap(sku);
4907
+ _json(res, 200, wrap);
4908
+ return wrap;
4909
+ }),
4910
+ async function (req, res) {
4911
+ var sku = req.params.wrap_sku;
4912
+ var existing;
4913
+ try { existing = await giftOptions.getWrap(sku); }
4914
+ catch (e) { if (e instanceof TypeError) return _redirect(res, "/admin/gift-wraps?err=1"); throw e; }
4915
+ if (!existing) return _redirect(res, "/admin/gift-wraps?err=1");
4916
+ await giftOptions.archiveWrap(sku);
4917
+ b.audit.safeEmit({ action: AUDIT_NAMESPACE + ".gift.wrap.archive", outcome: "success", metadata: { wrap_sku: sku } });
4918
+ _redirect(res, "/admin/gift-wraps?saved=1");
4919
+ },
4920
+ ));
4921
+ }
4922
+
4599
4923
  // ---- config ---------------------------------------------------------
4600
4924
 
4601
4925
  var config = deps.config || null;
@@ -10194,7 +10518,7 @@ function renderDashboard(opts) {
10194
10518
  }).join("");
10195
10519
  otherCurrencies =
10196
10520
  "<section><h2>Other currencies in window</h2><div class=\"panel\">" +
10197
- "<table><thead><tr><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>" +
10521
+ _tableWrap("<table><thead><tr><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + rows + "</tbody></table>") +
10198
10522
  "</div></section>";
10199
10523
  }
10200
10524
 
@@ -10228,11 +10552,11 @@ function renderDashboard(opts) {
10228
10552
  "<section><h2>Catalog + activity</h2><div class=\"two-col\">" +
10229
10553
  " <div class=\"panel\">" +
10230
10554
  " <h3 class=\"subhead\">Top SKUs by units sold</h3>" +
10231
- " <table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
10555
+ " " + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>") +
10232
10556
  " </div>" +
10233
10557
  " <div class=\"panel\">" +
10234
10558
  " <h3 class=\"subhead\">Recent orders</h3>" +
10235
- " <table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>" +
10559
+ " " + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th></tr></thead><tbody>" + recentRows + "</tbody></table>") +
10236
10560
  " </div>" +
10237
10561
  "</div></section>";
10238
10562
 
@@ -10277,6 +10601,9 @@ var ADMIN_NAV_ITEMS = [
10277
10601
  { key: "dsr", href: "/admin/dsr", label: "Privacy requests", requires: "complianceExport" },
10278
10602
  { key: "exchanges", href: "/admin/exchanges", label: "Exchanges", requires: "orderExchanges" },
10279
10603
  { key: "ratings", href: "/admin/ratings", label: "Ratings", requires: "orderRatings" },
10604
+ { key: "pickup-locations", href: "/admin/pickup-locations", label: "Pickup locations", requires: "clickAndCollect" },
10605
+ { key: "pickups", href: "/admin/pickups", label: "Pickups", requires: "clickAndCollect" },
10606
+ { key: "gift-wraps", href: "/admin/gift-wraps", label: "Gift wraps", requires: "giftOptions" },
10280
10607
  { key: "search-ranking", href: "/admin/search-ranking", label: "Search ranking", requires: "searchRanking" },
10281
10608
  { key: "trust-badges", href: "/admin/trust-badges", label: "Trust badges", requires: "trustBadges" },
10282
10609
  { key: "reviews", href: "/admin/reviews", label: "Reviews", requires: "reviews" },
@@ -10490,10 +10817,10 @@ function renderAdminIntegrations(opts) {
10490
10817
  "<section>" +
10491
10818
  "<h2>Integrations</h2>" +
10492
10819
  "<p class=\"meta\">Every integration is off until you supply its credentials — set them as deployment secrets, then redeploy. Nothing is enabled without your keys.</p>" +
10493
- "<div class=\"panel\"><table>" +
10820
+ "<div class=\"panel\">" + _tableWrap("<table>" +
10494
10821
  "<thead><tr><th scope=\"col\">Integration</th><th scope=\"col\">Status</th><th scope=\"col\">To enable</th></tr></thead>" +
10495
10822
  "<tbody>" + rows + "</tbody>" +
10496
- "</table></div>" +
10823
+ "</table>") + "</div>" +
10497
10824
  "<p class=\"meta mt-125\">Sign in with Apple and PayPal are planned. “Sign in with Shop” / Shop Pay isn't available to a self-hosted store. See the README “Optional integrations” section for full setup steps.</p>" +
10498
10825
  "<div class=\"actions-row\"><a class=\"btn btn--ghost\" href=\"/admin\">Back</a></div>" +
10499
10826
  "</section>";
@@ -10519,7 +10846,7 @@ function renderAdminProducts(opts) {
10519
10846
  "</div></td></tr>";
10520
10847
  }).join("");
10521
10848
  var table = products.length
10522
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
10849
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10523
10850
  : "<p class=\"empty\">No products yet — create your first one below.</p>";
10524
10851
  var body =
10525
10852
  "<section><h2>Products</h2>" + created + notice + table +
@@ -10575,7 +10902,7 @@ function renderAdminOrders(opts) {
10575
10902
  }).join("");
10576
10903
 
10577
10904
  var table = orders.length
10578
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
10905
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10579
10906
  : "<p class=\"empty\">No orders" + (active ? " with status “" + _htmlEscape(active) + "”" : " yet") + ".</p>";
10580
10907
 
10581
10908
  var body = "<section><h2>Orders</h2>" + notice + chips + table + "</section>";
@@ -10647,7 +10974,7 @@ function renderAdminAudit(opts) {
10647
10974
  }).join("");
10648
10975
 
10649
10976
  var table = rows.length
10650
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Time</th><th scope=\"col\">Action</th><th scope=\"col\">Outcome</th><th scope=\"col\">Actor</th><th scope=\"col\">Resource</th><th scope=\"col\">Details</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
10977
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Time</th><th scope=\"col\">Action</th><th scope=\"col\">Outcome</th><th scope=\"col\">Actor</th><th scope=\"col\">Resource</th><th scope=\"col\">Details</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
10651
10978
  : "<p class=\"empty\">No audit events match.</p>";
10652
10979
 
10653
10980
  // Pager: Newer steps back one page (disabled at offset 0); Older steps
@@ -10738,7 +11065,7 @@ function renderAdminReports(opts) {
10738
11065
  }).join("");
10739
11066
  var funnelBlock =
10740
11067
  "<section><h2>Order status</h2><div class=\"panel\">" +
10741
- "<table><thead><tr><th scope=\"col\">Stage</th><th scope=\"col\" class=\"num\">Orders</th></tr></thead><tbody>" + funnelRows + "</tbody></table>" +
11068
+ _tableWrap("<table><thead><tr><th scope=\"col\">Stage</th><th scope=\"col\" class=\"num\">Orders</th></tr></thead><tbody>" + funnelRows + "</tbody></table>") +
10742
11069
  "</div></section>";
10743
11070
 
10744
11071
  // Top products by gross revenue across the window.
@@ -10752,7 +11079,7 @@ function renderAdminReports(opts) {
10752
11079
  : "<tr><td colspan=\"3\" class=\"empty\">No sales in this window.</td></tr>";
10753
11080
  var topBlock =
10754
11081
  "<section><h2>Top products</h2><div class=\"panel\">" +
10755
- "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>" +
11082
+ _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Units</th><th scope=\"col\" class=\"num\">Revenue</th></tr></thead><tbody>" + topRows + "</tbody></table>") +
10756
11083
  "</div></section>";
10757
11084
 
10758
11085
  // By-day revenue series — one row per (day, currency) bucket.
@@ -10771,7 +11098,7 @@ function renderAdminReports(opts) {
10771
11098
  : "<tr><td colspan=\"6\" class=\"empty\">No sales in this window.</td></tr>";
10772
11099
  var dayBlock =
10773
11100
  "<section><h2>By day</h2><div class=\"panel\">" +
10774
- "<table><thead><tr><th scope=\"col\">Date</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Gross</th><th scope=\"col\" class=\"num\">Net</th><th scope=\"col\" class=\"num\">Refunds</th></tr></thead><tbody>" + dayRows + "</tbody></table>" +
11101
+ _tableWrap("<table><thead><tr><th scope=\"col\">Date</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Gross</th><th scope=\"col\" class=\"num\">Net</th><th scope=\"col\" class=\"num\">Refunds</th></tr></thead><tbody>" + dayRows + "</tbody></table>") +
10775
11102
  "</div></section>";
10776
11103
 
10777
11104
  var body =
@@ -10863,7 +11190,7 @@ function renderAdminExports(opts) {
10863
11190
  "</tr>";
10864
11191
  }).join("");
10865
11192
  var jobTable = exportsRows.length
10866
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Format</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rows</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + jobRows + "</tbody></table></div>"
11193
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Format</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rows</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + jobRows + "</tbody></table>") + "</div>"
10867
11194
  : "<p class=\"empty\">No scheduled exports yet.</p>";
10868
11195
 
10869
11196
  var scheduleFmtOptions = formats.map(function (f) {
@@ -10966,7 +11293,7 @@ function renderAdminShippingLabels(opts) {
10966
11293
 
10967
11294
  var rows = labels.map(_shippingLabelRow).join("");
10968
11295
  var table = labels.length
10969
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11296
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10970
11297
  : "<p class=\"empty\">No " + _htmlEscape(status) + " labels" + (status === "voided" ? " in this window" : "") + ".</p>";
10971
11298
 
10972
11299
  var body =
@@ -10988,7 +11315,7 @@ function renderAdminShippingLabelsPending(opts) {
10988
11315
  var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
10989
11316
  var rows = labels.map(_shippingLabelRow).join("");
10990
11317
  var table = labels.length
10991
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11318
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Broker</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\">Created</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
10992
11319
  : "<p class=\"empty\">No labels awaiting a broker mint.</p>";
10993
11320
  var body =
10994
11321
  "<section><h2>Shipping labels</h2>" + notice +
@@ -11044,7 +11371,7 @@ function renderAdminShippingLabelCosts(opts) {
11044
11371
  : "<tr><td colspan=\"4\" class=\"empty\">No labels purchased in this window.</td></tr>";
11045
11372
  var brokerBlock =
11046
11373
  "<section><h2>By broker</h2><div class=\"panel\">" +
11047
- "<table><thead><tr><th scope=\"col\">Broker</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Labels</th><th scope=\"col\" class=\"num\">Spend</th></tr></thead><tbody>" + brokerRows + "</tbody></table>" +
11374
+ _tableWrap("<table><thead><tr><th scope=\"col\">Broker</th><th scope=\"col\">Currency</th><th scope=\"col\" class=\"num\">Labels</th><th scope=\"col\" class=\"num\">Spend</th></tr></thead><tbody>" + brokerRows + "</tbody></table>") +
11048
11375
  "</div></section>";
11049
11376
 
11050
11377
  var body =
@@ -11077,7 +11404,7 @@ function renderAdminOrder(opts) {
11077
11404
  "</tr>";
11078
11405
  }).join("");
11079
11406
  var linesTable = (o.lines && o.lines.length)
11080
- ? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\" class=\"num\">Unit</th><th scope=\"col\" class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
11407
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\" class=\"num\">Unit</th><th scope=\"col\" class=\"num\">Line</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
11081
11408
  : "<p class=\"empty\">No line items recorded.</p>";
11082
11409
 
11083
11410
  function _total(label, minor, strong) {
@@ -11165,7 +11492,7 @@ function renderAdminOrder(opts) {
11165
11492
  "<td>" + _htmlEscape(_fmtDate(e.occurred_at)) + "</td></tr>";
11166
11493
  }).join("");
11167
11494
  var eventsTable = (s.events && s.events.length)
11168
- ? "<table><thead><tr><th scope=\"col\">Status</th><th scope=\"col\">Location</th><th scope=\"col\">When</th></tr></thead><tbody>" + eventRows + "</tbody></table>"
11495
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Status</th><th scope=\"col\">Location</th><th scope=\"col\">When</th></tr></thead><tbody>" + eventRows + "</tbody></table>")
11169
11496
  : "<p class=\"empty\">No carrier events yet.</p>";
11170
11497
  var statusOpts = statuses.map(function (st) {
11171
11498
  return "<option value=\"" + _htmlEscape(st) + "\">" + _htmlEscape(st) + "</option>";
@@ -11291,7 +11618,7 @@ function _orderLabelPanel(orderId, shipment, carriers, packageTypes, purchasedVi
11291
11618
  "</tr>";
11292
11619
  }).join("");
11293
11620
  var labelsTable = labels.length
11294
- ? "<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\"></th></tr></thead><tbody>" + labelRows + "</tbody></table>"
11621
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Carrier</th><th scope=\"col\">Tracking</th><th scope=\"col\" class=\"num\">Cost</th><th scope=\"col\">Status</th><th scope=\"col\">Label</th><th scope=\"col\"></th></tr></thead><tbody>" + labelRows + "</tbody></table>")
11295
11622
  : "<p class=\"empty\">No labels recorded for this shipment.</p>";
11296
11623
  var carrierOpts = carriers.map(function (c) {
11297
11624
  return "<option value=\"" + _htmlEscape(c) + "\">" + _htmlEscape(c) + "</option>";
@@ -11370,7 +11697,7 @@ function _orderSplitPanel(o, plans) {
11370
11697
  ? "<form method=\"post\" action=\"/admin/orders/" + _htmlEscape(o.id) + "/split/plan\" class=\"return-action\">" +
11371
11698
  "<h4>Plan a manual split</h4>" +
11372
11699
  "<p class=\"meta\">Assign each line to a parcel number (lines sharing a number ship together). Every unit must land in a parcel.</p>" +
11373
- "<table><thead><tr><th scope=\"col\">Line (order qty)</th><th scope=\"col\">Parcel</th><th scope=\"col\">Qty</th></tr></thead><tbody>" + lineRows + "</tbody></table>" +
11700
+ _tableWrap("<table><thead><tr><th scope=\"col\">Line (order qty)</th><th scope=\"col\">Parcel</th><th scope=\"col\">Qty</th></tr></thead><tbody>" + lineRows + "</tbody></table>") +
11374
11701
  "<button class=\"btn\" type=\"submit\">Propose split</button>" +
11375
11702
  "</form>"
11376
11703
  : "<p class=\"empty\">No order lines to split.</p>";
@@ -11403,7 +11730,7 @@ function renderAdminPickLists(opts) {
11403
11730
  "</tr>";
11404
11731
  }).join("");
11405
11732
  var table = lists.length
11406
- ? "<table><thead><tr><th scope=\"col\">List</th><th scope=\"col\">Location</th><th scope=\"col\">Status</th><th scope=\"col\">Sort</th><th scope=\"col\">Generated</th></tr></thead><tbody>" + rows + "</tbody></table>"
11733
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">List</th><th scope=\"col\">Location</th><th scope=\"col\">Status</th><th scope=\"col\">Sort</th><th scope=\"col\">Generated</th></tr></thead><tbody>" + rows + "</tbody></table>")
11407
11734
  : "<p class=\"empty\">No pick lists yet. Generate one from the open orders at a location.</p>";
11408
11735
 
11409
11736
  var statusFilter = "<form method=\"get\" action=\"/admin/pick-lists\" class=\"form-inline\">" +
@@ -11490,7 +11817,7 @@ function renderAdminPickList(opts) {
11490
11817
  "</tr>";
11491
11818
  }).join("");
11492
11819
  var linesTable = (l.lines && l.lines.length)
11493
- ? "<table><thead><tr><th scope=\"col\">Aisle/bin</th><th scope=\"col\">SKU</th><th scope=\"col\">Order</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\">Picked</th><th scope=\"col\"></th></tr></thead><tbody>" + lineRows + "</tbody></table>"
11820
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Aisle/bin</th><th scope=\"col\">SKU</th><th scope=\"col\">Order</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\">Picked</th><th scope=\"col\"></th></tr></thead><tbody>" + lineRows + "</tbody></table>")
11494
11821
  : "<p class=\"empty\">This worksheet has no lines.</p>";
11495
11822
 
11496
11823
  // Variance summary — short / over picks only (the discrepancies feed
@@ -11505,7 +11832,7 @@ function renderAdminPickList(opts) {
11505
11832
  }).join("");
11506
11833
  var variancePanel = discRows
11507
11834
  ? "<div class=\"panel mt\"><h3 class=\"subhead\">Variances</h3>" +
11508
- "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\" class=\"num\">Picked</th><th scope=\"col\" class=\"num\">Diff</th></tr></thead><tbody>" + discRows + "</tbody></table></div>"
11835
+ _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Expected</th><th scope=\"col\" class=\"num\">Picked</th><th scope=\"col\" class=\"num\">Diff</th></tr></thead><tbody>" + discRows + "</tbody></table>") + "</div>"
11509
11836
  : "";
11510
11837
 
11511
11838
  // Complete / cancel actions — only on a non-terminal worksheet.
@@ -11629,7 +11956,7 @@ function renderAdminCustomers(opts) {
11629
11956
  var notice = opts.notice ? "<div class=\"banner banner--warn\">" + _htmlEscape(opts.notice) + "</div>" : "";
11630
11957
 
11631
11958
  var table = customers.length
11632
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">ID</th><th scope=\"col\">Joined</th><th scope=\"col\">Sign-in method</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\"><span class=\"sr-only\">Manage</span></th></tr></thead><tbody>" + rows + "</tbody></table></div>"
11959
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Name</th><th scope=\"col\">ID</th><th scope=\"col\">Joined</th><th scope=\"col\">Sign-in method</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\"><span class=\"sr-only\">Manage</span></th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
11633
11960
  : "<p class=\"empty\">No customers yet.</p>";
11634
11961
 
11635
11962
  // Cursor pager — a Next link when the page filled and more rows remain.
@@ -11680,7 +12007,7 @@ function renderAdminCustomerDetail(opts) {
11680
12007
  }).join("");
11681
12008
  var ordersPanel = "<div class=\"panel\"><h3 class=\"subhead\">Recent orders</h3>" +
11682
12009
  ((opts.recent_orders || []).length
11683
- ? "<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + orderRows + "</tbody></table>"
12010
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Total</th><th scope=\"col\">Placed</th></tr></thead><tbody>" + orderRows + "</tbody></table>")
11684
12011
  : "<p class=\"empty\">No orders yet.</p>") +
11685
12012
  "</div>";
11686
12013
 
@@ -11704,7 +12031,7 @@ function renderAdminCustomerDetail(opts) {
11704
12031
  "</tr>";
11705
12032
  }).join("");
11706
12033
  var histTable = (opts.store_credit_history || []).length
11707
- ? "<table><thead><tr><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Reason</th><th scope=\"col\">When</th></tr></thead><tbody>" + histRows + "</tbody></table>"
12034
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Reason</th><th scope=\"col\">When</th></tr></thead><tbody>" + histRows + "</tbody></table>")
11708
12035
  : "<p class=\"empty\">No store-credit activity yet.</p>";
11709
12036
  creditPanel = "<div class=\"panel\"><h3 class=\"subhead\">Store credit</h3>" +
11710
12037
  creditNotice +
@@ -12004,7 +12331,7 @@ function renderAdminReturns(opts) {
12004
12331
  }).join("");
12005
12332
 
12006
12333
  var table = rmas.length
12007
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">RMA</th><th scope=\"col\">Order</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Refund</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12334
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">RMA</th><th scope=\"col\">Order</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Items</th><th scope=\"col\" class=\"num\">Refund</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12008
12335
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” returns.</p>";
12009
12336
 
12010
12337
  var body = "<section><h2>Returns</h2>" + notice + chips + table + "</section>";
@@ -12024,7 +12351,7 @@ function renderAdminReturn(opts) {
12024
12351
  "<td>" + _htmlEscape(l.reason || "—") + "</td></tr>";
12025
12352
  }).join("");
12026
12353
  var linesTable = (r.lines && r.lines.length)
12027
- ? "<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\">Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
12354
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">Qty</th><th scope=\"col\">Reason</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
12028
12355
  : "<p class=\"empty\">No line items recorded.</p>";
12029
12356
 
12030
12357
  function _field(label, value) {
@@ -12166,7 +12493,7 @@ function renderAdminSupport(opts) {
12166
12493
  }).join("");
12167
12494
 
12168
12495
  var table = tickets.length
12169
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Subject</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Priority</th><th scope=\"col\">Assignee</th><th scope=\"col\">Opened</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12496
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Subject</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Priority</th><th scope=\"col\">Assignee</th><th scope=\"col\">Opened</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12170
12497
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” tickets.</p>";
12171
12498
 
12172
12499
  var body = "<section><h2>Support</h2>" + notice + chips + table + "</section>";
@@ -12324,7 +12651,7 @@ function renderAdminDsr(opts) {
12324
12651
  }).join("");
12325
12652
 
12326
12653
  var table = requests.length
12327
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Customer</th><th scope=\"col\">Kind</th><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Scope</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12654
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Customer</th><th scope=\"col\">Kind</th><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Scope</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12328
12655
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” privacy requests.</p>";
12329
12656
 
12330
12657
  var body = "<section><h2>Privacy requests</h2>" +
@@ -12407,7 +12734,7 @@ function renderAdminDsrDetail(opts) {
12407
12734
  previewHtml =
12408
12735
  "<div class=\"panel mt\"><h3 class=\"subhead\">Erasure preview (dry run — nothing was changed)</h3>" +
12409
12736
  (dRows
12410
- ? "<table><thead><tr><th scope=\"col\">Table</th><th scope=\"col\">Affected</th></tr></thead><tbody>" + dRows + "</tbody></table>"
12737
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Table</th><th scope=\"col\">Affected</th></tr></thead><tbody>" + dRows + "</tbody></table>")
12411
12738
  : "<p class=\"empty\">No per-domain handlers wired.</p>") +
12412
12739
  (absent ? "<p class=\"meta\">Domains without a deletion handler: " + absent + "</p>" : "") +
12413
12740
  "<p class=\"meta\">Total affected: " + _htmlEscape(String(preview.total_affected)) + "</p>" +
@@ -12423,7 +12750,7 @@ function renderAdminDsrDetail(opts) {
12423
12750
  "</tr>";
12424
12751
  }).join("");
12425
12752
  var historyHtml = history.length
12426
- ? "<table><thead><tr><th scope=\"col\">Request</th><th scope=\"col\">Kind</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + histRows + "</tbody></table>"
12753
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Request</th><th scope=\"col\">Kind</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + histRows + "</tbody></table>")
12427
12754
  : "<p class=\"empty\">No prior requests.</p>";
12428
12755
 
12429
12756
  var body =
@@ -12458,6 +12785,9 @@ function renderAdminDsrDetail(opts) {
12458
12785
  // lifecycle order (the terminal closed / rejected don't appear in the open
12459
12786
  // queue, so they're not filter chips).
12460
12787
  var EXCHANGE_STATUS_FILTERS = ["pending", "approved", "shipped", "delivered", "received"];
12788
+ // The pickup-queue status chips the operator can filter by (mirrors the
12789
+ // click-and-collect FSM statuses the pickupsForLocation read accepts).
12790
+ var PICKUP_QUEUE_STATUSES = ["scheduled", "ready", "picked_up", "no_show", "cancelled"];
12461
12791
 
12462
12792
  function _exchangePillClass(status) {
12463
12793
  if (status === "closed") return "paid"; // completed — green
@@ -12494,7 +12824,7 @@ function renderAdminExchanges(opts) {
12494
12824
  }).join("");
12495
12825
 
12496
12826
  var table = list.length
12497
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Exchange</th><th scope=\"col\">Order</th><th scope=\"col\">Swap</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
12827
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Exchange</th><th scope=\"col\">Order</th><th scope=\"col\">Swap</th><th scope=\"col\">Reason</th><th scope=\"col\">Status</th><th scope=\"col\">Requested</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12498
12828
  : "<p class=\"empty\">No “" + _htmlEscape(active) + "” exchanges.</p>";
12499
12829
 
12500
12830
  var body = "<section><h2>Exchanges</h2>" + notice + chips + table + "</section>";
@@ -12612,6 +12942,197 @@ function renderAdminExchange(opts) {
12612
12942
  return _renderAdminShell(opts.shop_name, "Exchange " + String(x.id).slice(0, 8), body, "exchanges", opts.nav_available);
12613
12943
  }
12614
12944
 
12945
+ // ---- click-and-collect render fns -----------------------------------
12946
+
12947
+ // Pickup-locations CRUD screen: the active-location list (each with an
12948
+ // archive form) + a define/upsert form. Every operator free-text field —
12949
+ // name, address parts, hours — is HTML-escaped at the sink (the primitive
12950
+ // validates length/shape, NOT HTML, so escaping here is the XSS guard).
12951
+ function renderAdminPickupLocations(opts) {
12952
+ opts = opts || {};
12953
+ var list = opts.locations || [];
12954
+ var saved = opts.saved ? "<div class=\"banner banner--ok\">Pickup location saved.</div>" : "";
12955
+ var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
12956
+
12957
+ var rows = list.map(function (l) {
12958
+ var addr = l.address || {};
12959
+ var addrLine = [addr.line1, addr.city, addr.country].filter(Boolean).map(String).join(", ");
12960
+ return "<tr>" +
12961
+ "<td><code class=\"order-id\">" + _htmlEscape(l.code) + "</code></td>" +
12962
+ "<td>" + _htmlEscape(l.name) + "</td>" +
12963
+ "<td>" + _htmlEscape(addrLine) + "</td>" +
12964
+ "<td>" + _htmlEscape(String(l.capacity_per_hour)) + "/hr</td>" +
12965
+ "<td>" + _htmlEscape(String(l.lead_time_hours)) + "h</td>" +
12966
+ "<td>" +
12967
+ "<form method=\"post\" action=\"/admin/pickup-locations/" + encodeURIComponent(l.code) + "/archive\" class=\"form-inline\">" +
12968
+ "<button class=\"btn btn--ghost btn--sm\" type=\"submit\">Archive</button>" +
12969
+ "</form>" +
12970
+ "</td>" +
12971
+ "</tr>";
12972
+ }).join("");
12973
+
12974
+ var table = list.length
12975
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\">Name</th><th scope=\"col\">Address</th><th scope=\"col\">Capacity</th><th scope=\"col\">Lead time</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
12976
+ : "<p class=\"empty\">No active pickup locations yet. Add one below.</p>";
12977
+
12978
+ var form =
12979
+ "<div class=\"panel mt\"><h3 class=\"subhead\">Add / update a pickup location</h3>" +
12980
+ "<form method=\"post\" action=\"/admin/pickup-locations\">" +
12981
+ _setupField("Code", "code", "", "text", "Stable identifier (alnum + . _ -). Re-using a code updates that location.", " maxlength=\"64\" required") +
12982
+ _setupField("Name", "name", "", "text", "Shown to customers at checkout.", " maxlength=\"200\" required") +
12983
+ _setupField("Address line 1", "line1", "", "text", "", " maxlength=\"200\" required") +
12984
+ _setupField("City", "city", "", "text", "", " maxlength=\"100\" required") +
12985
+ _setupField("Country", "country", "", "text", "2-letter ISO code (e.g. US).", " maxlength=\"2\" class=\"input-code\" required") +
12986
+ "<label class=\"form-field\"><span>Opening hours</span>" +
12987
+ "<textarea name=\"hours\" rows=\"4\" placeholder=\"mon 09:00-17:00&#10;tue 09:00-17:00\"></textarea>" +
12988
+ "<small>One line per day: <code>mon 09:00-17:00</code>. Optional.</small>" +
12989
+ "</label>" +
12990
+ _setupField("Capacity per hour", "capacity_per_hour", "", "number", "Max concurrent pickups in a one-hour window.", " min=\"1\" required") +
12991
+ _setupField("Lead time (hours)", "lead_time_hours", "", "number", "Minimum gap before the earliest bookable slot. 0 allows same-hour.", " min=\"0\" required") +
12992
+ "<div class=\"actions-row\"><button type=\"submit\" class=\"btn\">Save location</button></div>" +
12993
+ "</form>" +
12994
+ "</div>";
12995
+
12996
+ var body = "<section><h2>Pickup locations</h2>" + saved + notice + table + form + "</section>";
12997
+ return _renderAdminShell(opts.shop_name, "Pickup locations", body, "pickup-locations", opts.nav_available);
12998
+ }
12999
+
13000
+ // Pickup queue for one location: a location selector + status chips, the
13001
+ // scheduled/ready rows, and the FSM action forms (ready / picked-up /
13002
+ // no-show) legal from each row's status. The no_show reason is operator-
13003
+ // authored free text — escaped at the sink.
13004
+ function renderAdminPickups(opts) {
13005
+ opts = opts || {};
13006
+ var locs = opts.locations || [];
13007
+ var list = opts.pickups || [];
13008
+ var selected = opts.selected || null;
13009
+ var status = opts.status || null;
13010
+ var moved = opts.moved ? "<div class=\"banner banner--ok\">Pickup updated.</div>" : "";
13011
+ var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
13012
+
13013
+ var selector = "";
13014
+ if (locs.length) {
13015
+ var opts2 = locs.map(function (l) {
13016
+ return "<option value=\"" + _htmlEscape(l.code) + "\"" + (l.code === selected ? " selected" : "") + ">" +
13017
+ _htmlEscape(l.name) + " (" + _htmlEscape(l.code) + ")</option>";
13018
+ }).join("");
13019
+ selector =
13020
+ "<form method=\"get\" action=\"/admin/pickups\" class=\"form-inline\">" +
13021
+ "<label class=\"form-field\"><span>Location</span><select name=\"location\" onchange=\"this.form.submit()\">" + opts2 + "</select></label>" +
13022
+ "<button class=\"btn btn--ghost btn--sm\" type=\"submit\">View</button>" +
13023
+ "</form>";
13024
+ }
13025
+
13026
+ var statusChip = function (s, label) {
13027
+ var href = "/admin/pickups?location=" + encodeURIComponent(selected || "") + (s ? "&status=" + encodeURIComponent(s) : "");
13028
+ var on = (s == null && status == null) || s === status;
13029
+ return "<a class=\"chip" + (on ? " chip--on" : "") + "\" href=\"" + href + "\">" + _htmlEscape(label) + "</a>";
13030
+ };
13031
+ var chips = selected
13032
+ ? "<div class=\"order-filters\">" + statusChip(null, "all") +
13033
+ PICKUP_QUEUE_STATUSES.map(function (s) { return statusChip(s, s); }).join("") + "</div>"
13034
+ : "";
13035
+
13036
+ function _pickupActions(p) {
13037
+ var blocks = [];
13038
+ if (p.status === "scheduled") {
13039
+ blocks.push(
13040
+ "<form method=\"post\" action=\"/admin/pickups/" + encodeURIComponent(p.order_id) + "/ready\" class=\"form-inline\">" +
13041
+ "<button class=\"btn btn--sm\" type=\"submit\">Mark ready</button></form>");
13042
+ }
13043
+ if (p.status === "ready") {
13044
+ blocks.push(
13045
+ "<form method=\"post\" action=\"/admin/pickups/" + encodeURIComponent(p.order_id) + "/picked-up\" class=\"return-action\">" +
13046
+ "<input type=\"hidden\" name=\"proof_kind\" value=\"store_credential\">" +
13047
+ "<button class=\"btn btn--sm\" type=\"submit\">Mark picked up</button></form>");
13048
+ }
13049
+ if (p.status === "scheduled" || p.status === "ready") {
13050
+ blocks.push(
13051
+ "<form method=\"post\" action=\"/admin/pickups/" + encodeURIComponent(p.order_id) + "/no-show\" class=\"return-action\">" +
13052
+ _setupField("No-show reason", "reason", "", "text", "Shown in the escalation queue.", " maxlength=\"280\" required") +
13053
+ "<button class=\"btn btn--danger btn--sm\" type=\"submit\">No-show</button></form>");
13054
+ }
13055
+ return blocks.length ? blocks.join("") : "<span class=\"meta\">—</span>";
13056
+ }
13057
+
13058
+ var rows = list.map(function (p) {
13059
+ return "<tr>" +
13060
+ "<td><a class=\"order-id\" href=\"/admin/orders/" + _htmlEscape(p.order_id) + "\">" + _htmlEscape(String(p.order_id).slice(0, 8)) + "</a></td>" +
13061
+ "<td><span class=\"status-pill\">" + _htmlEscape(p.status) + "</span></td>" +
13062
+ "<td>" + _htmlEscape(_fmtDate(p.scheduled_window_start)) + "</td>" +
13063
+ "<td>" + (p.picked_up_at ? _htmlEscape(_fmtDate(p.picked_up_at)) : "<span class=\"meta\">—</span>") + "</td>" +
13064
+ "<td>" + _pickupActions(p) + "</td>" +
13065
+ "</tr>";
13066
+ }).join("");
13067
+
13068
+ var table = !selected
13069
+ ? "<p class=\"empty\">Add a pickup location first.</p>"
13070
+ : (list.length
13071
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Order</th><th scope=\"col\">Status</th><th scope=\"col\">Window</th><th scope=\"col\">Picked up</th><th scope=\"col\">Action</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13072
+ : "<p class=\"empty\">No pickups in this window for this location.</p>");
13073
+
13074
+ var body = "<section><h2>Pickups</h2>" + moved + notice + selector + chips + table + "</section>";
13075
+ return _renderAdminShell(opts.shop_name, "Pickups", body, "pickups", opts.nav_available);
13076
+ }
13077
+
13078
+ // ---- gift-wraps render fn -------------------------------------------
13079
+
13080
+ // Gift-wrap catalog screen: the list (incl. archived) with an inline update
13081
+ // form + archive action per row, and a define form. The wrap title is
13082
+ // operator free text — escaped at the sink (the primitive bounds length +
13083
+ // refuses control/zero-width bytes, but not HTML). image_url is escaped as
13084
+ // an attribute value.
13085
+ function renderAdminGiftWraps(opts) {
13086
+ opts = opts || {};
13087
+ var list = opts.wraps || [];
13088
+ var saved = opts.saved ? "<div class=\"banner banner--ok\">Gift wrap saved.</div>" : "";
13089
+ var notice = opts.notice ? "<div class=\"banner banner--err\">" + _htmlEscape(opts.notice) + "</div>" : "";
13090
+
13091
+ var rows = list.map(function (w) {
13092
+ var stateLabel = w.archived_at ? "archived" : (w.active ? "active" : "inactive");
13093
+ var img = w.image_url
13094
+ ? "<img src=\"" + _htmlEscape(String(w.image_url)) + "\" alt=\"\" width=\"32\" height=\"32\" style=\"object-fit:cover\">"
13095
+ : "<span class=\"meta\">—</span>";
13096
+ var updateForm =
13097
+ "<form method=\"post\" action=\"/admin/gift-wraps/" + encodeURIComponent(w.wrap_sku) + "/update\" class=\"form-inline\">" +
13098
+ "<input type=\"text\" name=\"title\" value=\"" + _htmlEscape(w.title) + "\" maxlength=\"200\" aria-label=\"Title\">" +
13099
+ "<input type=\"number\" name=\"fee_minor\" value=\"" + _htmlEscape(String(w.fee_minor)) + "\" min=\"0\" aria-label=\"Fee (minor units)\">" +
13100
+ "<button class=\"btn btn--sm\" type=\"submit\">Update</button>" +
13101
+ "</form>";
13102
+ var archiveForm = w.archived_at ? "" :
13103
+ "<form method=\"post\" action=\"/admin/gift-wraps/" + encodeURIComponent(w.wrap_sku) + "/archive\" class=\"form-inline\">" +
13104
+ "<button class=\"btn btn--ghost btn--sm\" type=\"submit\">Archive</button>" +
13105
+ "</form>";
13106
+ return "<tr>" +
13107
+ "<td><code class=\"order-id\">" + _htmlEscape(w.wrap_sku) + "</code></td>" +
13108
+ "<td>" + img + "</td>" +
13109
+ "<td>" + _htmlEscape(w.title) + "</td>" +
13110
+ "<td>" + _htmlEscape(String(w.fee_minor)) + "</td>" +
13111
+ "<td><span class=\"status-pill\">" + _htmlEscape(stateLabel) + "</span></td>" +
13112
+ "<td>" + updateForm + archiveForm + "</td>" +
13113
+ "</tr>";
13114
+ }).join("");
13115
+
13116
+ var table = list.length
13117
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\">Image</th><th scope=\"col\">Title</th><th scope=\"col\">Fee</th><th scope=\"col\">State</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13118
+ : "<p class=\"empty\">No gift wraps yet. Define one below — the SKU must be a real catalog variant.</p>";
13119
+
13120
+ var form =
13121
+ "<div class=\"panel mt\"><h3 class=\"subhead\">Define a gift wrap</h3>" +
13122
+ "<form method=\"post\" action=\"/admin/gift-wraps\">" +
13123
+ _setupField("Wrap SKU", "wrap_sku", "", "text", "Must be a real catalog variant SKU — the fee flows through inventory + pricing.", " maxlength=\"128\" required") +
13124
+ _setupField("Title", "title", "", "text", "Shown to customers at checkout.", " maxlength=\"200\" required") +
13125
+ _setupField("Fee (minor units)", "fee_minor", "", "number", "Set this equal to the variant's catalog price; the fee is charged as a real cart line.", " min=\"0\" required") +
13126
+ _setupField("Image URL", "image_url", "", "text", "Optional. https:// or /absolute path.", " maxlength=\"2048\"") +
13127
+ _setupField("Max per order", "max_per_order", "", "number", "Optional cap on wraps per order.", " min=\"1\"") +
13128
+ "<div class=\"actions-row\"><button type=\"submit\" class=\"btn\">Save wrap</button></div>" +
13129
+ "</form>" +
13130
+ "</div>";
13131
+
13132
+ var body = "<section><h2>Gift wraps</h2>" + saved + notice + table + form + "</section>";
13133
+ return _renderAdminShell(opts.shop_name, "Gift wraps", body, "gift-wraps", opts.nav_available);
13134
+ }
13135
+
12615
13136
  // One rating card for the moderation queue / recent list. Shows the three
12616
13137
  // scores, the (escaped) comment, the flag state + reason, and the operator
12617
13138
  // reply — plus the flag / reply action forms.
@@ -12932,7 +13453,7 @@ function renderAdminInventory(opts) {
12932
13453
  }).join("");
12933
13454
 
12934
13455
  var table = rows.length
12935
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">On hand</th><th scope=\"col\" class=\"num\">Held</th><th scope=\"col\" class=\"num\">Available</th><th scope=\"col\">Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table></div>"
13456
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">SKU</th><th scope=\"col\" class=\"num\">On hand</th><th scope=\"col\" class=\"num\">Held</th><th scope=\"col\" class=\"num\">Available</th><th scope=\"col\">Restock / threshold</th></tr></thead><tbody>" + body + "</tbody></table>") + "</div>"
12936
13457
  : "<p class=\"empty\">No inventory rows" + (opts.low ? " below threshold" : " yet") + ".</p>";
12937
13458
 
12938
13459
  var createForm =
@@ -13039,7 +13560,7 @@ function renderAdminSubscriptionPlans(opts) {
13039
13560
  }).join("");
13040
13561
 
13041
13562
  var table = rows.length
13042
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Price / interval</th><th scope=\"col\">Stripe price</th><th scope=\"col\">Variant</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13563
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Price / interval</th><th scope=\"col\">Stripe price</th><th scope=\"col\">Variant</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13043
13564
  : "<p class=\"empty\">No subscription plans" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
13044
13565
 
13045
13566
  var intervalOpts = ["month", "year", "week", "day"].map(function (iv) {
@@ -13140,7 +13661,7 @@ function renderAdminTaxRates(opts) {
13140
13661
  "</tr>";
13141
13662
  }).join("");
13142
13663
  table = rows.length
13143
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Category</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">From</th><th scope=\"col\">Until</th><th scope=\"col\">Source</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13664
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Category</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">From</th><th scope=\"col\">Until</th><th scope=\"col\">Source</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13144
13665
  : "<p class=\"empty\">No rates for " + _htmlEscape(jurisdiction) + " yet.</p>";
13145
13666
  }
13146
13667
 
@@ -13223,7 +13744,7 @@ function renderAdminTaxFilings(opts) {
13223
13744
  }).join("");
13224
13745
  var dueBlock = upcoming.length
13225
13746
  ? "<section><h2>Due soon or overdue</h2><div class=\"panel\">" +
13226
- "<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Period</th><th scope=\"col\">Status</th><th scope=\"col\">Due</th></tr></thead><tbody>" + dueRows + "</tbody></table>" +
13747
+ _tableWrap("<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Period</th><th scope=\"col\">Status</th><th scope=\"col\">Due</th></tr></thead><tbody>" + dueRows + "</tbody></table>") +
13227
13748
  "</div></section>"
13228
13749
  : "";
13229
13750
 
@@ -13240,7 +13761,7 @@ function renderAdminTaxFilings(opts) {
13240
13761
  "</tr>";
13241
13762
  }).join("");
13242
13763
  var table = filings.length
13243
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Kind</th><th scope=\"col\">Period</th><th scope=\"col\">Due</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13764
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Jurisdiction</th><th scope=\"col\">Kind</th><th scope=\"col\">Period</th><th scope=\"col\">Due</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13244
13765
  : "<p class=\"empty\">No filings" + (jurisdiction ? " for " + _htmlEscape(jurisdiction) : "") + " yet. Open a period below.</p>";
13245
13766
 
13246
13767
  // Open-a-period form.
@@ -13330,7 +13851,7 @@ function renderAdminTaxFiling(opts) {
13330
13851
  "</tr>";
13331
13852
  }).join("");
13332
13853
  var breakdownBlock = bkeys.length
13333
- ? "<section><h2>By rate</h2><div class=\"panel\"><table><thead><tr><th scope=\"col\">Rate</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Taxable</th><th scope=\"col\" class=\"num\">Tax</th></tr></thead><tbody>" + breakdownRows + "</tbody></table></div></section>"
13854
+ ? "<section><h2>By rate</h2><div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Rate</th><th scope=\"col\" class=\"num\">Orders</th><th scope=\"col\" class=\"num\">Taxable</th><th scope=\"col\" class=\"num\">Tax</th></tr></thead><tbody>" + breakdownRows + "</tbody></table>") + "</div></section>"
13334
13855
  : "";
13335
13856
 
13336
13857
  // Audit trail — submission + payment + amendment columns once recorded.
@@ -13351,7 +13872,7 @@ function renderAdminTaxFiling(opts) {
13351
13872
  trailRows += "<tr><td>Amended</td><td>" + _htmlEscape(f.amended_reason) +
13352
13873
  (f.amended_at != null ? " <span class=\"meta\">" + _htmlEscape(_fmtDate(f.amended_at)) + "</span>" : "") + "</td></tr>";
13353
13874
  }
13354
- trail = "<section><h2>Audit trail</h2><div class=\"panel\"><table><tbody>" + trailRows + "</tbody></table></div></section>";
13875
+ trail = "<section><h2>Audit trail</h2><div class=\"panel\">" + _tableWrap("<table><tbody>" + trailRows + "</tbody></table>") + "</div></section>";
13355
13876
  }
13356
13877
 
13357
13878
  // Lifecycle actions — gated on the FSM. draft → compute; computed →
@@ -13426,7 +13947,7 @@ function renderAdminTaxFilingReport(opts) {
13426
13947
  "</tr>";
13427
13948
  }).join("");
13428
13949
  var table = (report.filings || []).length
13429
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Period</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
13950
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Period</th><th scope=\"col\">Window</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Collected</th><th scope=\"col\" class=\"num\">Owed</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13430
13951
  : "<p class=\"empty\">No filings intersect this window.</p>";
13431
13952
 
13432
13953
  var body =
@@ -13470,7 +13991,7 @@ function renderAdminShipping(opts) {
13470
13991
  }).join("");
13471
13992
 
13472
13993
  var table = rows.length
13473
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Regions</th><th scope=\"col\" class=\"num\">Rates</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
13994
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Regions</th><th scope=\"col\" class=\"num\">Rates</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13474
13995
  : "<p class=\"empty\">No shipping zones yet.</p>";
13475
13996
 
13476
13997
  var createForm =
@@ -13520,7 +14041,7 @@ function renderAdminShippingZone(opts) {
13520
14041
  "</tr>";
13521
14042
  }).join("");
13522
14043
  var rateTable = (z.rates || []).length
13523
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Service</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Bucket</th></tr></thead><tbody>" + rateRows + "</tbody></table></div>"
14044
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Service</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Bucket</th></tr></thead><tbody>" + rateRows + "</tbody></table>") + "</div>"
13524
14045
  : "<p class=\"empty\">No rate rows.</p>";
13525
14046
 
13526
14047
  var head = "<p class=\"meta\"><a href=\"/admin/shipping\">&larr; Shipping</a> · <code class=\"order-id\">" + _htmlEscape(z.slug) + "</code> · " +
@@ -13682,7 +14203,7 @@ function renderAdminDiscounts(opts) {
13682
14203
  "</tr>";
13683
14204
  }).join("");
13684
14205
  var ruleTable = rules.length
13685
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Rule</th><th scope=\"col\">Trigger</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + ruleRows + "</tbody></table></div>"
14206
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Rule</th><th scope=\"col\">Trigger</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + ruleRows + "</tbody></table>") + "</div>"
13686
14207
  : "<p class=\"empty\">No automatic discount rules yet.</p>";
13687
14208
 
13688
14209
  var TRIGGERS = [
@@ -13745,7 +14266,7 @@ function renderAdminDiscounts(opts) {
13745
14266
  "</tr>";
13746
14267
  }).join("");
13747
14268
  var polTable = policies.length
13748
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Policy</th><th scope=\"col\" class=\"num\">Max codes</th><th scope=\"col\">Combines with</th><th scope=\"col\" class=\"num\">Order min</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + polRows + "</tbody></table></div>"
14269
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Policy</th><th scope=\"col\" class=\"num\">Max codes</th><th scope=\"col\">Combines with</th><th scope=\"col\" class=\"num\">Order min</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + polRows + "</tbody></table>") + "</div>"
13749
14270
  : "<p class=\"empty\">No stacking policies yet — without one, only one code applies per order.</p>";
13750
14271
  var polForm =
13751
14272
  "<div class=\"panel mt mw-40\">" +
@@ -13851,7 +14372,7 @@ function renderAdminSegments(opts) {
13851
14372
  }).join("");
13852
14373
 
13853
14374
  var table = segments.length
13854
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Segment</th><th scope=\"col\">Rules</th><th scope=\"col\" class=\"num\">Members</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
14375
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Segment</th><th scope=\"col\">Rules</th><th scope=\"col\" class=\"num\">Members</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
13855
14376
  : "<p class=\"empty\">No " + (filter === "archived" ? "archived " : (filter === "all" ? "" : "active ")) + "segments yet. Define one to start grouping customers for campaigns.</p>";
13856
14377
 
13857
14378
  var body = "<section><h2>Customer segments</h2>" + created + updated + archived + unarchived + recomputed + notice +
@@ -13993,7 +14514,7 @@ function renderAdminGiftCards(opts) {
13993
14514
  }).join("");
13994
14515
 
13995
14516
  var table = rows.length
13996
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\" class=\"num\">Issued</th><th scope=\"col\" class=\"num\">Remaining</th><th scope=\"col\">Status</th><th scope=\"col\">Issued on</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14517
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Code</th><th scope=\"col\" class=\"num\">Issued</th><th scope=\"col\" class=\"num\">Remaining</th><th scope=\"col\">Status</th><th scope=\"col\">Issued on</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
13997
14518
  : "<p class=\"empty\">No gift cards" + (sf ? " " + _htmlEscape(sf) : " yet") + ".</p>";
13998
14519
 
13999
14520
  // The issue form composes the giftcards primitive's issue() — the
@@ -14060,7 +14581,7 @@ function renderAdminGiftCard(opts) {
14060
14581
  "</tr>";
14061
14582
  }).join("");
14062
14583
  var ledgerTable = ledger.length
14063
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Type</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Detail</th><th scope=\"col\">When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table></div>"
14584
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Type</th><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\" class=\"num\">Balance after</th><th scope=\"col\">Detail</th><th scope=\"col\">When</th></tr></thead><tbody>" + ledgerRows + "</tbody></table>") + "</div>"
14064
14585
  : "<p class=\"empty\">No ledger transactions recorded for this card yet.</p>";
14065
14586
 
14066
14587
  // Void is offered only while the card is active — a redeemed card has
@@ -14102,7 +14623,7 @@ function renderAdminWebhooks(opts) {
14102
14623
  }).join("");
14103
14624
 
14104
14625
  var table = rows.length
14105
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">URL</th><th scope=\"col\">Events</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14626
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">URL</th><th scope=\"col\">Events</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Rate</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14106
14627
  : "<p class=\"empty\">No webhook endpoints yet.</p>";
14107
14628
 
14108
14629
  var eventChecks = known.map(function (ev) {
@@ -14179,7 +14700,7 @@ function renderAdminWebhookDeliveries(opts) {
14179
14700
  }).join("");
14180
14701
 
14181
14702
  var table = rows.length
14182
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Event</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Code</th><th scope=\"col\" class=\"num\">Attempts</th><th scope=\"col\">Last error</th><th scope=\"col\">Created</th><th scope=\"col\"></th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14703
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Event</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Code</th><th scope=\"col\" class=\"num\">Attempts</th><th scope=\"col\">Last error</th><th scope=\"col\">Created</th><th scope=\"col\"></th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14183
14704
  : "<p class=\"empty\">No deliveries recorded for this endpoint yet.</p>";
14184
14705
 
14185
14706
  var head = "<p class=\"meta\">Endpoint <code class=\"order-id\">" + _htmlEscape(e.url) + "</code></p>";
@@ -14223,7 +14744,7 @@ function renderAdminCollections(opts) {
14223
14744
  }).join("");
14224
14745
 
14225
14746
  var table = rows.length
14226
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Type</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Products</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
14747
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Type</th><th scope=\"col\">Status</th><th scope=\"col\" class=\"num\">Products</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14227
14748
  : "<p class=\"empty\">No collections" + (af === "0" ? " archived" : af === "1" ? " active" : " yet") + ".</p>";
14228
14749
 
14229
14750
  // The create form toggles between a manual and a smart shape. Manual
@@ -14302,7 +14823,7 @@ function renderAdminDiscountAllocation(opts) {
14302
14823
  "</tr>";
14303
14824
  }).join("");
14304
14825
  var lineTable = breakdown.length
14305
- ? "<table><thead><tr><th scope=\"col\">Line</th><th scope=\"col\" class=\"num\">Allocated (minor)</th><th scope=\"col\" class=\"num\">Remaining (minor)</th></tr></thead><tbody>" + lineRows + "</tbody></table>"
14826
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Line</th><th scope=\"col\" class=\"num\">Allocated (minor)</th><th scope=\"col\" class=\"num\">Remaining (minor)</th></tr></thead><tbody>" + lineRows + "</tbody></table>")
14306
14827
  : "<p class=\"empty\">This allocation has no recorded lines.</p>";
14307
14828
  return "<div class=\"panel mt\">" +
14308
14829
  "<h3 class=\"subhead\">" + _htmlEscape(a.source || "—") + "</h3>" +
@@ -14614,7 +15135,7 @@ function renderAdminAnnouncements(opts) {
14614
15135
  }).join("");
14615
15136
 
14616
15137
  var table = rows.length
14617
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Message</th><th scope=\"col\">Theme</th><th scope=\"col\">Audience</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15138
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Message</th><th scope=\"col\">Theme</th><th scope=\"col\">Audience</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14618
15139
  : "<p class=\"empty\">No announcements" + (af === "1" ? " active right now" : " yet") + ".</p>";
14619
15140
 
14620
15141
  var themeOpts = ["urgency", "promo", "info", "success"].map(function (t) {
@@ -14688,7 +15209,7 @@ function renderAdminSearchRanking(opts) {
14688
15209
  "</tr>";
14689
15210
  }).join("");
14690
15211
  var table = weights.length
14691
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Name</th><th scope=\"col\">Weights</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
15212
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Name</th><th scope=\"col\">Weights</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
14692
15213
  : "<p class=\"empty\">No weight sets yet. Create one below and make it active to rerank storefront search.</p>";
14693
15214
 
14694
15215
  var createForm =
@@ -14723,7 +15244,7 @@ function renderAdminSearchRanking(opts) {
14723
15244
  "</form>";
14724
15245
  var pinTable = pinsQuery
14725
15246
  ? (pins.length
14726
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Product id</th><th scope=\"col\">Position</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + pinRows + "</tbody></table></div>"
15247
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Product id</th><th scope=\"col\">Position</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + pinRows + "</tbody></table>") + "</div>"
14727
15248
  : "<p class=\"empty\">No pins for &ldquo;" + _htmlEscape(pinsQuery) + "&rdquo; yet.</p>")
14728
15249
  : "";
14729
15250
  var pinCreateForm = pinsQuery
@@ -14798,7 +15319,7 @@ function renderAdminTrustBadges(opts) {
14798
15319
  "</tr>";
14799
15320
  }).join("");
14800
15321
  var table = badges.length
14801
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Placements</th><th scope=\"col\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table></div>"
15322
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Placements</th><th scope=\"col\">Priority</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + rows + "</tbody></table>") + "</div>"
14802
15323
  : "<p class=\"empty\">No trust badges yet. Create one below.</p>";
14803
15324
 
14804
15325
  var placementChecks = trustBadgesModule.ALLOWED_PLACEMENTS.map(function (pl) {
@@ -14938,11 +15459,11 @@ function renderAdminPreorders(opts) {
14938
15459
  }).join("");
14939
15460
 
14940
15461
  var table = rows.length
14941
- ? "<div class=\"panel\"><table><thead><tr>" +
15462
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr>" +
14942
15463
  "<th scope=\"col\">Slug</th><th scope=\"col\">SKU</th><th scope=\"col\">Ships</th>" +
14943
15464
  "<th scope=\"col\">Price</th><th scope=\"col\">Reserved / cap</th><th scope=\"col\">Remaining</th>" +
14944
15465
  "<th scope=\"col\">Status</th><th scope=\"col\">Actions</th>" +
14945
- "</tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15466
+ "</tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
14946
15467
  : "<p class=\"empty\">No pre-order campaigns yet.</p>";
14947
15468
 
14948
15469
  var createForm =
@@ -15111,7 +15632,7 @@ function renderAdminHelp(opts) {
15111
15632
  }).join("");
15112
15633
 
15113
15634
  var table = rows.length
15114
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Views</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15635
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Category</th><th scope=\"col\">Status</th><th scope=\"col\">Views</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15115
15636
  : "<p class=\"empty\">No articles" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15116
15637
 
15117
15638
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/help/new\">New article</a></div>";
@@ -15263,7 +15784,7 @@ function renderAdminBlog(opts) {
15263
15784
  }).join("");
15264
15785
 
15265
15786
  var table = rows.length
15266
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15787
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15267
15788
  : "<p class=\"empty\">No posts" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15268
15789
 
15269
15790
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/blog/new\">New post</a></div>";
@@ -15464,7 +15985,7 @@ function renderAdminPages(opts) {
15464
15985
  }).join("");
15465
15986
 
15466
15987
  var table = rows.length
15467
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
15988
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th><th scope=\"col\">Published</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15468
15989
  : "<p class=\"empty\">No pages" + (sf ? " " + _htmlEscape(sf) : " yet") + ". Write your first one.</p>";
15469
15990
 
15470
15991
  var newBtn = "<div class=\"actions-row\"><a class=\"btn\" href=\"/admin/pages/new\">New page</a></div>";
@@ -15618,7 +16139,7 @@ function renderAdminSurveys(opts) {
15618
16139
  }).join("");
15619
16140
 
15620
16141
  var table = rows.length
15621
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Kind</th><th scope=\"col\">Trigger</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16142
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Kind</th><th scope=\"col\">Trigger</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15622
16143
  : "<p class=\"empty\">No surveys" + (af === "1" ? " active" : " yet") + ".</p>";
15623
16144
 
15624
16145
  var kindOpts = [["nps", "NPS — recommend score"], ["csat", "CSAT — satisfaction"], ["ces", "CES — ease of effort"], ["custom", "Custom — open feedback"]]
@@ -15681,7 +16202,7 @@ function renderAdminSurveyDetail(opts) {
15681
16202
  var rollupPanel =
15682
16203
  "<div class=\"panel\">" +
15683
16204
  "<p class=\"meta\">" + _htmlEscape(String(roll.response_count)) + " response(s)." + (headline ? " " + headline : "") + "</p>" +
15684
- (qRows ? "<table><thead><tr><th scope=\"col\">Question</th><th scope=\"col\">Kind</th><th scope=\"col\">Result</th></tr></thead><tbody>" + qRows + "</tbody></table>" : "") +
16205
+ (qRows ? _tableWrap("<table><thead><tr><th scope=\"col\">Question</th><th scope=\"col\">Kind</th><th scope=\"col\">Result</th></tr></thead><tbody>" + qRows + "</tbody></table>") : "") +
15685
16206
  "</div>";
15686
16207
 
15687
16208
  var enc = _htmlEscape(encodeURIComponent(survey.slug));
@@ -15747,7 +16268,7 @@ function renderAdminHours(opts) {
15747
16268
  }).join("");
15748
16269
 
15749
16270
  var table = rows.length
15750
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Schedule</th><th scope=\"col\">Timezone</th><th scope=\"col\" class=\"num\">Days</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16271
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Schedule</th><th scope=\"col\">Timezone</th><th scope=\"col\" class=\"num\">Days</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15751
16272
  : "<p class=\"empty\">No schedules yet.</p>";
15752
16273
 
15753
16274
  var dayFields = DOW.map(function (label, d) {
@@ -15847,7 +16368,7 @@ function renderAdminCollection(opts) {
15847
16368
  "</tr>";
15848
16369
  }).join("");
15849
16370
  var memberTable = members.length
15850
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\" class=\"num\">#</th><th scope=\"col\">Product id</th><th scope=\"col\"></th></tr></thead><tbody>" + memberRows + "</tbody></table></div>"
16371
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">#</th><th scope=\"col\">Product id</th><th scope=\"col\"></th></tr></thead><tbody>" + memberRows + "</tbody></table>") + "</div>"
15851
16372
  : "<p class=\"empty\">No members yet — add a product below.</p>";
15852
16373
 
15853
16374
  // Reorder: a single field of the current ids, comma-joined, that the
@@ -15887,7 +16408,7 @@ function renderAdminCollection(opts) {
15887
16408
  "</tr>";
15888
16409
  }).join("");
15889
16410
  var previewTable = preview.length
15890
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th></tr></thead><tbody>" + previewCards + "</tbody></table></div>"
16411
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Title</th><th scope=\"col\">Slug</th><th scope=\"col\">Status</th></tr></thead><tbody>" + previewCards + "</tbody></table>") + "</div>"
15891
16412
  : "<p class=\"empty\">No products match these rules yet.</p>";
15892
16413
  detailBody = "<section class=\"mt\"><h3 class=\"fs-105\">Matched products (live preview)</h3>" +
15893
16414
  "<p class=\"meta\">The first " + _htmlEscape(String(preview.length)) + " products the rules match right now.</p>" +
@@ -15979,7 +16500,7 @@ function renderAdminQuantityDiscounts(opts) {
15979
16500
  }).join("");
15980
16501
 
15981
16502
  var table = rows.length
15982
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Scope</th><th scope=\"col\">Scope id</th><th scope=\"col\" class=\"num\">Tiers</th><th scope=\"col\">Stacking</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16503
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Scope</th><th scope=\"col\">Scope id</th><th scope=\"col\" class=\"num\">Tiers</th><th scope=\"col\">Stacking</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
15983
16504
  : "<p class=\"empty\">No quantity breaks" + (af === "1" ? " archived" : af === "0" ? " active" : " yet") + ".</p>";
15984
16505
 
15985
16506
  var scopeOpts = scopes.map(function (sc) {
@@ -16070,7 +16591,7 @@ function renderAdminQuantityDiscount(opts) {
16070
16591
  "</tr>";
16071
16592
  }).join("");
16072
16593
  var previewTable = previewRows
16073
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\" class=\"num\">Min qty</th><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Value</th><th scope=\"col\" class=\"num\">Unit @ min</th><th scope=\"col\" class=\"num\">Line saved</th><th scope=\"col\" class=\"num\">Line total</th></tr></thead><tbody>" + previewRows + "</tbody></table></div>"
16594
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">Min qty</th><th scope=\"col\">Kind</th><th scope=\"col\" class=\"num\">Value</th><th scope=\"col\" class=\"num\">Unit @ min</th><th scope=\"col\" class=\"num\">Line saved</th><th scope=\"col\" class=\"num\">Line total</th></tr></thead><tbody>" + previewRows + "</tbody></table>") + "</div>"
16074
16595
  : "<p class=\"empty\">No active rules to preview for this scope.</p>";
16075
16596
  var previewForm =
16076
16597
  "<form method=\"get\" action=\"/admin/quantity-discounts/" + _htmlEscape(enc) + "\" class=\"actions-row m-04\">" +
@@ -16153,8 +16674,8 @@ function renderAdminLoyalty(opts) {
16153
16674
  "<p class=\"meta\">Lifetime points (never decremented) place a customer in a tier. " +
16154
16675
  "Earning $1 grants " + _htmlEscape(String(opts.points_per_usd)) + " points; " +
16155
16676
  _htmlEscape(String(opts.redemption_points_per_usd)) + " points redeem for $1 of value.</p>" +
16156
- "<table><thead><tr><th scope=\"col\">Tier</th><th scope=\"col\" class=\"num\">Lifetime points to reach</th></tr></thead>" +
16157
- "<tbody>" + tierRows + "</tbody></table></div>";
16677
+ _tableWrap("<table><thead><tr><th scope=\"col\">Tier</th><th scope=\"col\" class=\"num\">Lifetime points to reach</th></tr></thead>" +
16678
+ "<tbody>" + tierRows + "</tbody></table>") + "</div>";
16158
16679
 
16159
16680
  // Earn-rule summary.
16160
16681
  var earnSummary;
@@ -16175,7 +16696,7 @@ function renderAdminLoyalty(opts) {
16175
16696
  earnSummary = "<div class=\"panel\"><div class=\"actions-row\"><h3 class=\"subhead\">Earn rules</h3>" +
16176
16697
  "<a class=\"btn btn--ghost\" href=\"/admin/loyalty/earn-rules\">Manage earn rules</a></div>" +
16177
16698
  (earnRules.length
16178
- ? "<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\">Status</th></tr></thead><tbody>" + earnRows + "</tbody></table>"
16699
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\">Status</th></tr></thead><tbody>" + earnRows + "</tbody></table>")
16179
16700
  : "<p class=\"empty\">No earn rules yet. Customers only earn points from active rules.</p>") +
16180
16701
  "</div>";
16181
16702
  }
@@ -16199,7 +16720,7 @@ function renderAdminLoyalty(opts) {
16199
16720
  rewardSummary = "<div class=\"panel\"><div class=\"actions-row\"><h3 class=\"subhead\">Rewards catalog</h3>" +
16200
16721
  "<a class=\"btn btn--ghost\" href=\"/admin/loyalty/rewards\">Manage rewards</a></div>" +
16201
16722
  (rewards.length
16202
- ? "<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th></tr></thead><tbody>" + rewardRows + "</tbody></table>"
16723
+ ? _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th></tr></thead><tbody>" + rewardRows + "</tbody></table>")
16203
16724
  : "<p class=\"empty\">No rewards yet. Customers only see active rewards in their catalog.</p>") +
16204
16725
  "</div>";
16205
16726
  }
@@ -16267,7 +16788,7 @@ function renderAdminLoyaltyEarnRules(opts) {
16267
16788
  "</tr>";
16268
16789
  }).join("");
16269
16790
  var table = rules.length
16270
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\" class=\"num\">Max/event</th><th scope=\"col\">Statuses</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16791
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Trigger</th><th scope=\"col\" class=\"num\">Points/unit</th><th scope=\"col\" class=\"num\">Max/event</th><th scope=\"col\">Statuses</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16271
16792
  : "<p class=\"empty\">No earn rules yet.</p>";
16272
16793
 
16273
16794
  var triggerOpts = triggers.map(function (t) {
@@ -16370,7 +16891,7 @@ function renderAdminLoyaltyRewards(opts) {
16370
16891
  "</tr>";
16371
16892
  }).join("");
16372
16893
  var table = rewards.length
16373
- ? "<div class=\"panel\"><table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table></div>"
16894
+ ? "<div class=\"panel\">" + _tableWrap("<table><thead><tr><th scope=\"col\">Slug</th><th scope=\"col\">Title</th><th scope=\"col\">Kind</th><th scope=\"col\">Value</th><th scope=\"col\" class=\"num\">Point cost</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead><tbody>" + bodyRows + "</tbody></table>") + "</div>"
16374
16895
  : "<p class=\"empty\">No rewards yet.</p>";
16375
16896
 
16376
16897
  var kindOpts = kinds.map(function (k) {
@@ -16528,7 +17049,7 @@ function renderAdminProduct(opts) {
16528
17049
  "</tr>";
16529
17050
  }).join("");
16530
17051
  var histTable = (pc.history && pc.history.length)
16531
- ? "<table><thead><tr><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\">From</th><th scope=\"col\">Until</th></tr></thead><tbody>" + histRows + "</tbody></table>"
17052
+ ? _tableWrap("<table><thead><tr><th scope=\"col\" class=\"num\">Amount</th><th scope=\"col\">From</th><th scope=\"col\">Until</th></tr></thead><tbody>" + histRows + "</tbody></table>")
16532
17053
  : "";
16533
17054
  return "<div class=\"m-04\">" +
16534
17055
  "<span class=\"u-mute\">" + _htmlEscape(pc.currency) + "</span> · " + current +
@@ -16747,4 +17268,7 @@ module.exports = {
16747
17268
  renderAdminLoyaltyReward: renderAdminLoyaltyReward,
16748
17269
  renderAdminConfirm: renderAdminConfirm,
16749
17270
  renderAdminAudit: renderAdminAudit,
17271
+ renderAdminPickupLocations: renderAdminPickupLocations,
17272
+ renderAdminPickups: renderAdminPickups,
17273
+ renderAdminGiftWraps: renderAdminGiftWraps,
16750
17274
  };