@mohasinac/appkit 4.62.1 → 4.63.0
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/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server.d.ts +27 -1
- package/dist/server.js +144 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HOLD on stock that is not yet an order.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this has to exist before S5b (Part 6)
|
|
5
|
+
*
|
|
6
|
+
* The plan calls this "the largest hidden cost" of the shared-purchase work,
|
|
7
|
+
* and the reason is that today there is no way to hold stock at all. The only
|
|
8
|
+
* mechanism is: `onOrderCreate` decrements `availableQuantity`, and
|
|
9
|
+
* `pendingOrderTimeout` puts it back if the buyer does not pay inside the
|
|
10
|
+
* 15-minute window. Both are keyed to an ORDER.
|
|
11
|
+
*
|
|
12
|
+
* A shared purchase runs for DAYS while co-buyers pay. With no primitive,
|
|
13
|
+
* either the coordinator places an order up front — which starts a 15-minute
|
|
14
|
+
* payment clock on a multi-day agreement — or nothing is held, and the item is
|
|
15
|
+
* sold out from under the arrangement by an ordinary buyer who happened to
|
|
16
|
+
* check out first. Neither is a product.
|
|
17
|
+
*
|
|
18
|
+
* ## The invariant, and where it already lives
|
|
19
|
+
*
|
|
20
|
+
* `products.repository.ts` already states it:
|
|
21
|
+
*
|
|
22
|
+
* reserved = stockQuantity - availableQuantity
|
|
23
|
+
*
|
|
24
|
+
* …as the comment on `resolveStockResync` puts it, *"the difference between
|
|
25
|
+
* the two IS the outstanding reservation"*. This collection does not invent a
|
|
26
|
+
* new idea; it makes that difference ADDRESSABLE — every held unit gets a row
|
|
27
|
+
* saying who holds it and until when, instead of being an anonymous gap
|
|
28
|
+
* between two numbers that only an order can close.
|
|
29
|
+
*
|
|
30
|
+
* 🛑 So a reservation MUST move `availableQuantity` in the same transaction
|
|
31
|
+
* that writes its row. A row without the decrement holds nothing; a decrement
|
|
32
|
+
* without a row is the anonymous gap again, and nothing can expire it.
|
|
33
|
+
*
|
|
34
|
+
* ## Why a TTL rather than a cancel button
|
|
35
|
+
*
|
|
36
|
+
* A hold with no expiry is a leak: the holder closes the tab, and those units
|
|
37
|
+
* are off the shelf until a human notices. `expiresAt` means the worst case is
|
|
38
|
+
* bounded by the sweep interval, and the seller never has to ask anyone to
|
|
39
|
+
* release anything.
|
|
40
|
+
*/
|
|
41
|
+
export const STOCK_RESERVATION_COLLECTION = "stockReservations";
|
|
42
|
+
/**
|
|
43
|
+
* Who is holding, and therefore what releases it.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
|
|
46
|
+
* and the release path both need to know WHICH system owns the hold before
|
|
47
|
+
* they can decide whether it is still legitimate. A discriminator makes
|
|
48
|
+
* "release every hold belonging to arrangement X" expressible; an opaque id
|
|
49
|
+
* makes it a string-prefix convention nobody enforces.
|
|
50
|
+
*/
|
|
51
|
+
export const RESERVATION_HOLDER_KINDS = ["shared-purchase", "manual"];
|
|
52
|
+
/**
|
|
53
|
+
* 🛑 `cart` and `order` are deliberately NOT holder kinds.
|
|
54
|
+
*
|
|
55
|
+
* A cart holds nothing today — adding an item does not decrement anything —
|
|
56
|
+
* and making it hold would silently turn every abandoned cart into held
|
|
57
|
+
* stock. An order already has its own hold through `onOrderCreate` +
|
|
58
|
+
* `pendingOrderTimeout`; a second mechanism over the same units would
|
|
59
|
+
* double-decrement, which is exactly the class of bug `resolveStockResync`'s
|
|
60
|
+
* delta exists to prevent. Add a kind only when the thing that owns it also
|
|
61
|
+
* owns its release.
|
|
62
|
+
*/
|
|
63
|
+
export const RESERVATION_STATUSES = ["held", "consumed", "released", "expired"];
|
|
64
|
+
/** Default hold, when a caller names none. */
|
|
65
|
+
export const RESERVATION_DEFAULT_TTL_HOURS = 72;
|
|
66
|
+
/**
|
|
67
|
+
* 🛑 A ceiling, and it is load-bearing rather than defensive.
|
|
68
|
+
*
|
|
69
|
+
* A caller may pass any TTL it likes, which makes "hold this for a year" one
|
|
70
|
+
* typo away — and a year-long hold is indistinguishable from permanently
|
|
71
|
+
* deleted stock, because nothing in the UI shows a seller why their own
|
|
72
|
+
* product is unbuyable. 30 days is longer than any agreement window Part 6
|
|
73
|
+
* contemplates and short enough that a mistake self-heals.
|
|
74
|
+
*/
|
|
75
|
+
export const RESERVATION_MAX_TTL_HOURS = 24 * 30;
|
|
76
|
+
/** A reservation is only holding stock while it is `held` and unexpired. */
|
|
77
|
+
export function isReservationActive(reservation, now = new Date()) {
|
|
78
|
+
if (reservation.status !== "held")
|
|
79
|
+
return false;
|
|
80
|
+
const expires = toDate(reservation.expiresAt);
|
|
81
|
+
return expires !== null && expires.getTime() > now.getTime();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
|
|
85
|
+
* the path a document took to get here, and the adapter that normalises those
|
|
86
|
+
* is not reachable from this file. Returning `null` on anything unparseable is
|
|
87
|
+
* the honest answer — and callers treat an unparseable expiry as NOT active,
|
|
88
|
+
* so the failure mode is a hold released early rather than one held forever.
|
|
89
|
+
*/
|
|
90
|
+
function toDate(value) {
|
|
91
|
+
if (value instanceof Date)
|
|
92
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
93
|
+
if (value && typeof value.toDate === "function") {
|
|
94
|
+
const d = value.toDate();
|
|
95
|
+
return d instanceof Date && !Number.isNaN(d.getTime()) ? d : null;
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
98
|
+
const d = new Date(value);
|
|
99
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
export { toDate as reservationExpiryDate };
|
|
@@ -94,6 +94,14 @@ export interface AppLayoutShellProps {
|
|
|
94
94
|
userWishlistHref?: string;
|
|
95
95
|
/** Href for the user settings page — auto-generates PROFILE section in sidebar. */
|
|
96
96
|
userSettingsHref?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Href for the user's OWN reviews list — the target of the "Reviews" stat.
|
|
99
|
+
*
|
|
100
|
+
* The stat pointed at `${profileHref}#reviews` before, an anchor the profile
|
|
101
|
+
* page does not render, so the count linked to the top of an unrelated page.
|
|
102
|
+
* Omitting this prop hides the tile rather than guessing at a route.
|
|
103
|
+
*/
|
|
104
|
+
userReviewsHref?: string;
|
|
97
105
|
/** Locale switcher node rendered at the bottom of the sidebar. */
|
|
98
106
|
sidebarLocaleSlot?: React.ReactNode;
|
|
99
107
|
/** When true, shows theme toggle button in the sidebar footer. */
|
|
@@ -148,4 +156,4 @@ export interface AppLayoutShellProps {
|
|
|
148
156
|
};
|
|
149
157
|
};
|
|
150
158
|
}
|
|
151
|
-
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
|
|
159
|
+
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
|
|
@@ -17,6 +17,7 @@ const ROLE_DOT_BG_CLASS = {
|
|
|
17
17
|
import { useTheme } from "../../_internal/client/theme/index.js";
|
|
18
18
|
import { useAuth } from "../../react/contexts/SessionContext.js";
|
|
19
19
|
import { useVisualViewportInset } from "../../react/hooks/useVisualViewportInset.js";
|
|
20
|
+
import { useListingTypeFlags } from "../../react/hooks/useListingTypeFlags.js";
|
|
20
21
|
import { isBuyerUser } from "../auth/role-predicates.js";
|
|
21
22
|
import { NavbarWithSettings } from "./NavbarWithSettings.js";
|
|
22
23
|
import SideActionDock from "./SideActionDock.js";
|
|
@@ -135,13 +136,20 @@ function SidebarUserHeader({ user, onClose, profileHref, }) {
|
|
|
135
136
|
function SidebarGuestHeader({ sidebarTitle, onClose, }) {
|
|
136
137
|
return (_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Div, { textWeight: "semibold", textSize: "sm", color: "primary", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "rounded-full p-[var(--appkit-space-2)] hover:bg-surface-hover hover:text-zinc-900 text-[var(--appkit-color-text-muted)] dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }));
|
|
137
138
|
}
|
|
138
|
-
function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
|
|
139
|
+
function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
|
|
139
140
|
const hasLegacyItems = sidebarItems.length > 0;
|
|
140
141
|
const hasSections = !!(sidebarSections && sidebarSections.length > 0);
|
|
141
142
|
const isAuthenticated = !!user;
|
|
142
143
|
const role = user?.role ?? "user";
|
|
143
144
|
const isAdminOrSeller = role === "admin" || role === "seller";
|
|
144
145
|
const resolvedStoreHref = storeHref ?? sellerHref;
|
|
146
|
+
/*
|
|
147
|
+
* Defaults to ENABLED while site settings are loading, matching the hook's
|
|
148
|
+
* own contract — hiding the tile on first paint and popping it in a moment
|
|
149
|
+
* later is worse than showing it to the handful of installs that have
|
|
150
|
+
* auctions switched off.
|
|
151
|
+
*/
|
|
152
|
+
const auctionsEnabled = useListingTypeFlags().isEnabled("auction");
|
|
145
153
|
const labels = {
|
|
146
154
|
sectionTitle: sidebarProfileLabels?.sectionTitle ?? "Profile",
|
|
147
155
|
profile: sidebarProfileLabels?.profile ?? "My Profile",
|
|
@@ -177,10 +185,10 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
177
185
|
action.variant === "outline"
|
|
178
186
|
? "border hover:bg-surface-hover border-[var(--appkit-color-border)] text-[var(--appkit-color-text)]"
|
|
179
187
|
: "bg-primary text-white hover:bg-primary-600 dark:bg-primary dark:hover:bg-primary-600 btn-glow",
|
|
180
|
-
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(TextLink, { href:
|
|
188
|
+
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && userReviewsHref && (_jsxs(TextLink, { href: userReviewsHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), auctionsEnabled && user.stats.auctionsWon != null && userOrdersHref && (_jsxs(TextLink, { href: `${userOrdersHref}?orderType=auction`, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800 dark:hover:text-primary-200", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
|
|
181
189
|
}
|
|
182
190
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
183
|
-
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
191
|
+
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
184
192
|
// QueryClientProvider used to be created here so AppLayoutShell could stand
|
|
185
193
|
// alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
|
|
186
194
|
// turned out to be a peer-dep duplicate of @tanstack/query-core under
|
|
@@ -228,7 +236,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
228
236
|
// by its countdown alone got no clearance). That mirror is gone: BottomChrome
|
|
229
237
|
// measures the real tier and publishes --bottom-chrome-height, and the footer
|
|
230
238
|
// reserves space from that. Nothing needs to guess any more.
|
|
231
|
-
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
239
|
+
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, userReviewsHref: userReviewsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
232
240
|
const normalizedLightBackground = {
|
|
233
241
|
type: lightBackground.type,
|
|
234
242
|
value: lightBackground.value,
|
|
@@ -233,7 +233,8 @@ export default function BottomActions() {
|
|
|
233
233
|
* own reveal — bulk mode is a state the user just entered deliberately, and
|
|
234
234
|
* making them scroll to act on it would be a puzzle.
|
|
235
235
|
*/
|
|
236
|
-
const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId)
|
|
236
|
+
const { revealed: ctaRevealed } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
|
|
237
|
+
const revealed = ctaRevealed || isBulkMode;
|
|
237
238
|
const isVisible = hasContent && revealed;
|
|
238
239
|
// A panel left open after its content is withdrawn (bulk mode taking over, or
|
|
239
240
|
// the page clearing it) would sit there as an empty sheet.
|
|
@@ -32,12 +32,20 @@ export interface BottomNavbarUser {
|
|
|
32
32
|
};
|
|
33
33
|
zoom: number;
|
|
34
34
|
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* The account holder's own activity AS A BUYER, derived live by
|
|
37
|
+
* `/api/user/profile`. `undefined` on a member means the count could not be
|
|
38
|
+
* taken — render nothing, never 0.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 `itemsSold` and `rating` were removed, not forgotten. `itemsSold` is a
|
|
41
|
+
* SELLER number and had no business in the account holder's own profile
|
|
42
|
+
* block (see `AppLayoutShell`); `rating` had no writer anywhere in either
|
|
43
|
+
* repo, so its `!= null` guard meant the tile could never render.
|
|
44
|
+
*/
|
|
35
45
|
stats?: {
|
|
36
46
|
totalOrders?: number;
|
|
37
47
|
auctionsWon?: number;
|
|
38
|
-
itemsSold?: number;
|
|
39
48
|
reviewsCount?: number;
|
|
40
|
-
rating?: number;
|
|
41
49
|
} | null;
|
|
42
50
|
}
|
|
43
51
|
export interface BottomNavbarProps {
|
|
@@ -54,7 +54,7 @@ import { useClickOutside } from "../../react/index.js";
|
|
|
54
54
|
import { useHandMode } from "../../_internal/client/hand-mode/index.js";
|
|
55
55
|
import { useCtaReveal } from "./useCtaReveal.js";
|
|
56
56
|
import { useIsDesktop } from "./useIsDesktop.js";
|
|
57
|
-
import { ActionRow, Div, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
|
|
57
|
+
import { ActionRow, Div, ICON_SIZE, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
|
|
58
58
|
const PANEL_SURFACE = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_94%,transparent)] backdrop-blur-md border border-[var(--appkit-color-border)]";
|
|
59
59
|
const FLEX_CENTER = "flex items-center justify-center";
|
|
60
60
|
const CLS_COUNT_BADGE = "bg-error-solid text-error-on-solid";
|
|
@@ -143,8 +143,40 @@ export default function SideActionDock() {
|
|
|
143
143
|
!!infoLabel ||
|
|
144
144
|
!!secondaryLabel;
|
|
145
145
|
// Rule 3 — a selection is its own reveal.
|
|
146
|
-
const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId)
|
|
146
|
+
const { revealed: ctaRevealed, hasAnchor, scrollToAnchor, } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
|
|
147
|
+
const revealed = ctaRevealed || isBulkMode;
|
|
147
148
|
const isVisible = isDesktop && hasContent && revealed;
|
|
149
|
+
/*
|
|
150
|
+
* ── Rule 4 — a page with a REAL Buy button gets a way BACK to it ─────────
|
|
151
|
+
*
|
|
152
|
+
* On a detail page the dock was a second copy of the CTA you had just
|
|
153
|
+
* scrolled past, which is the one thing rule 2 says a floating control must
|
|
154
|
+
* not be: `useCtaReveal` already means "you have met the Buy button and left
|
|
155
|
+
* it", so re-offering the purchase mid-screen answers a question the user
|
|
156
|
+
* did not ask. Taking them back to the real control answers the one they
|
|
157
|
+
* did — and it keeps the purchase decision on the page's own CTA, beside
|
|
158
|
+
* the price, the variant and the stock state that qualify it.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 Gated on `hasAnchor`, NOT on a route or a listing type. A bulk screen
|
|
161
|
+
* marks no anchor, so it keeps the centred dock carrying its actions — which
|
|
162
|
+
* is correct, because there was never an inline control there to return to.
|
|
163
|
+
* `ctaAnchorProps` has exactly one caller today (`ProductDetailPageView`),
|
|
164
|
+
* so this branch is opt-in by construction and ~70 admin bulk bars cannot
|
|
165
|
+
* reach it.
|
|
166
|
+
*
|
|
167
|
+
* It sits in the bottom-right cluster above `BackToTop` and shares that
|
|
168
|
+
* control's bottom-tier `calc()` verbatim, so the two stack rather than
|
|
169
|
+
* overlap — and rule 1's objection (a bottom-anchored bar reads as a browser
|
|
170
|
+
* download bar) does not apply to a single round puck in the corner that is
|
|
171
|
+
* already where the scroll affordance lives.
|
|
172
|
+
*
|
|
173
|
+
* 🛑 The RETURN for this branch is deliberately deferred to just above the
|
|
174
|
+
* main one. An early return here would sit above this component's remaining
|
|
175
|
+
* `useEffect`s, so the hook order would differ between a detail page and a
|
|
176
|
+
* bulk screen — React's "rendered fewer hooks than expected" crash, and the
|
|
177
|
+
* kind that only shows up on the page nobody opened during review.
|
|
178
|
+
*/
|
|
179
|
+
const isBackToCta = isDesktop && hasAnchor && !isBulkMode;
|
|
148
180
|
// Rule 1 — opposite the sidebar, which docks to the hand-mode side.
|
|
149
181
|
const dockSideClass = hand === "left" ? "right-0" : "left-0";
|
|
150
182
|
const panelAlignClass = hand === "left" ? "items-end" : "items-start";
|
|
@@ -216,6 +248,24 @@ export default function SideActionDock() {
|
|
|
216
248
|
const fabLabel = isBulkMode
|
|
217
249
|
? `${bulk?.selectedCount ?? 0} selected — bulk actions`
|
|
218
250
|
: (primaryAction?.label ?? infoLabel ?? "Page actions");
|
|
251
|
+
// Rule 4's deferred return — below every hook, for the reason given above.
|
|
252
|
+
if (isBackToCta) {
|
|
253
|
+
if (!ctaRevealed)
|
|
254
|
+
return null;
|
|
255
|
+
return (_jsx(Div, { className: [
|
|
256
|
+
"fixed pointer-events-auto z-[var(--appkit-z-back-to-top)]",
|
|
257
|
+
hand === "left" ? "left-4" : "right-4",
|
|
258
|
+
/*
|
|
259
|
+
* `BackToTop`'s own expression verbatim — including its `4rem`
|
|
260
|
+
* default for `--bottom-nav-height`, not the `0px` the dock's other
|
|
261
|
+
* branch would imply — plus 3.5rem to clear that control's touch
|
|
262
|
+
* box and its gap. Copying the expression rather than inventing one
|
|
263
|
+
* is what keeps the two stacked when the tier changes height.
|
|
264
|
+
*/
|
|
265
|
+
"bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--bottom-chrome-height,0px)+var(--appkit-space-4)+3.5rem)]",
|
|
266
|
+
"transition-[bottom] duration-300 ease-out",
|
|
267
|
+
].join(" "), "data-section": "back-to-cta", children: _jsxs(Button, { type: "button", variant: "primary", size: "sm", onClick: scrollToAnchor, "aria-label": `Back to ${primaryAction?.label ?? "the buy button"}`, children: [_jsx(ChevronUp, { "aria-hidden": "true", size: ICON_SIZE.sm }), `Back to ${primaryAction?.label ?? "buy"}`] }) }));
|
|
268
|
+
}
|
|
219
269
|
return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", className: [
|
|
220
270
|
"fixed z-[var(--appkit-z-overlay)] pointer-events-auto flex flex-col",
|
|
221
271
|
panelAlignClass,
|
|
@@ -23,6 +23,15 @@ const iconBtn = "flex items-center justify-center w-11 h-11 rounded-lg text-[var
|
|
|
23
23
|
* `important: true`, so the utility would beat the unlayered rule and the badge
|
|
24
24
|
* would silently stop mirroring. The vertical `-top-0.5` is hand-neutral and stays. */
|
|
25
25
|
const countBadge = "appkit-hand-badge absolute -top-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-solid text-error-on-solid text-[10px] font-bold leading-none";
|
|
26
|
+
/**
|
|
27
|
+
* Applied to the GLYPH, never the link, when the icon is carrying something.
|
|
28
|
+
*
|
|
29
|
+
* On the glyph so the count badge stays put: animating the link would swing
|
|
30
|
+
* the number with it, which turns a readable badge into a moving target. The
|
|
31
|
+
* keyframes and the `prefers-reduced-motion` opt-out live in
|
|
32
|
+
* `TitleBarLayout.style.css`.
|
|
33
|
+
*/
|
|
34
|
+
const nudgeWhenCarrying = (count) => count > 0 ? "appkit-nav-icon--has-items" : "";
|
|
26
35
|
/**
|
|
27
36
|
* TitleBarLayout — generic top sticky title-bar shell.
|
|
28
37
|
*
|
|
@@ -50,13 +59,13 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
50
59
|
// Dedicated notification bell — rendered immediately before the wishlist
|
|
51
60
|
// icon (per layout request). Falls back to invisible when no href is set;
|
|
52
61
|
// the count badge mirrors the wishlist/cart pattern.
|
|
53
|
-
const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg" }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
|
|
62
|
+
const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg", className: nudgeWhenCarrying(unreadNotificationCount) }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
|
|
54
63
|
// D9 — chat. Same shape as the notification bell: absent entirely when no
|
|
55
64
|
// href is supplied (the consumer only passes one when
|
|
56
65
|
// `siteSettings.chat.enabled === true`), never rendered-but-disabled.
|
|
57
|
-
const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg" }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
|
|
58
|
-
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg" }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
|
|
59
|
-
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg" }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
|
|
66
|
+
const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg", className: nudgeWhenCarrying(unreadMessageCount) }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
|
|
67
|
+
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg", className: nudgeWhenCarrying(wishlistCount) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
|
|
68
|
+
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg", className: nudgeWhenCarrying(cartCount) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
|
|
60
69
|
const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, "data-tour": "nav-profile", children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
|
|
61
70
|
const authButtonsEl = !user && (loginHref || registerHref) ? (
|
|
62
71
|
// Mirrors with TB1 so Register (the primary CTA) keeps the outer, more
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Title-bar icons that are carrying something.
|
|
3
|
+
*
|
|
4
|
+
* The count badge already states the fact, but it is a 16px dot in the corner
|
|
5
|
+
* of a dense bar — easy to miss on a phone, and it never changes once drawn.
|
|
6
|
+
* A small periodic nudge is what makes "you have three things in your cart"
|
|
7
|
+
* noticeable without adding a second place to read the number.
|
|
8
|
+
*
|
|
9
|
+
* 🛑 It is a PERIODIC nudge, not a loop. The movement lasts about half a
|
|
10
|
+
* second at the END of a 10s cycle and the icon is still for the other ~9.5s,
|
|
11
|
+
* so it reads as an occasional glance-catcher rather than a permanent
|
|
12
|
+
* animation sitting in the chrome of every page. A continuously-moving element
|
|
13
|
+
* in a fixed header is the kind of thing people learn to tune out — or leave.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 The percentages and the duration are ONE number, not two. The keyframe
|
|
16
|
+
* stops are a fraction of the cycle, so changing `10s` alone silently
|
|
17
|
+
* stretches the nudge itself — at 20s the same stops would swing the bell over
|
|
18
|
+
* a full second, which reads as a glitch rather than a nudge. Rescale the
|
|
19
|
+
* stops with it: the window is 4.8% here, which is ~480ms of a 10s cycle.
|
|
20
|
+
*
|
|
21
|
+
* The pivot is near the TOP of the glyph so the motion reads as a bell swing
|
|
22
|
+
* rather than a wobble around the middle, which is what the notification bell
|
|
23
|
+
* wants and what the cart and wishlist tolerate.
|
|
24
|
+
*/
|
|
25
|
+
@keyframes appkit-nav-icon-nudge {
|
|
26
|
+
0%,
|
|
27
|
+
95.2%,
|
|
28
|
+
100% {
|
|
29
|
+
transform: rotate(0deg);
|
|
30
|
+
}
|
|
31
|
+
96.1% {
|
|
32
|
+
transform: rotate(-12deg);
|
|
33
|
+
}
|
|
34
|
+
97.3% {
|
|
35
|
+
transform: rotate(9deg);
|
|
36
|
+
}
|
|
37
|
+
98.5% {
|
|
38
|
+
transform: rotate(-5deg);
|
|
39
|
+
}
|
|
40
|
+
99.4% {
|
|
41
|
+
transform: rotate(2deg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.appkit-nav-icon--has-items {
|
|
46
|
+
transform-origin: 50% 15%;
|
|
47
|
+
animation: appkit-nav-icon-nudge 10s ease-in-out infinite;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* 🛑 Honoured, not approximated. This is decoration carrying no information
|
|
52
|
+
* the badge does not already carry, so for a reader who has asked for less
|
|
53
|
+
* motion the correct amount is none — not a shorter or gentler version.
|
|
54
|
+
*/
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
.appkit-nav-icon--has-items {
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -21,4 +21,19 @@
|
|
|
21
21
|
* page that owns it mounts AFTER this dock — the dock lives in the app shell
|
|
22
22
|
* and the CTA arrives with the route.
|
|
23
23
|
*/
|
|
24
|
-
export
|
|
24
|
+
export interface CtaRevealState {
|
|
25
|
+
/** Should the floating dock / bar be on screen? */
|
|
26
|
+
revealed: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Does this page mark an inline CTA at all?
|
|
29
|
+
*
|
|
30
|
+
* The discriminator between a DETAIL page (a real Buy button exists, so the
|
|
31
|
+
* floating control's job is to take you back to it) and a BULK screen (no
|
|
32
|
+
* Buy button was ever there, so the floating control IS the only affordance
|
|
33
|
+
* and must carry the actions itself).
|
|
34
|
+
*/
|
|
35
|
+
hasAnchor: boolean;
|
|
36
|
+
/** Scroll the inline CTA back into view. A no-op when there is no anchor. */
|
|
37
|
+
scrollToAnchor: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function useCtaReveal(enabled?: boolean, anchorId?: string): CtaRevealState;
|
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { CTA_ANCHOR_ATTR } from "./cta-anchor.js";
|
|
4
|
-
/**
|
|
5
|
-
* useCtaReveal — "has the user met the Buy button and scrolled past it?"
|
|
6
|
-
*
|
|
7
|
-
* Returns true when the floating action dock should be on screen:
|
|
8
|
-
*
|
|
9
|
-
* - the page marks NO CTA anchor → true immediately (nothing to wait
|
|
10
|
-
* for; an admin bulk bar has no Buy button)
|
|
11
|
-
* - the page marks one, not yet seen → false (the inline CTA is doing the
|
|
12
|
-
* job; a second copy of it beside itself is noise)
|
|
13
|
-
* - seen, and now out of view → true
|
|
14
|
-
* - seen, and scrolled back into view → false again
|
|
15
|
-
*
|
|
16
|
-
* 🛑 `IntersectionObserver`, never a scroll-position threshold. The bar this
|
|
17
|
-
* replaces used `window.scrollY > 400`, which is a guess about where a CTA sits
|
|
18
|
-
* — wrong on a short page (the dock appears while the real button is still on
|
|
19
|
-
* screen) and wrong on a long gallery (it appears before the CTA is reached).
|
|
20
|
-
* Observing the element itself is the same rule stated exactly, and it costs no
|
|
21
|
-
* scroll handler.
|
|
22
|
-
*
|
|
23
|
-
* The anchor is looked up on an interval-free `MutationObserver` because the
|
|
24
|
-
* page that owns it mounts AFTER this dock — the dock lives in the app shell
|
|
25
|
-
* and the CTA arrives with the route.
|
|
26
|
-
*/
|
|
27
4
|
export function useCtaReveal(enabled = true, anchorId) {
|
|
28
5
|
const [anchor, setAnchor] = useState(null);
|
|
29
6
|
const [anchorExists, setAnchorExists] = useState(false);
|
|
@@ -74,9 +51,23 @@ export function useCtaReveal(enabled = true, anchorId) {
|
|
|
74
51
|
io.observe(anchor);
|
|
75
52
|
return () => io.disconnect();
|
|
76
53
|
}, [anchor]);
|
|
54
|
+
/*
|
|
55
|
+
* Re-read at CLICK time rather than closing over `anchor`. A client-side
|
|
56
|
+
* navigation swaps the page under a dock that never unmounts, and the
|
|
57
|
+
* element captured when the effect last ran may already be detached — a
|
|
58
|
+
* `scrollIntoView` on a detached node silently does nothing.
|
|
59
|
+
*/
|
|
60
|
+
const scrollToAnchor = () => {
|
|
61
|
+
if (typeof document === "undefined")
|
|
62
|
+
return;
|
|
63
|
+
const el = anchorId
|
|
64
|
+
? document.getElementById(anchorId)
|
|
65
|
+
: document.querySelector(`[${CTA_ANCHOR_ATTR}]`);
|
|
66
|
+
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
67
|
+
};
|
|
77
68
|
if (!enabled)
|
|
78
|
-
return false;
|
|
69
|
+
return { revealed: false, hasAnchor: anchorExists, scrollToAnchor };
|
|
79
70
|
if (!anchorExists)
|
|
80
|
-
return true;
|
|
81
|
-
return seen && !inView;
|
|
71
|
+
return { revealed: true, hasAnchor: false, scrollToAnchor };
|
|
72
|
+
return { revealed: seen && !inView, hasAnchor: true, scrollToAnchor };
|
|
82
73
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
+
import { type JournalEntryDocument } from "../schemas/firestore";
|
|
3
|
+
import { type PostingInput } from "../../../_internal/shared/features/ledger/posting";
|
|
4
|
+
/**
|
|
5
|
+
* How many entries one period read may return.
|
|
6
|
+
*
|
|
7
|
+
* Sized against a plausible month rather than a round number: a few hundred
|
|
8
|
+
* orders, each posting two or three entries, plus settlements and expenses.
|
|
9
|
+
* 2,000 leaves real headroom and still fits comfortably inside a function's
|
|
10
|
+
* memory and the ~3-round-trip budget.
|
|
11
|
+
*/
|
|
12
|
+
export declare const JOURNAL_PERIOD_SCAN_LIMIT = 2000;
|
|
13
|
+
export declare class JournalRepository extends BaseRepository<JournalEntryDocument> {
|
|
14
|
+
constructor();
|
|
15
|
+
/**
|
|
16
|
+
* 🛑 APPEND-ONLY, and the method bodies refuse rather than a comment
|
|
17
|
+
* asking nicely.
|
|
18
|
+
*
|
|
19
|
+
* `inventoryMovements` takes the same stance for the same reason: a
|
|
20
|
+
* Firestore update succeeds silently, so a caller who edits history never
|
|
21
|
+
* finds out, and a ledger whose rows can be edited cannot be used as
|
|
22
|
+
* evidence of anything. The correction mechanism is `reverse()`.
|
|
23
|
+
*/
|
|
24
|
+
update(): Promise<never>;
|
|
25
|
+
delete(): Promise<never>;
|
|
26
|
+
/**
|
|
27
|
+
* Post one entry.
|
|
28
|
+
*
|
|
29
|
+
* 🛑 **The balance check and the lock check are INSIDE the transaction.**
|
|
30
|
+
* Reading the period's state first and writing after is a TOCTOU window: an
|
|
31
|
+
* admin filing a return between the two gets an entry in the period they
|
|
32
|
+
* just closed, which is the one outcome the lock exists to prevent. It is a
|
|
33
|
+
* narrow window and it is exactly the kind that shows up once, in the
|
|
34
|
+
* quarter it matters.
|
|
35
|
+
*/
|
|
36
|
+
post(input: PostingInput): Promise<JournalEntryDocument>;
|
|
37
|
+
/**
|
|
38
|
+
* Reverse an entry.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 **Refuses a SECOND reversal of the same entry.** Without the check a
|
|
41
|
+
* double-click posts the opposite legs twice, and the books then show the
|
|
42
|
+
* original plus two reversals — a net position that is the mirror image of
|
|
43
|
+
* the truth, balanced, and wrong. `reversedByEntryId` is set inside the
|
|
44
|
+
* same transaction that reads it, so the second attempt loses the race
|
|
45
|
+
* rather than both succeeding.
|
|
46
|
+
*/
|
|
47
|
+
reverse(entryId: string, reason: string, at?: Date): Promise<JournalEntryDocument>;
|
|
48
|
+
/**
|
|
49
|
+
* Close a period.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 Takes the reason and the actor, both required. "Who closed October
|
|
52
|
+
* and under which return" is the first question anyone asks when a figure
|
|
53
|
+
* disagrees with a filing, and a lock with no answer to it is a lock
|
|
54
|
+
* somebody will simply reopen.
|
|
55
|
+
*/
|
|
56
|
+
lockPeriod(period: string, lockedByUid: string, lockedReason: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* 🛑 There is deliberately NO `unlockPeriod`.
|
|
59
|
+
*
|
|
60
|
+
* Reopening a period a return was filed from is not an operation the
|
|
61
|
+
* system should make easy — the correct response to a late correction is a
|
|
62
|
+
* reversing entry in the OPEN period, which is what `resolvePostingPeriod`
|
|
63
|
+
* computes and what every accountant expects. If a period genuinely must
|
|
64
|
+
* be reopened (a return withdrawn, say), that is a deliberate act with a
|
|
65
|
+
* paper trail outside this system, and a console write is the right amount
|
|
66
|
+
* of friction for it.
|
|
67
|
+
*
|
|
68
|
+
* This method exists only to say so. Deleting it would leave the absence
|
|
69
|
+
* looking like an oversight somebody helpfully fills in.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Every entry in a period, for a trial balance or a return.
|
|
73
|
+
*
|
|
74
|
+
* 🛑 **BOUNDED, and truncation is REPORTED rather than silent.** An
|
|
75
|
+
* unbounded read is the Rule #6 violation, but the reason it matters here
|
|
76
|
+
* is sharper than cost: a trial balance computed from a silently truncated
|
|
77
|
+
* page is a trial balance that BALANCES — both sides lose the same
|
|
78
|
+
* entries — while being wrong. It would look perfect and reconcile to
|
|
79
|
+
* nothing.
|
|
80
|
+
*
|
|
81
|
+
* So `truncated` is part of the return type and callers must handle it.
|
|
82
|
+
* The same stance `listPublicProducts` takes with its own `truncated`
|
|
83
|
+
* flag: a floor rendered as "50+", never a figure presented as a total.
|
|
84
|
+
*
|
|
85
|
+
* The real answer at volume is a period rollup written by a scheduled job
|
|
86
|
+
* — the `revenueRollup` pattern — and this is deliberately NOT that, so
|
|
87
|
+
* the flag is what forces the question rather than hiding it.
|
|
88
|
+
*/
|
|
89
|
+
listByPeriod(period: string, limit?: number): Promise<{
|
|
90
|
+
entries: JournalEntryDocument[];
|
|
91
|
+
truncated: boolean;
|
|
92
|
+
}>;
|
|
93
|
+
/** Whether a period is closed. Absence means open — see `post`. */
|
|
94
|
+
isPeriodLocked(period: string): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* The cutover entry.
|
|
97
|
+
*
|
|
98
|
+
* 🛑 **NOT a backfill.** Pre-cutover data here is seeded and test, so
|
|
99
|
+
* replaying it would post fixtures as trade — the first financial year is
|
|
100
|
+
* therefore split across the old rollups and this ledger, and the
|
|
101
|
+
* "pre-ledger estimate" label on the old reports is what keeps that
|
|
102
|
+
* honest rather than hiding it.
|
|
103
|
+
*
|
|
104
|
+
* 🛑 Refuses to run twice. An opening balance posted a second time doubles
|
|
105
|
+
* every starting figure, and because both entries balance nothing
|
|
106
|
+
* downstream would object.
|
|
107
|
+
*/
|
|
108
|
+
postOpeningBalances(at: Date, lines: PostingInput["lines"]): Promise<JournalEntryDocument>;
|
|
109
|
+
}
|
|
110
|
+
export declare const journalRepository: JournalRepository;
|