@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/CHANGELOG.md +4 -0
- package/lib/admin.js +599 -75
- package/lib/asset-manifest.json +9 -5
- package/lib/click-and-collect.js +35 -5
- package/lib/payment.js +77 -0
- package/lib/storefront.js +877 -47
- package/package.json +1 -1
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\"
|
|
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
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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 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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\">← 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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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 “" + _htmlEscape(pinsQuery) + "” 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\"
|
|
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\"
|
|
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
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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\"
|
|
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
|
|
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\"
|
|
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\"
|
|
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
|
};
|