@mohasinac/appkit 3.1.6 → 3.2.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/_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/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/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/seed/orders-seed-data.js +55 -1
- 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/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
|
+
}
|
|
@@ -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 ?? "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, {})] }) }));
|
|
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;
|
|
@@ -14,10 +14,11 @@ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-cente
|
|
|
14
14
|
*
|
|
15
15
|
* Receives all domain data as props — zero domain imports.
|
|
16
16
|
*/
|
|
17
|
-
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
17
|
+
export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, onTourStart, hasDashboardNav, onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
|
|
18
18
|
// ── Element builders ────────────────────────────────────────────────────────
|
|
19
19
|
const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-1 px-3 py-1 rounded-full text-xs font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-200 dark:hover:bg-secondary-900/60 transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
|
|
20
20
|
const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
|
|
21
|
+
const tourBtn = onTourStart ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Start product tour", onClick: onTourStart, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }) })) : null;
|
|
21
22
|
const hamburgerBtn = !hideSidebarToggle ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: sidebarOpen ? (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })) : (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" })) }) })) : null;
|
|
22
23
|
// Compare is always lg+ (desktop-only feature, less critical on mobile)
|
|
23
24
|
const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }) })) : null;
|
|
@@ -34,5 +35,5 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
34
35
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
35
36
|
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-xs text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), siteLogoUrl ? (_jsx(Row, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, size: "md" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
36
37
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
37
|
-
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
|
|
38
|
+
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, tourBtn, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
|
|
38
39
|
}
|
|
@@ -37,6 +37,7 @@ export declare const PaymentStatusValues: {
|
|
|
37
37
|
/** Runtime-accessible payment method values â€" use instead of bare string literals. */
|
|
38
38
|
export declare const PaymentMethodValues: {
|
|
39
39
|
readonly COD: "cod";
|
|
40
|
+
readonly CASH: "cash";
|
|
40
41
|
readonly ONLINE: "online";
|
|
41
42
|
readonly UPI_MANUAL: "upi_manual";
|
|
42
43
|
readonly RAZORPAY: "razorpay";
|
|
@@ -209,6 +210,14 @@ export interface OrderDocument extends BaseDocument {
|
|
|
209
210
|
pickedAt?: Date;
|
|
210
211
|
/** Timestamp when the order was packed and ready for courier handoff. */
|
|
211
212
|
packedAt?: Date;
|
|
213
|
+
/** Media slug for buyer-uploaded UPI/bank screenshot (via /api/media proxy). */
|
|
214
|
+
paymentProofUrl?: string;
|
|
215
|
+
/** UTR / transaction reference entered by the buyer. */
|
|
216
|
+
paymentTransactionId?: string;
|
|
217
|
+
/** MIME type detected at finalize step (image/jpeg, image/png, application/pdf). */
|
|
218
|
+
paymentProofMimeType?: string;
|
|
219
|
+
/** When the buyer submitted the proof. */
|
|
220
|
+
paymentProofUploadedAt?: Date;
|
|
212
221
|
}
|
|
213
222
|
export declare const ORDER_COLLECTION: "orders";
|
|
214
223
|
export declare const ORDER_INDEXED_FIELDS: readonly ["userId", "productId", "storeId", "status", "paymentStatus", "payoutStatus", "shippingMethod", "orderDate", "createdAt", "assignedWorkerId", "pickedAt", "packedAt"];
|
|
@@ -31,6 +31,7 @@ export const PaymentStatusValues = {
|
|
|
31
31
|
/** Runtime-accessible payment method values â€" use instead of bare string literals. */
|
|
32
32
|
export const PaymentMethodValues = {
|
|
33
33
|
COD: "cod",
|
|
34
|
+
CASH: "cash",
|
|
34
35
|
ONLINE: "online",
|
|
35
36
|
UPI_MANUAL: "upi_manual",
|
|
36
37
|
RAZORPAY: "razorpay",
|
|
@@ -11,6 +11,7 @@ import { ImageUpload, MediaUploadField, MediaUploadList, } from "../../media";
|
|
|
11
11
|
import { useMediaUpload } from "../../media";
|
|
12
12
|
import { resolveDate } from "../../../utils/date.formatter";
|
|
13
13
|
import { normalizeRichTextHtml } from "../../../utils/string.formatter";
|
|
14
|
+
import { useListingTypeFlags } from "../../../react/hooks/useListingTypeFlags";
|
|
14
15
|
import { isAuctionListing, isPreOrderListing, isPrizeDrawListing, } from "../utils/listing-type";
|
|
15
16
|
import { PrizeDrawItemsEditor } from "./PrizeDrawItemsEditor";
|
|
16
17
|
export const PRODUCT_STATUS_OPTIONS = [
|
|
@@ -30,6 +31,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
30
31
|
const t = useTranslations("adminProducts");
|
|
31
32
|
const { upload } = useMediaUpload();
|
|
32
33
|
const galleryIndexRef = useRef(0);
|
|
34
|
+
const listingTypeFlags = useListingTypeFlags();
|
|
33
35
|
const update = (partial) => {
|
|
34
36
|
onChange({ ...product, ...partial });
|
|
35
37
|
};
|
|
@@ -133,7 +135,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
133
135
|
] })] }), _jsx(Checkbox, { label: t("formInsurance"), checked: !!product.insurance, onChange: (e) => update({
|
|
134
136
|
insurance: e.target.checked,
|
|
135
137
|
insuranceCost: e.target.checked ? product.insuranceCost || 0 : undefined,
|
|
136
|
-
}), disabled: isReadonly }), product.insurance && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formInsuranceHelp"), children: t("formInsuranceHelp") }), _jsx(FormField, { name: "insuranceCost", label: t("formInsuranceCost"), type: "number", value: String(product.insuranceCost ?? ""), onChange: (value) => update({ insuranceCost: Number(value) }), disabled: isReadonly, placeholder: `${currencyPrefix}0` })] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionAuctionSettings") }), _jsx(Checkbox, { label: t("formIsAuction"), checked: isAuctionListing(product), onChange: (e) => update({ listingType: e.target.checked ? "auction" : "standard" }), disabled: isReadonly }), isAuctionListing(product) && (_jsxs(_Fragment, { children: [_jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: t("formStartingBid"), type: "number", value: String(product.startingBid ?? ""), onChange: (value) => update({ startingBid: Number(value) }), disabled: isReadonly, placeholder: "0" }), _jsx(FormField, { name: "auctionEndDate", label: t("formAuctionEndDate"), type: "datetime-local", value: (() => {
|
|
138
|
+
}), disabled: isReadonly }), product.insurance && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formInsuranceHelp"), children: t("formInsuranceHelp") }), _jsx(FormField, { name: "insuranceCost", label: t("formInsuranceCost"), type: "number", value: String(product.insuranceCost ?? ""), onChange: (value) => update({ insuranceCost: Number(value) }), disabled: isReadonly, placeholder: `${currencyPrefix}0` })] })), listingTypeFlags.auction && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionAuctionSettings") }), _jsx(Checkbox, { label: t("formIsAuction"), checked: isAuctionListing(product), onChange: (e) => update({ listingType: e.target.checked ? "auction" : "standard" }), disabled: isReadonly })] })), isAuctionListing(product) && (_jsxs(_Fragment, { children: [_jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "startingBid", label: t("formStartingBid"), type: "number", value: String(product.startingBid ?? ""), onChange: (value) => update({ startingBid: Number(value) }), disabled: isReadonly, placeholder: "0" }), _jsx(FormField, { name: "auctionEndDate", label: t("formAuctionEndDate"), type: "datetime-local", value: (() => {
|
|
137
139
|
const d = resolveDate(product.auctionEndDate);
|
|
138
140
|
if (!d)
|
|
139
141
|
return "";
|
|
@@ -143,8 +145,8 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
143
145
|
}), disabled: isReadonly, options: [
|
|
144
146
|
{ value: "winner", label: t("formAuctionShippingPaidByWinner") },
|
|
145
147
|
{ value: "seller", label: t("formAuctionShippingPaidBySeller") },
|
|
146
|
-
] }), _jsx(Text, { variant: "secondary", weight: "semibold", className: "mt-2", children: t("sectionAuctionAdvanced") }), _jsx(Checkbox, { label: t("formAutoExtendable"), checked: !!product.autoExtendable, onChange: (e) => update({ autoExtendable: e.target.checked }), disabled: isReadonly }), product.autoExtendable && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAutoExtendableHelp"), children: t("formAutoExtendableHelp") }), _jsx(FormField, { name: "auctionExtensionMinutes", label: t("formAuctionExtensionMinutes"), type: "number", value: String(product.auctionExtensionMinutes ?? 5), onChange: (value) => update({ auctionExtensionMinutes: Number(value) || 5 }), disabled: isReadonly, placeholder: "5", helpText: t("formAuctionExtensionMinutesHelp") })] }))] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionPreOrderSettings") }), _jsx(Checkbox, { label: t("formIsPreOrder"), checked: isPreOrderListing(product), onChange: (e) => update({ listingType: e.target.checked ? "pre-order" : "standard" }), disabled: isReadonly }), _jsx(Heading, { level: 4, className: "mt-4", children: "Prize Draw Settings" }), _jsx(Checkbox, { label: "This is a prize-draw listing", checked: isPrizeDrawListing(product), onChange: (e) => update({ listingType: e.target.checked ? "prize-draw" : "standard" }), disabled: isReadonly ||
|
|
147
|
-
|
|
148
|
+
] }), _jsx(Text, { variant: "secondary", weight: "semibold", className: "mt-2", children: t("sectionAuctionAdvanced") }), _jsx(Checkbox, { label: t("formAutoExtendable"), checked: !!product.autoExtendable, onChange: (e) => update({ autoExtendable: e.target.checked }), disabled: isReadonly }), product.autoExtendable && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAutoExtendableHelp"), children: t("formAutoExtendableHelp") }), _jsx(FormField, { name: "auctionExtensionMinutes", label: t("formAuctionExtensionMinutes"), type: "number", value: String(product.auctionExtensionMinutes ?? 5), onChange: (value) => update({ auctionExtensionMinutes: Number(value) || 5 }), disabled: isReadonly, placeholder: "5", helpText: t("formAuctionExtensionMinutesHelp") })] }))] })), listingTypeFlags["pre-order"] && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionPreOrderSettings") }), _jsx(Checkbox, { label: t("formIsPreOrder"), checked: isPreOrderListing(product), onChange: (e) => update({ listingType: e.target.checked ? "pre-order" : "standard" }), disabled: isReadonly })] })), listingTypeFlags["prize-draw"] && (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: "Prize Draw Settings" }), _jsx(Checkbox, { label: "This is a prize-draw listing", checked: isPrizeDrawListing(product), onChange: (e) => update({ listingType: e.target.checked ? "prize-draw" : "standard" }), disabled: isReadonly ||
|
|
149
|
+
(product.prizeDrawItems ?? []).some((it) => it.isWon) })] })), isPrizeDrawListing(product) && ((() => {
|
|
148
150
|
const prizeItems = (product.prizeDrawItems ?? []);
|
|
149
151
|
const anyWon = prizeItems.some((it) => it.isWon);
|
|
150
152
|
const lockedForReveal = anyWon;
|
|
@@ -127,6 +127,7 @@ export declare const DEFAULT_ROUTE_MAP: {
|
|
|
127
127
|
readonly ORDER_CANCEL: (id: string) => string;
|
|
128
128
|
readonly ORDER_TRACK: (id: string) => string;
|
|
129
129
|
readonly ORDER_INVOICE: (id: string) => string;
|
|
130
|
+
readonly ORDER_PAYMENT: (id: string) => string;
|
|
130
131
|
readonly NOTIFICATIONS: "/user/notifications";
|
|
131
132
|
readonly MESSAGES: "/user/messages";
|
|
132
133
|
/** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
|
|
@@ -463,6 +464,7 @@ export declare const ROUTES: {
|
|
|
463
464
|
readonly ORDER_CANCEL: (id: string) => string;
|
|
464
465
|
readonly ORDER_TRACK: (id: string) => string;
|
|
465
466
|
readonly ORDER_INVOICE: (id: string) => string;
|
|
467
|
+
readonly ORDER_PAYMENT: (id: string) => string;
|
|
466
468
|
readonly NOTIFICATIONS: "/user/notifications";
|
|
467
469
|
readonly MESSAGES: "/user/messages";
|
|
468
470
|
/** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
|
|
@@ -115,6 +115,7 @@ export const DEFAULT_ROUTE_MAP = {
|
|
|
115
115
|
ORDER_CANCEL: (id) => `/user/orders/${id}/cancel`,
|
|
116
116
|
ORDER_TRACK: (id) => `/user/orders/${id}/track`,
|
|
117
117
|
ORDER_INVOICE: (id) => `/user/orders/${id}/invoice`,
|
|
118
|
+
ORDER_PAYMENT: (id) => `/user/orders/${id}/payment`,
|
|
118
119
|
NOTIFICATIONS: "/user/notifications",
|
|
119
120
|
MESSAGES: "/user/messages",
|
|
120
121
|
/** Plan §10 — claimed-coupons wallet (Active / Expired / Used tabs). */
|
|
@@ -291,4 +291,58 @@ for (let i = _rawOrdersSeedData.length; i < 50; i++) {
|
|
|
291
291
|
updatedAt: daysAgo(Math.max(0, daysBack - 10)),
|
|
292
292
|
});
|
|
293
293
|
}
|
|
294
|
-
|
|
294
|
+
// P-1 MVP: two cash/UPI manual payment orders for demo purposes.
|
|
295
|
+
// cashOrderPendingProof: buyer submitted proof, admin has NOT yet verified.
|
|
296
|
+
const cashOrderPendingProof = {
|
|
297
|
+
id: "order-1-20260729-cash01",
|
|
298
|
+
productId: "product-dark-magician-lob-1st",
|
|
299
|
+
productTitle: "Dark Magician — LOB 1st Edition",
|
|
300
|
+
userId: "user-yugi-muto",
|
|
301
|
+
userName: "Yugi Muto",
|
|
302
|
+
userEmail: "yugi@duelkingdom.in",
|
|
303
|
+
storeId: "store-kaiba-corp-cards",
|
|
304
|
+
quantity: 1,
|
|
305
|
+
unitPrice: 499900,
|
|
306
|
+
totalPrice: 499900,
|
|
307
|
+
currency: "INR",
|
|
308
|
+
status: "pending",
|
|
309
|
+
paymentStatus: "pending",
|
|
310
|
+
paymentMethod: "cash",
|
|
311
|
+
paymentProofUrl: "/media/payment-proof-demo-pending.jpg",
|
|
312
|
+
paymentTransactionId: "UPI-DEMO-20260728-PROOF",
|
|
313
|
+
paymentProofMimeType: "image/jpeg",
|
|
314
|
+
paymentProofUploadedAt: daysAgo(1),
|
|
315
|
+
shippingAddress: "addr-yugi-home",
|
|
316
|
+
orderDate: daysAgo(2),
|
|
317
|
+
createdAt: daysAgo(2),
|
|
318
|
+
updatedAt: daysAgo(1),
|
|
319
|
+
};
|
|
320
|
+
const cashOrderVerified = {
|
|
321
|
+
id: "order-1-20260729-cash02",
|
|
322
|
+
productId: "product-hot-wheels-redline-vintage",
|
|
323
|
+
productTitle: "Hot Wheels Redline — Vintage",
|
|
324
|
+
userId: "user-yugi-muto",
|
|
325
|
+
userName: "Yugi Muto",
|
|
326
|
+
userEmail: "yugi@duelkingdom.in",
|
|
327
|
+
storeId: "store-diecast-depot",
|
|
328
|
+
quantity: 1,
|
|
329
|
+
unitPrice: 129900,
|
|
330
|
+
totalPrice: 129900,
|
|
331
|
+
currency: "INR",
|
|
332
|
+
status: "processing",
|
|
333
|
+
paymentStatus: "paid",
|
|
334
|
+
paymentMethod: "cash",
|
|
335
|
+
paymentProofUrl: "/media/payment-proof-demo-verified.jpg",
|
|
336
|
+
paymentTransactionId: "UPI-DEMO-20260727-YUGI",
|
|
337
|
+
paymentProofMimeType: "image/jpeg",
|
|
338
|
+
paymentProofUploadedAt: daysAgo(3),
|
|
339
|
+
shippingAddress: "addr-yugi-home",
|
|
340
|
+
orderDate: daysAgo(4),
|
|
341
|
+
createdAt: daysAgo(4),
|
|
342
|
+
updatedAt: daysAgo(2),
|
|
343
|
+
};
|
|
344
|
+
export const ordersSeedData = [
|
|
345
|
+
cashOrderPendingProof,
|
|
346
|
+
cashOrderVerified,
|
|
347
|
+
...[..._rawOrdersSeedData, ...expandedOrders].slice(0, 48),
|
|
348
|
+
];
|
|
@@ -374,16 +374,26 @@ export const siteSettingsSeedData = {
|
|
|
374
374
|
smsVerification: true,
|
|
375
375
|
translations: true,
|
|
376
376
|
wishlists: true,
|
|
377
|
-
auctions:
|
|
377
|
+
auctions: false,
|
|
378
378
|
reviews: true,
|
|
379
|
-
events:
|
|
380
|
-
blog:
|
|
381
|
-
coupons:
|
|
379
|
+
events: false,
|
|
380
|
+
blog: false,
|
|
381
|
+
coupons: false,
|
|
382
382
|
notifications: true,
|
|
383
383
|
sellerRegistration: true,
|
|
384
|
-
preOrders:
|
|
384
|
+
preOrders: false,
|
|
385
385
|
seedPanel: true,
|
|
386
386
|
adminCheckoutBypass: false,
|
|
387
|
+
// P-1 MVP: only standard listing type active; others unlocked in later patches.
|
|
388
|
+
listingTypes: {
|
|
389
|
+
standard: true,
|
|
390
|
+
auction: false,
|
|
391
|
+
"pre-order": false,
|
|
392
|
+
"prize-draw": false,
|
|
393
|
+
classified: false,
|
|
394
|
+
"digital-code": false,
|
|
395
|
+
live: false,
|
|
396
|
+
},
|
|
387
397
|
},
|
|
388
398
|
credentials: {
|
|
389
399
|
razorpayKeyId: "rzp_test_PLACEHOLDER",
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { SERVER_ERRORS_COLLECTION, SERVER_ERROR_FIELDS, SERVER_ERROR_STACK_MAX_B
|
|
|
6
6
|
export type { ServerErrorDocument, ServerErrorSource, } from "./features/server-errors/schemas/firestore";
|
|
7
7
|
export { wrapAction, isOk, unwrap, } from "./_internal/shared/types/action-result";
|
|
8
8
|
export type { ActionResult } from "./_internal/shared/types/action-result";
|
|
9
|
+
export { attachPaymentProofAction, adminVerifyPaymentAction, } from "./_internal/server/features/orders/actions";
|
|
9
10
|
export { wrapJobHandler, wrapScheduleHandler, wrapTriggerHandler, wrapCallableHandler, } from "./_internal/server/jobs/core/wrapJobHandler";
|
|
10
11
|
export { analyzeLogs } from "./_internal/server/features/maintenance/analyze";
|
|
11
12
|
export type { AnalyzeOptions, AnalyzeReport, } from "./_internal/server/features/maintenance/analyze";
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,8 @@ export { SERVER_ERRORS_COLLECTION, SERVER_ERROR_FIELDS, SERVER_ERROR_STACK_MAX_B
|
|
|
11
11
|
// [SERVER-ONLY] ActionResult envelope + wrapAction helper for "use server" functions.
|
|
12
12
|
// Mirrors the routeHandler envelope so client wrappers consume both surfaces uniformly.
|
|
13
13
|
export { wrapAction, isOk, unwrap, } from "./_internal/shared/types/action-result";
|
|
14
|
+
// [SERVER-ONLY] P-1 manual payment proof server actions.
|
|
15
|
+
export { attachPaymentProofAction, adminVerifyPaymentAction, } from "./_internal/server/features/orders/actions";
|
|
14
16
|
// [SERVER-ONLY] Cloud Function handler wrappers — persist exceptions to
|
|
15
17
|
// serverErrors (source: "function") before re-throwing so retry semantics
|
|
16
18
|
// are preserved. Apply at the consumer's runtime adapter boundary.
|
|
@@ -242,6 +242,12 @@ export type MediaFilenameContext = ({
|
|
|
242
242
|
refundId: string;
|
|
243
243
|
ext?: string;
|
|
244
244
|
date?: Date;
|
|
245
|
+
} | {
|
|
246
|
+
type: "payment-proof";
|
|
247
|
+
orderId: string;
|
|
248
|
+
buyerName: string;
|
|
249
|
+
ext?: string;
|
|
250
|
+
date?: Date;
|
|
245
251
|
};
|
|
246
252
|
export declare function generateMediaFilename(ctx: MediaFilenameContext): string;
|
|
247
253
|
export declare function validateMediaFilename(filename: string): boolean;
|
|
@@ -384,6 +384,15 @@ export function generateMediaFilename(ctx) {
|
|
|
384
384
|
const ext = ctx.ext ?? "pdf";
|
|
385
385
|
return `refund-proof-${ctx.orderId}-${ctx.refundId}-${y}${m}${day}.${ext}`;
|
|
386
386
|
}
|
|
387
|
+
case "payment-proof": {
|
|
388
|
+
const d = ctx.date ?? new Date();
|
|
389
|
+
const y = d.getFullYear();
|
|
390
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
391
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
392
|
+
const ext = ctx.ext ?? "jpg";
|
|
393
|
+
const name = ctx.buyerName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").slice(0, 20);
|
|
394
|
+
return `payment-proof-${ctx.orderId}-${name}-${y}${m}${day}.${ext}`;
|
|
395
|
+
}
|
|
387
396
|
}
|
|
388
397
|
}
|
|
389
398
|
/**
|