@mohasinac/appkit 3.1.7 → 3.2.1
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/_internal/client/features/layout/DashboardLayoutClient.js +39 -9
- package/dist/_internal/client/features/tour/TourProvider.d.ts +12 -0
- package/dist/_internal/client/features/tour/TourProvider.js +19 -0
- package/dist/_internal/server/features/checkout/actions.js +8 -2
- package/dist/_internal/server/features/media/contextGuards.js +2 -2
- package/dist/_internal/server/features/orders/actions.d.ts +6 -0
- package/dist/_internal/server/features/orders/actions.js +43 -1
- package/dist/_internal/server/features/products/data.js +5 -1
- package/dist/_internal/server/jobs/core/assignSpinPrize.js +4 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +4 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +4 -0
- package/dist/_internal/server/jobs/core/bundleStockSync.js +4 -0
- package/dist/_internal/server/jobs/core/payoutBatch.js +4 -0
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +4 -0
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +4 -0
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +4 -0
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +4 -0
- package/dist/_internal/server/jobs/core/promotions.js +4 -0
- package/dist/_internal/server/jobs/core/triggerEventRaffle.js +4 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +4 -0
- package/dist/_internal/shared/actions/action-registry.js +13 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +5 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +30 -4
- package/dist/features/admin/components/AdminOrdersView.js +5 -1
- package/dist/features/layout/AppLayoutShell.d.ts +3 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +3 -1
- package/dist/features/layout/TitleBarLayout.js +3 -2
- package/dist/features/orders/schemas/firestore.d.ts +9 -0
- package/dist/features/orders/schemas/firestore.js +1 -0
- package/dist/features/products/components/ProductForm.js +5 -3
- package/dist/features/products/components/ProductGrid.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/seed/categories-seed-data.d.ts +1 -0
- package/dist/seed/categories-seed-data.js +6 -0
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +1 -1
- package/dist/seed/orders-seed-data.js +63 -9
- package/dist/seed/products-standard-seed-data.js +15 -15
- package/dist/seed/site-settings-seed-data.js +15 -5
- package/dist/server.d.ts +1 -0
- package/dist/server.js +2 -0
- package/dist/styles.css +9 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PaginatedSelect.js +10 -1
- package/dist/ui/components/PaginatedSelect.style.css +8 -0
- package/dist/utils/id-generators.d.ts +6 -0
- package/dist/utils/id-generators.js +9 -0
- package/package.json +1 -1
|
@@ -34,36 +34,65 @@ import { Div } from "../../../../ui";
|
|
|
34
34
|
* Hoisted drawer-state hook — the matchMedia-aware open/close logic that was
|
|
35
35
|
* triplicated across admin/store/user layouts. Used internally by
|
|
36
36
|
* DashboardLayoutClient; not exported because it's not generically useful.
|
|
37
|
+
*
|
|
38
|
+
* storageKey: variant-scoped localStorage key (`appkit:sidebar-open:{variant}`)
|
|
39
|
+
* so admin/store/user each persist their own collapse state independently.
|
|
37
40
|
*/
|
|
38
|
-
function useResponsiveDrawer() {
|
|
41
|
+
function useResponsiveDrawer(storageKey) {
|
|
39
42
|
const [desktopOpen, setDesktopOpen] = useState(false);
|
|
40
43
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
41
44
|
const { registerNav, unregisterNav } = useDashboardNav();
|
|
42
45
|
const isDesktop = useCallback(() => typeof window !== "undefined" && window.matchMedia(DASHBOARD_DESKTOP_MEDIA_QUERY).matches, []);
|
|
46
|
+
// Restore persisted desktop-open state after hydration (avoids SSR mismatch).
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
try {
|
|
49
|
+
if (localStorage.getItem(storageKey) === "true")
|
|
50
|
+
setDesktopOpen(true);
|
|
51
|
+
}
|
|
52
|
+
catch { /* localStorage unavailable (private-browse / SSR fallback) */ }
|
|
53
|
+
}, [storageKey]);
|
|
43
54
|
const open = useCallback(() => {
|
|
44
55
|
startTransition(() => {
|
|
45
|
-
if (isDesktop())
|
|
56
|
+
if (isDesktop()) {
|
|
57
|
+
try {
|
|
58
|
+
localStorage.setItem(storageKey, "true");
|
|
59
|
+
}
|
|
60
|
+
catch { /* noop */ }
|
|
46
61
|
setDesktopOpen(true);
|
|
62
|
+
}
|
|
47
63
|
else
|
|
48
64
|
setMobileOpen(true);
|
|
49
65
|
});
|
|
50
|
-
}, [isDesktop]);
|
|
66
|
+
}, [isDesktop, storageKey]);
|
|
51
67
|
const close = useCallback(() => {
|
|
52
68
|
startTransition(() => {
|
|
53
|
-
if (isDesktop())
|
|
69
|
+
if (isDesktop()) {
|
|
70
|
+
try {
|
|
71
|
+
localStorage.setItem(storageKey, "false");
|
|
72
|
+
}
|
|
73
|
+
catch { /* noop */ }
|
|
54
74
|
setDesktopOpen(false);
|
|
75
|
+
}
|
|
55
76
|
else
|
|
56
77
|
setMobileOpen(false);
|
|
57
78
|
});
|
|
58
|
-
}, [isDesktop]);
|
|
79
|
+
}, [isDesktop, storageKey]);
|
|
59
80
|
const toggle = useCallback(() => {
|
|
60
81
|
startTransition(() => {
|
|
61
|
-
if (isDesktop())
|
|
62
|
-
setDesktopOpen((prev) =>
|
|
82
|
+
if (isDesktop()) {
|
|
83
|
+
setDesktopOpen((prev) => {
|
|
84
|
+
const next = !prev;
|
|
85
|
+
try {
|
|
86
|
+
localStorage.setItem(storageKey, String(next));
|
|
87
|
+
}
|
|
88
|
+
catch { /* noop */ }
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
63
92
|
else
|
|
64
93
|
setMobileOpen((prev) => !prev);
|
|
65
94
|
});
|
|
66
|
-
}, [isDesktop]);
|
|
95
|
+
}, [isDesktop, storageKey]);
|
|
67
96
|
useEffect(() => {
|
|
68
97
|
registerNav({ open, close, toggle });
|
|
69
98
|
return () => unregisterNav();
|
|
@@ -87,7 +116,8 @@ const DEFAULT_CONTENT_MAX_WIDTH = "max-w-screen-2xl";
|
|
|
87
116
|
export function DashboardLayoutClient({ variant, groups, permissions, activeHref: explicitActiveHref, responsive: _responsive, className, contentPadding, contentSurface, contentMaxWidth, children, }) {
|
|
88
117
|
const pathname = usePathname();
|
|
89
118
|
const activeHref = explicitActiveHref ?? pathname ?? "";
|
|
90
|
-
const
|
|
119
|
+
const storageKey = `appkit:sidebar-open:${variant}`;
|
|
120
|
+
const { desktopOpen, mobileOpen, close, toggle } = useResponsiveDrawer(storageKey);
|
|
91
121
|
const { data: settings } = useSiteSettings();
|
|
92
122
|
const navConfig = settings?.navConfig;
|
|
93
123
|
const filteredGroups = filterGroups(groups, navConfig, permissions);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TourContextValue {
|
|
3
|
+
startTour: () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function useTour(): TourContextValue;
|
|
6
|
+
/**
|
|
7
|
+
* TourProvider — skeleton shell for Patch 1.
|
|
8
|
+
* driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
|
|
9
|
+
*/
|
|
10
|
+
export declare function TourProvider({ children }: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
const TourContext = React.createContext({
|
|
5
|
+
startTour: () => { },
|
|
6
|
+
});
|
|
7
|
+
export function useTour() {
|
|
8
|
+
return React.useContext(TourContext);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* TourProvider — skeleton shell for Patch 1.
|
|
12
|
+
* driver.js steps are wired in Patch 2+; this provider is an identity wrapper.
|
|
13
|
+
*/
|
|
14
|
+
export function TourProvider({ children }) {
|
|
15
|
+
const startTour = React.useCallback(() => {
|
|
16
|
+
// driver.js will be imported and configured here in Patch 2+.
|
|
17
|
+
}, []);
|
|
18
|
+
return (_jsx(TourContext.Provider, { value: { startTour }, children: children }));
|
|
19
|
+
}
|
|
@@ -232,12 +232,18 @@ export async function createCheckoutOrderAction(input) {
|
|
|
232
232
|
const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
|
|
233
233
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
234
234
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
235
|
-
// W1-43 — enforce COD toggle.
|
|
235
|
+
// W1-43 — enforce COD toggle.
|
|
236
236
|
if (!adminBypass &&
|
|
237
237
|
paymentMethod === "cod" &&
|
|
238
238
|
siteSettings?.payment?.codEnabled === false) {
|
|
239
239
|
throw new ValidationError("Cash on Delivery is not currently accepted. Please choose another payment method.");
|
|
240
240
|
}
|
|
241
|
+
// P-1 — enforce UPI/cash manual payment toggle.
|
|
242
|
+
if (!adminBypass &&
|
|
243
|
+
(paymentMethod === "upi_manual" || paymentMethod === "cash") &&
|
|
244
|
+
siteSettings?.payment?.upiManualEnabled === false) {
|
|
245
|
+
throw new ValidationError("Manual UPI / cash payment is not currently accepted. Please choose another payment method.");
|
|
246
|
+
}
|
|
241
247
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
242
248
|
if (!cart.items || cart.items.length === 0) {
|
|
243
249
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
@@ -470,7 +476,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
470
476
|
});
|
|
471
477
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
472
478
|
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId, groupTotal, commissions);
|
|
473
|
-
const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual";
|
|
479
|
+
const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
|
|
474
480
|
const depositAmount = isCodLike
|
|
475
481
|
? Math.round(groupTotal * (commissions.codDepositPercent / 100) * 100) / 100
|
|
476
482
|
: undefined;
|
|
@@ -45,7 +45,7 @@ export const CONTEXT_LIMITS = {
|
|
|
45
45
|
RICH_TEXT_IMAGE_MAX: 20,
|
|
46
46
|
};
|
|
47
47
|
// Contexts that accept both image and PDF (proof documents).
|
|
48
|
-
const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof"];
|
|
48
|
+
const IMAGE_OR_PDF_CONTEXTS = ["shipping-proof", "refund-proof", "payment-proof"];
|
|
49
49
|
const PDF_ONLY_CONTEXTS = ["invoice", "payout-doc"];
|
|
50
50
|
const IMAGE_ONLY_CONTEXT_TYPES = new Set([
|
|
51
51
|
"store-logo",
|
|
@@ -215,7 +215,7 @@ export function applyMediaContextGuards({ detectedMime, context: ctx, }) {
|
|
|
215
215
|
return {
|
|
216
216
|
ok: false,
|
|
217
217
|
status: 400,
|
|
218
|
-
error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, or
|
|
218
|
+
error: "PDF uploads are only allowed for invoice, payout-doc, shipping-proof, refund-proof, or payment-proof contexts",
|
|
219
219
|
details: { context: ctx.type, detected: detectedMime },
|
|
220
220
|
};
|
|
221
221
|
}
|
|
@@ -3,3 +3,9 @@ export declare function createOrderAction(input: unknown): Promise<ActionResult<
|
|
|
3
3
|
export declare function cancelOrderAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
4
4
|
export declare function requestReturnAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
5
5
|
export declare function updateOrderStatusAction(input: unknown): Promise<ActionResult<unknown>>;
|
|
6
|
+
export declare function attachPaymentProofAction(orderId: string, proof: {
|
|
7
|
+
proofUrl: string;
|
|
8
|
+
transactionId?: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
}): Promise<ActionResult<void>>;
|
|
11
|
+
export declare function adminVerifyPaymentAction(orderId: string): Promise<ActionResult<void>>;
|
|
@@ -6,7 +6,7 @@ import { createOrderSchema, updateOrderStatusSchema, cancelOrderSchema, } from "
|
|
|
6
6
|
import { assertOrderCancellable, assertReturnWindowOpen } from "./service";
|
|
7
7
|
import { ValidationError } from "../../../shared/errors/index";
|
|
8
8
|
import { OrderNotFoundError, OrderOwnershipError } from "../../../shared/features/orders/errors";
|
|
9
|
-
import { isAdminUser } from "../../../../features/auth/role-predicates";
|
|
9
|
+
import { isAdminUser, isModeratorUser } from "../../../../features/auth/role-predicates";
|
|
10
10
|
export async function createOrderAction(input) {
|
|
11
11
|
return wrapAction(async () => {
|
|
12
12
|
const user = await requireRoleUser(["buyer", "seller", "admin"]);
|
|
@@ -58,3 +58,45 @@ export async function updateOrderStatusAction(input) {
|
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
// ── P-1 Manual payment proof ─────────────────────────────────────────────────
|
|
62
|
+
export async function attachPaymentProofAction(orderId, proof) {
|
|
63
|
+
return wrapAction(async () => {
|
|
64
|
+
const user = await requireRoleUser(["buyer", "seller", "admin"]);
|
|
65
|
+
const order = await orderRepository.findById(orderId).catch(() => null);
|
|
66
|
+
if (!order)
|
|
67
|
+
throw new OrderNotFoundError(orderId);
|
|
68
|
+
if (!isAdminUser(user) && order.userId !== user.uid)
|
|
69
|
+
throw new OrderOwnershipError(orderId);
|
|
70
|
+
const pm = order.paymentMethod ?? "";
|
|
71
|
+
if (pm !== "cash" && pm !== "upi_manual") {
|
|
72
|
+
throw new ValidationError("Payment proof can only be attached to cash or UPI orders");
|
|
73
|
+
}
|
|
74
|
+
if (order.paymentProofUrl) {
|
|
75
|
+
throw new ValidationError("PROOF_ALREADY_ATTACHED");
|
|
76
|
+
}
|
|
77
|
+
await orderRepository.update(orderId, {
|
|
78
|
+
paymentProofUrl: proof.proofUrl,
|
|
79
|
+
paymentTransactionId: proof.transactionId,
|
|
80
|
+
paymentProofMimeType: proof.mimeType,
|
|
81
|
+
paymentProofUploadedAt: new Date(),
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export async function adminVerifyPaymentAction(orderId) {
|
|
86
|
+
return wrapAction(async () => {
|
|
87
|
+
const user = await requireRoleUser(["admin", "moderator"]);
|
|
88
|
+
if (!isAdminUser(user) && !isModeratorUser(user)) {
|
|
89
|
+
throw new ValidationError("Only admin or moderator can verify payments");
|
|
90
|
+
}
|
|
91
|
+
const order = await orderRepository.findById(orderId).catch(() => null);
|
|
92
|
+
if (!order)
|
|
93
|
+
throw new OrderNotFoundError(orderId);
|
|
94
|
+
if (order.paymentStatus === "paid")
|
|
95
|
+
return; // idempotent
|
|
96
|
+
await orderRepository.update(orderId, {
|
|
97
|
+
paymentStatus: "paid",
|
|
98
|
+
paymentId: order.paymentTransactionId ?? order.paymentId ?? `manual-${orderId}`,
|
|
99
|
+
status: "processing",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -11,7 +11,11 @@ import { PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
/** Fetch a single product by slug or id, deduped per request. */
|
|
13
13
|
export const getProductForDetail = cache(async (slugOrId) => {
|
|
14
|
-
|
|
14
|
+
const product = (await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined)) ?? null;
|
|
15
|
+
// Fire-and-forget viewCount increment — must not affect response time
|
|
16
|
+
if (product)
|
|
17
|
+
void productRepository.incrementViewCount(product.id);
|
|
18
|
+
return product;
|
|
15
19
|
});
|
|
16
20
|
/** Fetch the first page of approved reviews for a product, deduped per request. */
|
|
17
21
|
export const getReviewsForProduct = cache(async (productId) => {
|
|
@@ -8,6 +8,10 @@ const EVENTS_COLLECTION = "events";
|
|
|
8
8
|
const EVENT_ENTRIES_COLLECTION = "eventEntries";
|
|
9
9
|
const COUPONS_COLLECTION = "coupons";
|
|
10
10
|
export async function runAssignSpinPrize(input, ctx) {
|
|
11
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
12
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping spin prize assignment");
|
|
13
|
+
return { eventId: input.eventId, userId: input.userId, reason: "feature_disabled" };
|
|
14
|
+
}
|
|
11
15
|
const { eventId, userId } = input;
|
|
12
16
|
const eventSnap = await ctx.db.collection(EVENTS_COLLECTION).doc(eventId).get();
|
|
13
17
|
if (!eventSnap.exists) {
|
|
@@ -57,6 +57,10 @@ async function settleAuction(ctx, product) {
|
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
export async function runAuctionSettlement(ctx) {
|
|
60
|
+
if (ctx.env("FEATURE_AUCTIONS") !== "true") {
|
|
61
|
+
ctx.logger.info("FEATURE_AUCTIONS disabled — skipping auction settlement");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
60
64
|
ctx.logger.info("Starting auction settlement sweep");
|
|
61
65
|
const expired = await productRepository.getExpiredAuctions(ctx.now);
|
|
62
66
|
if (expired.length === 0) {
|
|
@@ -13,6 +13,10 @@ function getBusinessDayCutoff(daysAgo) {
|
|
|
13
13
|
return cutoff;
|
|
14
14
|
}
|
|
15
15
|
export async function runAutoPayoutEligibility(ctx) {
|
|
16
|
+
if (ctx.env("FEATURE_PAYOUTS") !== "true") {
|
|
17
|
+
ctx.logger.info("FEATURE_PAYOUTS disabled — skipping auto-payout eligibility");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
ctx.logger.info("Starting daily auto-payout eligibility sweep", {
|
|
17
21
|
windowDays: AUTO_PAYOUT_WINDOW_DAYS,
|
|
18
22
|
});
|
|
@@ -36,6 +36,10 @@ async function computeBundleStockStatus(productIds, ctx) {
|
|
|
36
36
|
return "in_stock";
|
|
37
37
|
}
|
|
38
38
|
export async function runBundleStockSync(ctx) {
|
|
39
|
+
if (ctx.env("FEATURE_BUNDLES") !== "true") {
|
|
40
|
+
ctx.logger.info("FEATURE_BUNDLES disabled — skipping bundle stock sync");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
39
43
|
ctx.logger.info("Bundle stock sync starting (SB-UNI-V categories)");
|
|
40
44
|
const snap = await ctx.db
|
|
41
45
|
.collection(CATEGORIES_COLLECTION)
|
|
@@ -87,6 +87,10 @@ async function dispatch(ctx, entry) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
export async function runPayoutBatch(ctx) {
|
|
90
|
+
if (ctx.env("FEATURE_PAYOUTS") !== "true") {
|
|
91
|
+
ctx.logger.info("FEATURE_PAYOUTS disabled — skipping payout batch");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
90
94
|
ctx.logger.info("Starting payout batch sweep");
|
|
91
95
|
const pending = await payoutRepository.getPending();
|
|
92
96
|
if (pending.length === 0) {
|
|
@@ -2,6 +2,10 @@ import { PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names
|
|
|
2
2
|
const PRODUCT_COLLECTION = "products";
|
|
3
3
|
const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
|
|
4
4
|
export async function runPrizeRevealClose(ctx) {
|
|
5
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
6
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal close");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
5
9
|
ctx.logger.info("Prize reveal close sweep starting");
|
|
6
10
|
const snap = await ctx.db
|
|
7
11
|
.collection(PRODUCT_COLLECTION)
|
|
@@ -3,6 +3,10 @@ import { sendNotification } from "../../../../features/admin/actions/notificatio
|
|
|
3
3
|
import { ORDER_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
|
|
4
4
|
const ORDER_COLLECTION = "orders";
|
|
5
5
|
export async function runPrizeRevealExpiry(ctx) {
|
|
6
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
7
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal expiry");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
6
10
|
ctx.logger.info("Prize reveal expiry sweep starting");
|
|
7
11
|
const snap = await ctx.db
|
|
8
12
|
.collection(ORDER_COLLECTION)
|
|
@@ -5,6 +5,10 @@ const PRODUCT_COLLECTION = "products";
|
|
|
5
5
|
const ORDER_COLLECTION = "orders";
|
|
6
6
|
const PRIZE_DRAW_LISTING_TYPE = "prize-draw";
|
|
7
7
|
export async function runPrizeRevealOpen(ctx) {
|
|
8
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
9
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal open");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
ctx.logger.info("Prize reveal open sweep starting");
|
|
9
13
|
const snap = await ctx.db
|
|
10
14
|
.collection(PRODUCT_COLLECTION)
|
|
@@ -4,6 +4,10 @@ import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names"
|
|
|
4
4
|
const ORDER_COLLECTION = "orders";
|
|
5
5
|
const ONE_DAY_MS = 86400000;
|
|
6
6
|
export async function runPrizeRevealReminder(ctx) {
|
|
7
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
8
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping prize reveal reminder");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
7
11
|
ctx.logger.info("Prize reveal reminder sweep starting");
|
|
8
12
|
const cutoff = new Date(ctx.now.getTime() + ONE_DAY_MS);
|
|
9
13
|
const snap = await ctx.db
|
|
@@ -6,6 +6,10 @@ import { ProductStatusValues } from "../../../../features/products/schemas/fires
|
|
|
6
6
|
* (coupon.startDate IS NULL OR <= now) filter Firestore can't express.
|
|
7
7
|
*/
|
|
8
8
|
export async function runPromotions(_input, ctx) {
|
|
9
|
+
if (ctx.env("FEATURE_EVENTS") !== "true") {
|
|
10
|
+
ctx.logger.info("FEATURE_EVENTS disabled — skipping promotions");
|
|
11
|
+
return { promotedProducts: [], featuredProducts: [], activeCoupons: [] };
|
|
12
|
+
}
|
|
9
13
|
ctx.logger.info("Promotions data requested");
|
|
10
14
|
const now = new Date();
|
|
11
15
|
const nowIso = now.toISOString();
|
|
@@ -7,6 +7,10 @@ import { EVENT_ENTRY_FIELDS } from "../../../../constants/field-names";
|
|
|
7
7
|
const EVENTS_COLLECTION = "events";
|
|
8
8
|
const EVENT_ENTRIES_COLLECTION = "eventEntries";
|
|
9
9
|
export async function runTriggerEventRaffle(input, ctx) {
|
|
10
|
+
if (ctx.env("FEATURE_PRIZE_DRAWS") !== "true") {
|
|
11
|
+
ctx.logger.info("FEATURE_PRIZE_DRAWS disabled — skipping event raffle");
|
|
12
|
+
return { eventId: input.eventId, raffleEntryCount: 0 };
|
|
13
|
+
}
|
|
10
14
|
const eventRef = ctx.db.collection(EVENTS_COLLECTION).doc(input.eventId);
|
|
11
15
|
const eventSnap = await eventRef.get();
|
|
12
16
|
if (!eventSnap.exists) {
|
|
@@ -7,6 +7,10 @@ import { BATCH_LIMIT } from "../handlers/messages";
|
|
|
7
7
|
import { getDefaultCurrency } from "../../../../core";
|
|
8
8
|
const PLATFORM_COMMISSION_RATE = 0.05;
|
|
9
9
|
export async function runWeeklyPayoutEligibility(ctx) {
|
|
10
|
+
if (ctx.env("FEATURE_PAYOUTS") !== "true") {
|
|
11
|
+
ctx.logger.info("FEATURE_PAYOUTS disabled — skipping weekly payout eligibility");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
10
14
|
ctx.logger.info("Starting weekly payout eligibility sweep");
|
|
11
15
|
const eligible = await orderRepository.getEligibleShiprocket();
|
|
12
16
|
if (eligible.length === 0) {
|
|
@@ -835,6 +835,19 @@ export const ACTIONS = {
|
|
|
835
835
|
kind: "secondary",
|
|
836
836
|
permissions: ["admin"],
|
|
837
837
|
},
|
|
838
|
+
"verify-payment": {
|
|
839
|
+
id: "admin.verify-payment",
|
|
840
|
+
label: "Verify Payment",
|
|
841
|
+
ariaLabel: "Mark payment as received and verified",
|
|
842
|
+
description: "Confirms manual cash/UPI payment was received. Moves order to Processing.",
|
|
843
|
+
kind: "primary",
|
|
844
|
+
permissions: ["admin", "moderator"],
|
|
845
|
+
confirmation: {
|
|
846
|
+
title: "Verify payment?",
|
|
847
|
+
body: "This marks the payment as received. The order will move to Processing. This action cannot be undone.",
|
|
848
|
+
confirmLabel: "Yes, verify",
|
|
849
|
+
},
|
|
850
|
+
},
|
|
838
851
|
"verify-vendor": {
|
|
839
852
|
id: "admin.verify-vendor",
|
|
840
853
|
label: "Verify vendor",
|
|
@@ -4,5 +4,5 @@ export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
|
|
|
4
4
|
readonly platformShippingPercent: 10;
|
|
5
5
|
readonly platformShippingFixedMin: 50;
|
|
6
6
|
};
|
|
7
|
-
export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cod", "online", "upi_manual", "admin_bypass"];
|
|
7
|
+
export declare const CHECKOUT_PAYMENT_METHODS: readonly ["cash", "cod", "online", "upi_manual", "admin_bypass"];
|
|
8
8
|
export type CheckoutPaymentMethod = (typeof CHECKOUT_PAYMENT_METHODS)[number];
|
|
@@ -4,4 +4,4 @@ export const CHECKOUT_DEFAULT_COMMISSIONS = {
|
|
|
4
4
|
platformShippingPercent: 10,
|
|
5
5
|
platformShippingFixedMin: 50,
|
|
6
6
|
};
|
|
7
|
-
export const CHECKOUT_PAYMENT_METHODS = ["cod", "online", "upi_manual", "admin_bypass"];
|
|
7
|
+
export const CHECKOUT_PAYMENT_METHODS = ["cash", "cod", "online", "upi_manual", "admin_bypass"];
|
|
@@ -4,5 +4,9 @@ export interface AdminOrderEditorViewProps {
|
|
|
4
4
|
orderId?: string;
|
|
5
5
|
orderLabel?: string;
|
|
6
6
|
currentStatus?: string;
|
|
7
|
+
paymentProofUrl?: string;
|
|
8
|
+
paymentTransactionId?: string;
|
|
9
|
+
paymentMethod?: string;
|
|
10
|
+
paymentStatus?: string;
|
|
7
11
|
}
|
|
8
|
-
export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }: AdminOrderEditorViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
-
import React from "react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
5
6
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Button, Form, FormActions, Input, Label, Select, SideDrawer, Stack, useToast } from "../../../ui";
|
|
7
|
+
import { Button, Div, Form, FormActions, Input, Label, Select, SideDrawer, Stack, Text, useToast } from "../../../ui";
|
|
8
|
+
import { MediaImage } from "../../media";
|
|
7
9
|
import { apiClient } from "../../../http";
|
|
8
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
9
12
|
const STATUS_OPTIONS = [
|
|
10
13
|
{ label: "Pending", value: "pending" },
|
|
11
14
|
{ label: "Processing", value: "processing" },
|
|
@@ -25,7 +28,7 @@ const CARRIER_OPTIONS = [
|
|
|
25
28
|
{ label: "Other", value: "Other" },
|
|
26
29
|
];
|
|
27
30
|
// --- Component ---------------------------------------------------------------
|
|
28
|
-
export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, }) {
|
|
31
|
+
export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, }) {
|
|
29
32
|
const queryClient = useQueryClient();
|
|
30
33
|
const { showToast } = useToast();
|
|
31
34
|
const [status, setStatus] = React.useState(currentStatus ?? "pending");
|
|
@@ -33,6 +36,9 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
33
36
|
const [carrier, setCarrier] = React.useState("");
|
|
34
37
|
const [notes, setNotes] = React.useState("");
|
|
35
38
|
const [refundAmount, setRefundAmount] = React.useState("");
|
|
39
|
+
const [isVerifyingPayment, setIsVerifyingPayment] = useState(false);
|
|
40
|
+
const isCashOrUpi = paymentMethod === "cash" || paymentMethod === "upi_manual";
|
|
41
|
+
const needsVerification = isCashOrUpi && paymentStatus === "pending";
|
|
36
42
|
React.useEffect(() => {
|
|
37
43
|
if (open) {
|
|
38
44
|
setStatus(currentStatus ?? "pending");
|
|
@@ -68,8 +74,28 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
68
74
|
showToast(err?.message ?? "Failed to update order.", "error");
|
|
69
75
|
},
|
|
70
76
|
});
|
|
77
|
+
const handleVerifyPayment = async () => {
|
|
78
|
+
if (!orderId)
|
|
79
|
+
return;
|
|
80
|
+
setIsVerifyingPayment(true);
|
|
81
|
+
try {
|
|
82
|
+
await apiClient.patch(`/api/admin/orders/${orderId}/payment-verify`, {});
|
|
83
|
+
showToast("Payment verified. Order moved to Processing.", "success");
|
|
84
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "orders"] });
|
|
85
|
+
onClose();
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
void normalizeError(err);
|
|
89
|
+
showToast(err?.message ?? "Failed to verify payment.", "error");
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
setIsVerifyingPayment(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
71
95
|
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsxs(Form, { onSubmit: (e) => {
|
|
72
96
|
e.preventDefault();
|
|
73
97
|
saveMutation.mutate();
|
|
74
|
-
}, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), _jsxs(
|
|
98
|
+
}, spacing: "md", padding: "md", children: [_jsx(Select, { label: "Order status", options: STATUS_OPTIONS, value: status, onValueChange: setStatus }), _jsx(Input, { label: "Tracking number (optional)", value: trackingNumber, onChange: (e) => setTrackingNumber(e.target.value), placeholder: "e.g. DEL1234567890IN" }), _jsx(Select, { label: "Carrier (optional)", options: CARRIER_OPTIONS, value: carrier, onValueChange: setCarrier }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Internal note (optional)" }), _jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 3, placeholder: "Reason for status change, escalation notes\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), (status === "refunded" || status === "return_requested") && (_jsx(Input, { label: "Refund amount \u20B9 (optional)", type: "number", min: "0", step: "0.01", value: refundAmount, onChange: (e) => setRefundAmount(e.target.value), placeholder: "e.g. 499.00" })), isCashOrUpi && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card", className: "rounded-lg border border-zinc-200 dark:border-zinc-700" }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "mt-1 w-full" })), !needsVerification && paymentStatus === "paid" && (
|
|
99
|
+
// audit-variant-ok: themed success border color not in BORDER_MAP
|
|
100
|
+
_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !orderId || saveMutation.isPending, children: "Save changes" })] })] }) }));
|
|
75
101
|
}
|
|
@@ -51,6 +51,10 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
51
51
|
].join(" · "),
|
|
52
52
|
status: toStringValue(item.status, "Unknown"),
|
|
53
53
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
54
|
+
paymentProofUrl: toStringValue(item.paymentProofUrl, "") || undefined,
|
|
55
|
+
paymentTransactionId: toStringValue(item.paymentTransactionId, "") || undefined,
|
|
56
|
+
paymentMethod: toStringValue(item.paymentMethod, "") || undefined,
|
|
57
|
+
paymentStatus: toStringValue(item.paymentStatus, "") || undefined,
|
|
54
58
|
})),
|
|
55
59
|
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
56
60
|
buildFilters: (f) => (f.status && f.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, f.status) : undefined),
|
|
@@ -117,5 +121,5 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
117
121
|
] })),
|
|
118
122
|
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
|
|
119
123
|
};
|
|
120
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status })] }));
|
|
124
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus })] }));
|
|
121
125
|
}
|
|
@@ -88,6 +88,8 @@ export interface AppLayoutShellProps {
|
|
|
88
88
|
logout?: string;
|
|
89
89
|
};
|
|
90
90
|
eventBannerSlot?: React.ReactNode;
|
|
91
|
+
/** When provided, renders a tour-start icon button in the title bar. Null in Patch 1. */
|
|
92
|
+
onTourStart?: () => void;
|
|
91
93
|
/**
|
|
92
94
|
* Override className for the main content wrapper div.
|
|
93
95
|
* When set, replaces the default `w-full px-4 py-6 …`
|
|
@@ -113,4 +115,4 @@ export interface AppLayoutShellProps {
|
|
|
113
115
|
};
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
|
-
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
|
|
118
|
+
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -104,7 +104,7 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
104
104
|
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: navItemClass, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: navItemClass, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: navItemClass, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: navItemClass, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && (_jsxs(Div, { 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(Div, { 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" })] })), user.stats.auctionsWon != null && (_jsxs(Div, { 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" })] })), user.stats.itemsSold != null && (_jsxs(Div, { className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.itemsSold }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Items Sold" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: navItemClass, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: navItemClass, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t dark:border-slate-800", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-primary-50 hover:text-primary-800 dark:text-zinc-300 dark:hover:bg-slate-800 dark:hover:text-secondary-300", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), theme === "dark" ? "Light mode" : "Dark mode"] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
|
|
105
105
|
}
|
|
106
106
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
107
|
-
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
107
|
+
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, onTourStart, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
108
108
|
// QueryClientProvider used to be created here so AppLayoutShell could stand
|
|
109
109
|
// alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
|
|
110
110
|
// turned out to be a peer-dep duplicate of @tanstack/query-core under
|
|
@@ -164,5 +164,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
164
164
|
opacity: darkBackground.overlay?.opacity ?? 0,
|
|
165
165
|
},
|
|
166
166
|
};
|
|
167
|
-
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "w-full px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
167
|
+
return (_jsx(_Fragment, { children: _jsxs(Stack, { className: "min-h-screen w-full overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onTourStart: onTourStart, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { layout: "flex", className: "relative w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { padding: "y-lg", className: `flex-1 ${contentClassName ?? "mx-auto w-full max-w-screen-xl px-5 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
|
|
168
168
|
}
|
|
@@ -52,6 +52,8 @@ export interface TitleBarLayoutProps {
|
|
|
52
52
|
onToggleDashboardNav?: () => void;
|
|
53
53
|
/** Hide the public sidebar toggle button when nested layouts own navigation. */
|
|
54
54
|
hideSidebarToggle?: boolean;
|
|
55
|
+
/** When provided, renders a tour-start icon button before the theme toggle. Null in Patch 1. */
|
|
56
|
+
onTourStart?: () => void;
|
|
55
57
|
id?: string;
|
|
56
58
|
className?: string;
|
|
57
59
|
}
|
|
@@ -64,4 +66,4 @@ export interface TitleBarLayoutProps {
|
|
|
64
66
|
*
|
|
65
67
|
* Receives all domain data as props — zero domain imports.
|
|
66
68
|
*/
|
|
67
|
-
export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
69
|
+
export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
|