@mohasinac/appkit 3.3.3 → 3.3.4
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/lottery/LotteryAdminSlotView.js +1 -1
- package/dist/_internal/server/features/cart/actions.js +1 -1
- package/dist/_internal/server/features/lottery/data.d.ts +2 -3
- package/dist/_internal/server/features/products/actions.js +1 -1
- package/dist/features/admin/components/AdminSectionsView.js +7 -4
- package/dist/features/admin/constants/filter-tabs.js +11 -0
- package/dist/features/auctions/columns/index.js +1 -1
- package/dist/features/auctions/schemas/index.d.ts +2 -2
- package/dist/features/auth/hooks/useRBAC.js +6 -1
- package/dist/features/blog/columns/index.js +1 -1
- package/dist/features/categories/schemas/firestore.d.ts +1 -1
- package/dist/features/contact/email.js +0 -1
- package/dist/features/forms/Slider.js +4 -1
- package/dist/features/forms/Toggle.js +4 -1
- package/dist/features/homepage/components/NewsletterBanner.js +1 -1
- package/dist/features/layout/BottomActions.js +27 -0
- package/dist/features/layout/BottomActionsContext.d.ts +36 -0
- package/dist/features/layout/BottomActionsContext.js +36 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/payments/schemas/index.d.ts +4 -4
- package/dist/features/products/repository/products.repository.js +1 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/promotions/repository/claimed-coupons.repository.js +1 -1
- package/dist/features/reviews/columns/index.js +1 -1
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/features/seller/actions/seller-actions.d.ts +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +4 -1
- package/dist/features/seller/components/SellerProductsView.js +35 -31
- package/dist/features/seller/components/SellerReviewsView.js +1 -1
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/stores/columns/index.js +1 -1
- package/dist/features/stores/components/StoresIndexListing.js +20 -15
- package/dist/features/wishlist/actions/wishlist-actions.js +1 -1
- package/dist/next/components/NotFoundView.js +17 -0
- package/dist/next/components/UnauthorizedView.js +13 -0
- package/dist/schemas/registry.d.ts +93 -94
- package/dist/schemas/webhooks/razorpay.d.ts +86 -86
- package/dist/ui/columns/column-renderers.js +1 -1
- package/dist/ui/components/Accordion.js +10 -6
- package/dist/ui/components/ListingLayout.js +6 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Stack, Text, Heading, Badge, Section, Container, DataTable
|
|
3
|
+
import { Stack, Text, Heading, Badge, Section, Container, DataTable } from "../../../../ui";
|
|
4
4
|
/**
|
|
5
5
|
* Admin-only view showing slots with price + weight + % chance.
|
|
6
6
|
* Separate from LotterySlotGrid (public) which never shows price/weight.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { wrapAction } from "@mohasinac/appkit/server";
|
|
3
3
|
import { cartRepository } from "../../../../repositories";
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
|
-
import { addToCartSchema, removeFromCartSchema, mergeGuestCartSchema
|
|
5
|
+
import { addToCartSchema, removeFromCartSchema, mergeGuestCartSchema } from "../../../shared/features/cart/schema";
|
|
6
6
|
import { upsertCartItem, mergeGuestItems } from "./service";
|
|
7
7
|
import { ValidationError } from "../../../shared/errors/index";
|
|
8
8
|
export async function addToCartAction(input) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SieveModel } from "../../../../providers/db-firebase";
|
|
2
2
|
import type { EventDocument } from "../../../../features/events/schemas/firestore";
|
|
3
|
-
import type { LotteryEntryDocument } from "../../../../features/lottery/schemas/firestore";
|
|
4
3
|
import type { ClientLotteryConfig } from "../../../../features/lottery/types";
|
|
5
4
|
/** Event shape with lottery config stripped of price/weight. */
|
|
6
5
|
export type LotteryEventClient = Omit<EventDocument, "lotteryConfig"> & {
|
|
@@ -15,6 +14,6 @@ export declare const listLotteryEvents: (opts?: {
|
|
|
15
14
|
pageSize?: number;
|
|
16
15
|
}) => Promise<LotteryEventClient[]>;
|
|
17
16
|
/** Fetch lottery entries for admin view (includes phone + txId via PII decrypt). */
|
|
18
|
-
export declare function getLotteryEntriesForAdmin(sourceType: "event" | "product", sourceId: string, model: SieveModel): Promise<import("../../../..").FirebaseSieveResult<LotteryEntryDocument>>;
|
|
17
|
+
export declare function getLotteryEntriesForAdmin(sourceType: "event" | "product", sourceId: string, model: SieveModel): Promise<import("../../../..").FirebaseSieveResult<import("../../../..").LotteryEntryDocument>>;
|
|
19
18
|
/** Fetch lottery entries for a specific user (own entries only). */
|
|
20
|
-
export declare function getLotteryEntriesForUser(userId: string, model: SieveModel): Promise<import("../../../..").FirebaseSieveResult<LotteryEntryDocument>>;
|
|
19
|
+
export declare function getLotteryEntriesForUser(userId: string, model: SieveModel): Promise<import("../../../..").FirebaseSieveResult<import("../../../..").LotteryEntryDocument>>;
|
|
@@ -3,7 +3,7 @@ import { wrapAction } from "@mohasinac/appkit/server";
|
|
|
3
3
|
import { productRepository } from "../../../../repositories";
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
5
|
import { isAdminUser } from "../../../../features/auth/role-predicates";
|
|
6
|
-
import { productInputSchema, productUpdateSchema, auctionInputSchema, preOrderInputSchema, setFeaturedSchema, setStatusSchema
|
|
6
|
+
import { productInputSchema, productUpdateSchema, auctionInputSchema, preOrderInputSchema, setFeaturedSchema, setStatusSchema } from "../../../shared/features/products/schema";
|
|
7
7
|
import { assertProductOwnership, assertStatusTransition } from "./service";
|
|
8
8
|
import { ValidationError } from "../../../shared/errors/index";
|
|
9
9
|
export async function createProductAction(input) {
|
|
@@ -12,7 +12,7 @@ import { ADMIN_ENDPOINTS, DEMO_ENDPOINTS } from "../../../constants";
|
|
|
12
12
|
import { useAdminSectionsListing } from "../hooks/useAdminSectionsListing";
|
|
13
13
|
import { DataTable } from "./DataTable";
|
|
14
14
|
import { SECTION_TYPE_OPTIONS, SUPPORTED_TYPED_BUILDERS, RESOURCE_SORT_OPTIONS, FAQ_CATEGORY_OPTIONS, DEFAULT_PRODUCTS_BUILDER, DEFAULT_AUCTIONS_BUILDER, DEFAULT_STATS_BUILDER, DEFAULT_PRE_ORDERS_BUILDER, DEFAULT_STORES_BUILDER, DEFAULT_EVENTS_BUILDER, DEFAULT_SOCIAL_FEED_BUILDER, DEFAULT_WELCOME_BUILDER, DEFAULT_TRUST_INDICATORS_BUILDER, DEFAULT_CATEGORIES_BUILDER, DEFAULT_BRANDS_BUILDER, DEFAULT_BANNER_BUILDER, DEFAULT_FEATURES_BUILDER, DEFAULT_REVIEWS_BUILDER, DEFAULT_WHATSAPP_BUILDER, DEFAULT_FAQ_BUILDER, DEFAULT_BLOG_BUILDER, DEFAULT_NEWSLETTER_BUILDER, DEFAULT_CAROUSEL_BUILDER, DEFAULT_CUSTOM_CARDS_BUILDER, DEFAULT_GOOGLE_REVIEWS_BUILDER, DEFAULT_FEATURED_BUNDLES_BUILDER, DEFAULT_PRIZE_DRAWS_BUILDER, DEFAULT_EVENT_RAFFLES_BUILDER, DEFAULT_COLLECTION_CARDS_BUILDER, } from "./sections/adminSectionsTypes";
|
|
15
|
-
import { toStringValue, buildProductsConfig, buildAuctionsConfig, buildStatsConfig, buildPreOrdersConfig, buildStoresConfig, buildEventsConfig, buildSocialFeedConfig, buildWelcomeConfig, buildTrustIndicatorsConfig, buildCategoriesConfig, buildBrandsConfig, buildBannerConfig, buildFeaturesConfig, buildReviewsConfig, buildWhatsAppConfig, buildFAQConfig, buildBlogConfig, buildNewsletterConfig, buildCarouselConfig, buildCustomCardsConfig, buildGoogleReviewsConfig, buildFeaturedBundlesConfig, buildPrizeDrawsConfig, buildEventRafflesConfig, buildCollectionCardsConfig, parseProductsBuilder, parseAuctionsBuilder, parseStatsBuilder, parsePreOrdersBuilder, parseStoresBuilder, parseEventsBuilder, parseSocialFeedBuilder, parseWelcomeBuilder, parseTrustIndicatorsBuilder, parseCategoriesBuilder, parseBrandsBuilder, parseBannerBuilder, parseFeaturesBuilder, parseReviewsBuilder, parseWhatsAppBuilder, parseFAQBuilder, parseBlogBuilder, parseNewsletterBuilder, parseCarouselBuilder, parseCustomCardsBuilder, parseGoogleReviewsBuilder, parseFeaturedBundlesBuilder, parsePrizeDrawsBuilder, parseEventRafflesBuilder, parseCollectionCardsBuilder
|
|
15
|
+
import { toStringValue, buildProductsConfig, buildAuctionsConfig, buildStatsConfig, buildPreOrdersConfig, buildStoresConfig, buildEventsConfig, buildSocialFeedConfig, buildWelcomeConfig, buildTrustIndicatorsConfig, buildCategoriesConfig, buildBrandsConfig, buildBannerConfig, buildFeaturesConfig, buildReviewsConfig, buildWhatsAppConfig, buildFAQConfig, buildBlogConfig, buildNewsletterConfig, buildCarouselConfig, buildCustomCardsConfig, buildGoogleReviewsConfig, buildFeaturedBundlesConfig, buildPrizeDrawsConfig, buildEventRafflesConfig, buildCollectionCardsConfig, parseProductsBuilder, parseAuctionsBuilder, parseStatsBuilder, parsePreOrdersBuilder, parseStoresBuilder, parseEventsBuilder, parseSocialFeedBuilder, parseWelcomeBuilder, parseTrustIndicatorsBuilder, parseCategoriesBuilder, parseBrandsBuilder, parseBannerBuilder, parseFeaturesBuilder, parseReviewsBuilder, parseWhatsAppBuilder, parseFAQBuilder, parseBlogBuilder, parseNewsletterBuilder, parseCarouselBuilder, parseCustomCardsBuilder, parseGoogleReviewsBuilder, parseFeaturedBundlesBuilder, parsePrizeDrawsBuilder, parseEventRafflesBuilder, parseCollectionCardsBuilder } from "./sections/adminSectionsBuildParse";
|
|
16
16
|
const __P = {
|
|
17
17
|
p3: "p-3",
|
|
18
18
|
p4: "p-4",
|
|
@@ -944,6 +944,12 @@ export function AdminSectionsView({ children }) {
|
|
|
944
944
|
default: return null;
|
|
945
945
|
}
|
|
946
946
|
}
|
|
947
|
+
// React.useMemo must run unconditionally on every render — it was
|
|
948
|
+
// previously declared after the `hasChildren` early return below, which
|
|
949
|
+
// skips the hook call in passthrough mode, violating the Rules of Hooks.
|
|
950
|
+
const sectionOrderMap = React.useMemo(() => {
|
|
951
|
+
return new Map(sections.map((section) => [section.id, section.order]));
|
|
952
|
+
}, [sections]);
|
|
947
953
|
// If children exist, render passthrough mode (detail view)
|
|
948
954
|
if (hasChildren) {
|
|
949
955
|
return _jsx(_Fragment, { children: children });
|
|
@@ -956,9 +962,6 @@ export function AdminSectionsView({ children }) {
|
|
|
956
962
|
status: section.enabled ? "Active" : "Inactive",
|
|
957
963
|
updatedAt: new Date(section.updatedAt).toLocaleDateString(),
|
|
958
964
|
}));
|
|
959
|
-
const sectionOrderMap = React.useMemo(() => {
|
|
960
|
-
return new Map(sections.map((section) => [section.id, section.order]));
|
|
961
|
-
}, [sections]);
|
|
962
965
|
const hasReorderChanges = reorderDraft.some((item) => sectionOrderMap.get(item.id) !== item.order);
|
|
963
966
|
const canUndoReorderChanges = reorderUndoStack.length > 0;
|
|
964
967
|
return (_jsxs(_Fragment, { children: [_jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [_jsxs(Row, { className: "mb-4", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsx(Text, { size: "base", weight: "semibold", color: "primary", children: "Homepage Sections" }), _jsx(Text, { size: "sm", color: "muted", children: "Manage homepage sections and their display order" })] }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setSeedResetOpen(true), children: "Reset seed data" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => setIsModalOpen(true), children: "Manage Sections" })] })] }), errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No sections found" })] }), _jsxs(Stack, { className: `mt-4 ${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsxs(Row, { align: "center", justify: "between", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Reorder Sections" }), _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: reindexDraft, disabled: reorderSections.isPending || reorderDraft.length === 0, children: "Reindex 1..N" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: undoReorderChange, disabled: reorderSections.isPending || !canUndoReorderChanges, children: "Undo unsaved" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: resetToServerOrder, disabled: reorderSections.isPending || !hasReorderChanges, children: "Reset to server" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: () => reorderSections.mutate(), disabled: !hasReorderChanges || reorderSections.isPending, children: reorderSections.isPending ? "Saving order..." : "Save order" })] })] }), reorderDraft.length === 0 ? (_jsx(Text, { size: "sm", color: "muted", children: "No sections to reorder." })) : (_jsx(Stack, { gap: "sm", children: reorderDraft.map((item, index) => (_jsxs(Div, { layout: "grid", gap: "2", align: "center", className: "grid-cols-[auto_1fr_auto_auto_auto]", rounded: "md", padding: "xs", border: "default", draggable: true, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => handleReorderDrop(index), onDragEnd: () => setDragIndex(null), children: [_jsx(Text, { size: "sm", weight: "semibold", color: "muted", children: "\u2261" }), _jsxs(Text, { size: "sm", color: "primary", children: [item.type, " #", item.order] }), _jsx(Input, { type: "number", min: 1, max: reorderDraft.length, value: String(item.order), onChange: (event) => updateReorderItemOrder(index, Number(event.target.value) || 1), className: "w-24" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, -1), disabled: index === 0, children: "Up" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => moveReorderItem(index, 1), disabled: index === reorderDraft.length - 1, children: "Down" })] }, `reorder-${item.id}`))) }))] }), _jsx(ConfirmDeleteModal, { isOpen: seedResetOpen, onClose: () => setSeedResetOpen(false), onConfirm: () => resetSeed.mutate(), title: "Reset homepage sections seed data?", message: "This will reload the 19 default homepage sections from seed data. Any manual changes made in Firestore will be overwritten.", confirmText: "Reset seed", cancelText: "Cancel", isDeleting: resetSeed.isPending, variant: "danger" }), _jsx(Modal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), title: "Manage Homepage Section", size: "lg", children: _jsxs(Form, { onSubmit: (event) => {
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin filter-chip tab sets (SB10-C completion, S8 2026-05-13).
|
|
3
|
+
*
|
|
4
|
+
* Match current view-filter values exactly so migrating an inline
|
|
5
|
+
* `STATUS_OPTIONS` array to one of these constants is a behaviour-preserving
|
|
6
|
+
* rename. The `ALL_TAB` sentinel collapses to an empty filter string when
|
|
7
|
+
* clicked.
|
|
8
|
+
*
|
|
9
|
+
* Each entry is a typed `{ id, label }` row so views render with one
|
|
10
|
+
* primitive and stay in sync when statuses are added.
|
|
11
|
+
*/
|
|
1
12
|
export const ALL_TAB = { id: "All", label: "All" };
|
|
2
13
|
/** Admin > Products — listing status filter chip set. */
|
|
3
14
|
export const ADMIN_PRODUCT_STATUS_TABS = [
|
|
@@ -58,8 +58,8 @@ export declare const auctionItemSchema: z.ZodObject<{
|
|
|
58
58
|
createdAt: z.ZodString;
|
|
59
59
|
updatedAt: z.ZodString;
|
|
60
60
|
}, "strip", z.ZodTypeAny, {
|
|
61
|
-
status: "draft" | "published" | "archived" | "sold";
|
|
62
61
|
currency: string;
|
|
62
|
+
status: "draft" | "published" | "archived" | "sold";
|
|
63
63
|
featured: boolean;
|
|
64
64
|
listingType: "auction";
|
|
65
65
|
createdAt: string;
|
|
@@ -87,8 +87,8 @@ export declare const auctionItemSchema: z.ZodObject<{
|
|
|
87
87
|
}[] | undefined;
|
|
88
88
|
storeSlug?: string | undefined;
|
|
89
89
|
}, {
|
|
90
|
-
status: "draft" | "published" | "archived" | "sold";
|
|
91
90
|
currency: string;
|
|
91
|
+
status: "draft" | "published" | "archived" | "sold";
|
|
92
92
|
featured: boolean;
|
|
93
93
|
listingType: "auction";
|
|
94
94
|
createdAt: string;
|
|
@@ -51,9 +51,14 @@ export function useRoleChecks() {
|
|
|
51
51
|
}
|
|
52
52
|
export function useIsOwner(resourceOwnerId) {
|
|
53
53
|
const { user } = useCurrentUser();
|
|
54
|
+
// useHasRole must run unconditionally on every render — it was previously
|
|
55
|
+
// called after the early return below, which skips the hook call whenever
|
|
56
|
+
// !user || !resourceOwnerId, violating the Rules of Hooks (call-count
|
|
57
|
+
// mismatch risk when that condition flips between renders).
|
|
58
|
+
const isAdmin = useHasRole("admin");
|
|
54
59
|
if (!user || !resourceOwnerId)
|
|
55
60
|
return false;
|
|
56
|
-
if (
|
|
61
|
+
if (isAdmin)
|
|
57
62
|
return true;
|
|
58
63
|
return user.id === resourceOwnerId;
|
|
59
64
|
}
|
|
@@ -194,7 +194,7 @@ export declare const categoryQueryHelpers: {
|
|
|
194
194
|
/** @deprecated Use byCategoryType("brand"). */
|
|
195
195
|
readonly brands: () => readonly ["isBrand", "==", true];
|
|
196
196
|
/** SB-UNI B + C + D — discriminator-based listing. */
|
|
197
|
-
readonly byCategoryType: (type: import("../types").CategoryType) => readonly ["categoryType", "==", import("
|
|
197
|
+
readonly byCategoryType: (type: import("../types").CategoryType) => readonly ["categoryType", "==", import("..").CategoryType];
|
|
198
198
|
readonly sublistings: () => readonly ["categoryType", "==", "sublisting"];
|
|
199
199
|
readonly brandPages: () => readonly ["categoryType", "==", "brand"];
|
|
200
200
|
readonly active: () => readonly ["isActive", "==", true];
|
|
@@ -8,7 +8,6 @@ import "server-only";
|
|
|
8
8
|
// → providers.config.ts → @mohasinac/appkit/server → ... → email.tsx).
|
|
9
9
|
// `require()` is opaque to Next's static analyser so the chain stops here.
|
|
10
10
|
function renderToStaticMarkup(el) {
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
11
|
const m = require("react-dom/server");
|
|
13
12
|
return m.renderToStaticMarkup(el);
|
|
14
13
|
}
|
|
@@ -9,7 +9,10 @@ import { cn, LABEL_BASE, ERROR_BASE } from "./utils";
|
|
|
9
9
|
export function Slider({ value: controlledValue, defaultValue = 0, min = 0, max = 100, step = 1, onChange, label, error, disabled = false, showValue = true, formatValue, className = "", id, }) {
|
|
10
10
|
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
11
11
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
12
|
-
|
|
12
|
+
// React.useId() must run unconditionally on every render — using `id ?? React.useId()`
|
|
13
|
+
// skips the hook call whenever `id` is provided, violating the Rules of Hooks.
|
|
14
|
+
const generatedId = React.useId();
|
|
15
|
+
const sliderId = id ?? generatedId;
|
|
13
16
|
const pct = ((value - min) / (max - min)) * 100;
|
|
14
17
|
const fillRef = useRef(null);
|
|
15
18
|
useEffect(() => {
|
|
@@ -38,7 +38,10 @@ export function Toggle({ checked: controlledChecked, defaultChecked = false, onC
|
|
|
38
38
|
setInternalChecked(newChecked);
|
|
39
39
|
onChange?.(newChecked);
|
|
40
40
|
};
|
|
41
|
-
|
|
41
|
+
// React.useId() must run unconditionally on every render — using `id ?? React.useId()`
|
|
42
|
+
// skips the hook call whenever `id` is provided, violating the Rules of Hooks.
|
|
43
|
+
const generatedId = React.useId();
|
|
44
|
+
const toggleId = id ?? generatedId;
|
|
42
45
|
const cfg = SIZE_CONFIG[size];
|
|
43
46
|
return (_jsxs(Row, { className: cn("gap-3", className), children: [_jsx(Button, { type: "button", role: "switch", id: toggleId, "aria-checked": checked, disabled: disabled, onClick: handleChange, className: cn("relative inline-flex items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:focus:ring-secondary-400/30 focus:ring-offset-2 dark:focus:ring-offset-slate-900", cfg.track, checked
|
|
44
47
|
? "bg-primary-600 dark:bg-secondary-500"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
-
import { Button, Div, Heading, Span, Text
|
|
5
|
+
import { Button, Div, Heading, Span, Text } from "../../../ui";
|
|
6
6
|
import { DynamicBgDiv } from "../../../ui/components/DynamicBgDiv";
|
|
7
7
|
import { Form } from "../../../ui/components/Form";
|
|
8
8
|
import { FieldInput } from "../../../ui/forms/FieldInput";
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* BottomActions Component
|
|
5
|
+
*
|
|
6
|
+
* A fixed-bottom mobile action bar rendered **above** BottomNavbar (bottom-14).
|
|
7
|
+
* Reads from `BottomActionsContext` — features register their actions via the
|
|
8
|
+
* `useBottomActions` hook; this component just renders whatever is registered.
|
|
9
|
+
*
|
|
10
|
+
* Two modes:
|
|
11
|
+
* - **Page mode** — shows registered page-level actions inline (Add to Cart,
|
|
12
|
+
* Buy Now, Place Bid, Proceed to Checkout, etc.) with an optional info label.
|
|
13
|
+
* - **Bulk mode** — activates when sieveFilter("bulk.selectedCount", SIEVE_OP.GT, "0"); shows:
|
|
14
|
+
* • Selection count pill on the left (tap to deselect all)
|
|
15
|
+
* • An upward-opening type-picker dropdown (middle, flex-1) — tap to
|
|
16
|
+
* choose WHICH action to run; the chosen label is always visible.
|
|
17
|
+
* • An "Apply" submit button on the right — executes the selected action,
|
|
18
|
+
* styled with the selected action's variant (danger = red, etc.).
|
|
19
|
+
*
|
|
20
|
+
* Layout rules:
|
|
21
|
+
* - Hidden on lg+ screens (`lg:hidden`) — desktop shows inline action panels.
|
|
22
|
+
* - The bar slides up with a 300 ms ease-out transition; `pointer-events-none`
|
|
23
|
+
* while off-screen.
|
|
24
|
+
*
|
|
25
|
+
* @component
|
|
26
|
+
* @example
|
|
27
|
+
* // Automatically rendered by LayoutClient — no manual usage required.
|
|
28
|
+
* // Features use `useBottomActions` to register their actions.
|
|
29
|
+
*/
|
|
3
30
|
import { useState, useRef, useEffect } from "react";
|
|
4
31
|
import { X, ChevronUp, ChevronDown, Check } from "lucide-react";
|
|
5
32
|
import { useBottomActionsContext } from "./BottomActionsContext";
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BottomActionsContext
|
|
3
|
+
*
|
|
4
|
+
* Provides a context for registering page-level mobile action bars.
|
|
5
|
+
* Features call `useBottomActions` to push actions; the `<BottomActions>`
|
|
6
|
+
* layout component reads this context and renders the bar above BottomNavbar.
|
|
7
|
+
*
|
|
8
|
+
* Supports two modes:
|
|
9
|
+
* - Page mode: primary page actions (Add to Cart, Buy Now, Place Bid, etc.)
|
|
10
|
+
* - Bulk mode: activated when sieveFilter("bulk.selectedCount", SIEVE_OP.GT, "0") — shows selection
|
|
11
|
+
* count + custom bulk action buttons (Delete, Archive, Export, etc.)
|
|
12
|
+
*
|
|
13
|
+
* @example — product detail page
|
|
14
|
+
* ```tsx
|
|
15
|
+
* useBottomActions({
|
|
16
|
+
* actions: [
|
|
17
|
+
* { id: "wishlist", icon: <Heart className="w-4 h-4" />, label: t("wishlist"), variant: "ghost", grow: false, onClick: handleWishlist },
|
|
18
|
+
* { id: "cart", label: t("addToCart"), variant: "outline", onClick: handleAddToCart },
|
|
19
|
+
* { id: "buy", label: t("buyNow"), variant: "primary", onClick: handleBuyNow },
|
|
20
|
+
* ],
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example — admin listing with bulk select
|
|
25
|
+
* ```tsx
|
|
26
|
+
* useBottomActions({
|
|
27
|
+
* bulk: {
|
|
28
|
+
* selectedCount: selectedIds.length,
|
|
29
|
+
* onClearSelection: () => setSelectedIds([]),
|
|
30
|
+
* actions: [
|
|
31
|
+
* { id: "delete", label: t("bulkDelete", { count: selectedIds.length }), variant: "danger", onClick: handleBulkDelete },
|
|
32
|
+
* ],
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
1
37
|
import React from "react";
|
|
2
38
|
import type { ButtonProps } from "../../ui";
|
|
3
39
|
export interface BottomAction {
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* BottomActionsContext
|
|
5
|
+
*
|
|
6
|
+
* Provides a context for registering page-level mobile action bars.
|
|
7
|
+
* Features call `useBottomActions` to push actions; the `<BottomActions>`
|
|
8
|
+
* layout component reads this context and renders the bar above BottomNavbar.
|
|
9
|
+
*
|
|
10
|
+
* Supports two modes:
|
|
11
|
+
* - Page mode: primary page actions (Add to Cart, Buy Now, Place Bid, etc.)
|
|
12
|
+
* - Bulk mode: activated when sieveFilter("bulk.selectedCount", SIEVE_OP.GT, "0") — shows selection
|
|
13
|
+
* count + custom bulk action buttons (Delete, Archive, Export, etc.)
|
|
14
|
+
*
|
|
15
|
+
* @example — product detail page
|
|
16
|
+
* ```tsx
|
|
17
|
+
* useBottomActions({
|
|
18
|
+
* actions: [
|
|
19
|
+
* { id: "wishlist", icon: <Heart className="w-4 h-4" />, label: t("wishlist"), variant: "ghost", grow: false, onClick: handleWishlist },
|
|
20
|
+
* { id: "cart", label: t("addToCart"), variant: "outline", onClick: handleAddToCart },
|
|
21
|
+
* { id: "buy", label: t("buyNow"), variant: "primary", onClick: handleBuyNow },
|
|
22
|
+
* ],
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example — admin listing with bulk select
|
|
27
|
+
* ```tsx
|
|
28
|
+
* useBottomActions({
|
|
29
|
+
* bulk: {
|
|
30
|
+
* selectedCount: selectedIds.length,
|
|
31
|
+
* onClearSelection: () => setSelectedIds([]),
|
|
32
|
+
* actions: [
|
|
33
|
+
* { id: "delete", label: t("bulkDelete", { count: selectedIds.length }), variant: "danger", onClick: handleBulkDelete },
|
|
34
|
+
* ],
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
3
39
|
import { createContext, useCallback, useContext, useMemo, useRef, useState, } from "react";
|
|
4
40
|
const EMPTY = { actions: [] };
|
|
5
41
|
const BottomActionsContext = createContext(null);
|
|
@@ -521,8 +521,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
521
521
|
bin: string;
|
|
522
522
|
}>>;
|
|
523
523
|
}, "strip", z.ZodTypeAny, {
|
|
524
|
-
status: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
|
|
525
524
|
currency: string;
|
|
525
|
+
status: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
|
|
526
526
|
createdAt: string | Date | z.objectOutputType<{
|
|
527
527
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
528
528
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -666,8 +666,8 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
666
666
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
667
667
|
shippingProofUploadedBy?: string | undefined;
|
|
668
668
|
}, {
|
|
669
|
-
status: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
|
|
670
669
|
currency: string;
|
|
670
|
+
status: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
|
|
671
671
|
createdAt: string | Date | z.objectInputType<{
|
|
672
672
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
673
673
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -150,8 +150,8 @@ export declare const payoutFirestoreSchema: z.ZodObject<{
|
|
|
150
150
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
151
151
|
}, z.ZodTypeAny, "passthrough">>]>>;
|
|
152
152
|
}, "strip", z.ZodTypeAny, {
|
|
153
|
-
status: "pending" | "paid" | "failed" | "processing";
|
|
154
153
|
currency: string;
|
|
154
|
+
status: "pending" | "paid" | "failed" | "processing";
|
|
155
155
|
storeId: string;
|
|
156
156
|
createdAt: string | Date | z.objectOutputType<{
|
|
157
157
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
@@ -200,8 +200,8 @@ export declare const payoutFirestoreSchema: z.ZodObject<{
|
|
|
200
200
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
201
201
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
202
202
|
}, {
|
|
203
|
-
status: "pending" | "paid" | "failed" | "processing";
|
|
204
203
|
currency: string;
|
|
204
|
+
status: "pending" | "paid" | "failed" | "processing";
|
|
205
205
|
storeId: string;
|
|
206
206
|
createdAt: string | Date | z.objectInputType<{
|
|
207
207
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
@@ -289,8 +289,8 @@ export declare const paymentRecordSchema: z.ZodObject<{
|
|
|
289
289
|
status: z.ZodDefault<z.ZodEnum<["pending", "authorized", "captured", "failed", "refunded", "partially_refunded"]>>;
|
|
290
290
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
291
291
|
}, "strip", z.ZodTypeAny, {
|
|
292
|
-
status: "pending" | "failed" | "captured" | "authorized" | "refunded" | "partially_refunded";
|
|
293
292
|
currency: string;
|
|
293
|
+
status: "pending" | "failed" | "captured" | "authorized" | "refunded" | "partially_refunded";
|
|
294
294
|
id: string;
|
|
295
295
|
amount: number;
|
|
296
296
|
orderId: string;
|
|
@@ -302,8 +302,8 @@ export declare const paymentRecordSchema: z.ZodObject<{
|
|
|
302
302
|
amount: number;
|
|
303
303
|
orderId: string;
|
|
304
304
|
gateway: "bank_transfer" | "upi" | "razorpay" | "stripe" | "paypal" | "cod" | "whatsapp";
|
|
305
|
-
status?: "pending" | "failed" | "captured" | "authorized" | "refunded" | "partially_refunded" | undefined;
|
|
306
305
|
currency?: string | undefined;
|
|
306
|
+
status?: "pending" | "failed" | "captured" | "authorized" | "refunded" | "partially_refunded" | undefined;
|
|
307
307
|
createdAt?: string | undefined;
|
|
308
308
|
gatewayPaymentId?: string | undefined;
|
|
309
309
|
}>;
|
|
@@ -4,7 +4,7 @@ import { DatabaseError } from "../../../errors";
|
|
|
4
4
|
import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
5
5
|
import { cacheManager } from "../../../core";
|
|
6
6
|
import { generateUniqueId, slugify, buildSearchTokens, tokenizeQuery, generateBarcodeId } from "../../../utils";
|
|
7
|
-
import { PRODUCT_COLLECTION, ProductStatusValues
|
|
7
|
+
import { PRODUCT_COLLECTION, ProductStatusValues } from "../schemas";
|
|
8
8
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
9
9
|
/**
|
|
10
10
|
* Canonical listing-kind tokens used in `listingType`. Note: the public Sieve
|
|
@@ -22,7 +22,7 @@ export declare const productFeatureAdminCreateSchema: z.ZodObject<{
|
|
|
22
22
|
isActive: z.ZodBoolean;
|
|
23
23
|
displayOrder: z.ZodNumber;
|
|
24
24
|
}, "strip", z.ZodTypeAny, {
|
|
25
|
-
category: "
|
|
25
|
+
category: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform";
|
|
26
26
|
label: string;
|
|
27
27
|
scope: "store" | "platform";
|
|
28
28
|
icon: string;
|
|
@@ -33,7 +33,7 @@ export declare const productFeatureAdminCreateSchema: z.ZodObject<{
|
|
|
33
33
|
description?: string | undefined;
|
|
34
34
|
iconColor?: string | undefined;
|
|
35
35
|
}, {
|
|
36
|
-
category: "
|
|
36
|
+
category: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform";
|
|
37
37
|
label: string;
|
|
38
38
|
scope: "store" | "platform";
|
|
39
39
|
icon: string;
|
|
@@ -55,7 +55,7 @@ export declare const productFeatureStoreCreateSchema: z.ZodObject<{
|
|
|
55
55
|
isActive: z.ZodBoolean;
|
|
56
56
|
displayOrder: z.ZodNumber;
|
|
57
57
|
}, "strip", z.ZodTypeAny, {
|
|
58
|
-
category: "
|
|
58
|
+
category: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform";
|
|
59
59
|
label: string;
|
|
60
60
|
icon: string;
|
|
61
61
|
isActive: boolean;
|
|
@@ -64,7 +64,7 @@ export declare const productFeatureStoreCreateSchema: z.ZodObject<{
|
|
|
64
64
|
description?: string | undefined;
|
|
65
65
|
iconColor?: string | undefined;
|
|
66
66
|
}, {
|
|
67
|
-
category: "
|
|
67
|
+
category: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform";
|
|
68
68
|
label: string;
|
|
69
69
|
icon: string;
|
|
70
70
|
isActive: boolean;
|
|
@@ -84,7 +84,7 @@ export declare const productFeatureUpdateSchema: z.ZodObject<{
|
|
|
84
84
|
isActive: z.ZodOptional<z.ZodBoolean>;
|
|
85
85
|
displayOrder: z.ZodOptional<z.ZodNumber>;
|
|
86
86
|
}, "strip", z.ZodTypeAny, {
|
|
87
|
-
category?: "
|
|
87
|
+
category?: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform" | undefined;
|
|
88
88
|
description?: string | undefined;
|
|
89
89
|
label?: string | undefined;
|
|
90
90
|
icon?: string | undefined;
|
|
@@ -93,7 +93,7 @@ export declare const productFeatureUpdateSchema: z.ZodObject<{
|
|
|
93
93
|
iconColor?: string | undefined;
|
|
94
94
|
productTypes?: ("auction" | "prize-draw" | "classified" | "digital-code" | "live" | "all" | "product" | "preorder")[] | undefined;
|
|
95
95
|
}, {
|
|
96
|
-
category?: "
|
|
96
|
+
category?: "seller" | "custom" | "auction" | "condition" | "shipping" | "preorder" | "platform" | undefined;
|
|
97
97
|
description?: string | undefined;
|
|
98
98
|
label?: string | undefined;
|
|
99
99
|
icon?: string | undefined;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
9
9
|
import { normalizeError } from "../../../errors/normalize";
|
|
10
|
-
import { CLAIMED_COUPONS_COLLECTION, createClaimedCouponId
|
|
10
|
+
import { CLAIMED_COUPONS_COLLECTION, createClaimedCouponId } from "../schemas";
|
|
11
11
|
import { DatabaseError } from "../../../errors";
|
|
12
12
|
import { COUPON_USAGE_FIELDS } from "../../../constants/field-names";
|
|
13
13
|
export class ClaimedCouponsRepository extends BaseRepository {
|
|
@@ -24,8 +24,8 @@ export declare const searchProductItemSchema: z.ZodObject<{
|
|
|
24
24
|
title: string;
|
|
25
25
|
slug: string;
|
|
26
26
|
id: string;
|
|
27
|
-
status?: string | undefined;
|
|
28
27
|
currency?: string | undefined;
|
|
28
|
+
status?: string | undefined;
|
|
29
29
|
featured?: boolean | undefined;
|
|
30
30
|
listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
|
|
31
31
|
isPromoted?: boolean | undefined;
|
|
@@ -35,8 +35,8 @@ export declare const searchProductItemSchema: z.ZodObject<{
|
|
|
35
35
|
title: string;
|
|
36
36
|
slug: string;
|
|
37
37
|
id: string;
|
|
38
|
-
status?: string | undefined;
|
|
39
38
|
currency?: string | undefined;
|
|
39
|
+
status?: string | undefined;
|
|
40
40
|
featured?: boolean | undefined;
|
|
41
41
|
listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
|
|
42
42
|
isPromoted?: boolean | undefined;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (src/actions/seller.actions.ts) alongside the other seller server actions.
|
|
10
10
|
*/
|
|
11
11
|
import type { JsonValue } from "../../../schemas/types";
|
|
12
|
-
import type { SellerPayoutDetails
|
|
12
|
+
import type { SellerPayoutDetails } from "../../auth/schemas/firestore";
|
|
13
13
|
import type { StoreDocument } from "../../stores/schemas/firestore";
|
|
14
14
|
import type { ProductDocument } from "../../products/schemas/firestore";
|
|
15
15
|
import type { OrderDocument } from "../../orders/schemas/firestore";
|
|
@@ -115,7 +115,7 @@ export declare function requestPayout(userId: string, userName: string, userEmai
|
|
|
115
115
|
export declare function bulkSellerOrder(userId: string, userRole: string, userDisplayName: string, userEmail: string, orderIds: string[]): Promise<BulkSellerOrderResult>;
|
|
116
116
|
export declare function createSellerProduct(userId: string, userName: string, userEmail: string, input: Record<string, JsonValue>): Promise<void>;
|
|
117
117
|
export declare function getSellerStore(userId: string): Promise<StoreDocument | null>;
|
|
118
|
-
export declare function getSellerShipping(userId: string): Promise<SellerShippingConfig | null>;
|
|
118
|
+
export declare function getSellerShipping(userId: string): Promise<import("../../auth").SellerShippingConfig | null>;
|
|
119
119
|
export declare function getSellerPayoutSettings(userId: string): Promise<SellerPayoutDetails | {
|
|
120
120
|
method: string;
|
|
121
121
|
isConfigured: boolean;
|
|
@@ -358,9 +358,12 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
|
|
|
358
358
|
buildBulkAction(ACTIONS.STORE["set-location"], () => setSetLocationOpen(true), { icon: _jsx(MapPin, { className: "w-4 h-4" }) }),
|
|
359
359
|
buildBulkAction(ACTIONS.STORE["request-payout"], () => void requestPayoutForSelection(), { variant: "primary" }),
|
|
360
360
|
];
|
|
361
|
+
// useBottomActions must run unconditionally on every render — it was
|
|
362
|
+
// previously called after the `hasChildren` early return below, which
|
|
363
|
+
// skips the hook call in passthrough mode, violating the Rules of Hooks.
|
|
364
|
+
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
361
365
|
if (hasChildren) {
|
|
362
366
|
return _jsx(ListingLayout, { portal: "seller", ...props, children: children });
|
|
363
367
|
}
|
|
364
|
-
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
365
368
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by order ID or buyer name", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), selection.selectedIds.length > 0 && (_jsx(Div, { border: "default", paddingX: "x-sm-md", className: "sticky top-[calc(var(--header-height,0px)+88px)] z-20 backdrop-blur-sm border-b", surface: "default", padding: "y-xs", children: _jsx(BulkActionBar, { selectedCount: selection.selectedIds.length, onClearSelection: selection.clearSelection, actions: bulkActions }) })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: rows, columns: columns, isLoading: isLoading, emptyLabel: "No orders yet", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: () => selection.toggleAll(), renderRowActions: renderRowActions })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }) }), selectedOrderId && (_jsx(OrderDetailDrawer, { orderId: selectedOrderId, apiBase: orderDetailApiBase, onClose: () => setSelectedOrderId(null) })), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selection.selectedIds.length, onSave: handleSetLocation, onClose: () => setSetLocationOpen(false) }))] }));
|
|
366
369
|
}
|
|
@@ -214,6 +214,41 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, children, .
|
|
|
214
214
|
.filter((r) => !deletedIds.has(r.id))
|
|
215
215
|
.map((r) => statusOverrides.has(r.id) ? { ...r, status: statusOverrides.get(r.id) } : r);
|
|
216
216
|
const selection = useBulkSelection({ items: visibleRows, keyExtractor: (r) => r.id });
|
|
217
|
+
// handleBulkPrintLabels/handleSetLocation (useCallback) and useBottomActions
|
|
218
|
+
// must run unconditionally on every render — they were previously declared
|
|
219
|
+
// after the `hasChildren` early return below, which skips these hook calls
|
|
220
|
+
// in passthrough mode, violating the Rules of Hooks.
|
|
221
|
+
const handleBulkPrintLabels = useCallback(() => {
|
|
222
|
+
const ids = selection.selectedIds.join(",");
|
|
223
|
+
void dispatch({
|
|
224
|
+
type: "NAVIGATE",
|
|
225
|
+
href: `${String(ROUTES.STORE.INVENTORY_PRINT)}?type=product&ids=${ids}&autoprint=1`,
|
|
226
|
+
});
|
|
227
|
+
}, [selection.selectedIds, dispatch]);
|
|
228
|
+
const handleSetLocation = useCallback(async (loc) => {
|
|
229
|
+
try {
|
|
230
|
+
const res = await fetch(SELLER_ENDPOINTS.PRODUCTS_BULK_LOCATION, {
|
|
231
|
+
method: "PATCH",
|
|
232
|
+
headers: { "Content-Type": "application/json" },
|
|
233
|
+
body: JSON.stringify({ productIds: selection.selectedIds, physicalLocation: loc }),
|
|
234
|
+
});
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
const body = await res.json().catch(() => null);
|
|
237
|
+
throw new Error(body?.error ?? "Failed to update location");
|
|
238
|
+
}
|
|
239
|
+
showToast("Location updated.", "success");
|
|
240
|
+
setSetLocationOpen(false);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
void normalizeError(err);
|
|
244
|
+
showToast(err instanceof Error ? err.message : "Failed to update location.", "error");
|
|
245
|
+
}
|
|
246
|
+
}, [selection.selectedIds, showToast]);
|
|
247
|
+
const bulkActions = [
|
|
248
|
+
buildBulkAction(ACTIONS.STORE["print-labels"], handleBulkPrintLabels, { icon: _jsx(Printer, { className: "w-4 h-4" }) }),
|
|
249
|
+
buildBulkAction(ACTIONS.STORE["set-location"], () => setSetLocationOpen(true), { icon: _jsx(MapPin, { className: "w-4 h-4" }) }),
|
|
250
|
+
];
|
|
251
|
+
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
217
252
|
if (hasChildren) {
|
|
218
253
|
return (_jsx(ListingLayout, { portal: "seller", ...props, children: children }));
|
|
219
254
|
}
|
|
@@ -262,37 +297,6 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, children, .
|
|
|
262
297
|
setPublishingId(null);
|
|
263
298
|
}
|
|
264
299
|
};
|
|
265
|
-
const handleBulkPrintLabels = useCallback(() => {
|
|
266
|
-
const ids = selection.selectedIds.join(",");
|
|
267
|
-
void dispatch({
|
|
268
|
-
type: "NAVIGATE",
|
|
269
|
-
href: `${String(ROUTES.STORE.INVENTORY_PRINT)}?type=product&ids=${ids}&autoprint=1`,
|
|
270
|
-
});
|
|
271
|
-
}, [selection.selectedIds, dispatch]);
|
|
272
|
-
const handleSetLocation = useCallback(async (loc) => {
|
|
273
|
-
try {
|
|
274
|
-
const res = await fetch(SELLER_ENDPOINTS.PRODUCTS_BULK_LOCATION, {
|
|
275
|
-
method: "PATCH",
|
|
276
|
-
headers: { "Content-Type": "application/json" },
|
|
277
|
-
body: JSON.stringify({ productIds: selection.selectedIds, physicalLocation: loc }),
|
|
278
|
-
});
|
|
279
|
-
if (!res.ok) {
|
|
280
|
-
const body = await res.json().catch(() => null);
|
|
281
|
-
throw new Error(body?.error ?? "Failed to update location");
|
|
282
|
-
}
|
|
283
|
-
showToast("Location updated.", "success");
|
|
284
|
-
setSetLocationOpen(false);
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
void normalizeError(err);
|
|
288
|
-
showToast(err instanceof Error ? err.message : "Failed to update location.", "error");
|
|
289
|
-
}
|
|
290
|
-
}, [selection.selectedIds, showToast]);
|
|
291
|
-
const bulkActions = [
|
|
292
|
-
buildBulkAction(ACTIONS.STORE["print-labels"], handleBulkPrintLabels, { icon: _jsx(Printer, { className: "w-4 h-4" }) }),
|
|
293
|
-
buildBulkAction(ACTIONS.STORE["set-location"], () => setSetLocationOpen(true), { icon: _jsx(MapPin, { className: "w-4 h-4" }) }),
|
|
294
|
-
];
|
|
295
|
-
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
296
300
|
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products by name\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, toggles: [
|
|
297
301
|
{ label: "Show sold", active: showSold, onChange: (next) => table.set("showSold", next ? "true" : "") },
|
|
298
302
|
], extra: onCreateClick ? (_jsx(Button, { variant: "primary", size: "sm", onClick: onCreateClick, children: "+ New Listing" })) : null }), _jsx(TypeDropdown, { active: listingKind, onChange: handleKindChange }), totalPages > 1 && (_jsx(Row, { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 bg-[var(--appkit-color-surface)]/95 backdrop-blur-sm border-b border-[var(--appkit-color-border)] py-[0.375rem]", padding: "x-sm", justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), selection.selectedIds.length > 0 && (_jsx(Div, { paddingX: "x-sm-lg-md", className: "sticky top-[calc(var(--header-height,0px)+88px)] z-20 bg-[var(--appkit-color-surface)]/95 backdrop-blur-sm border-b border-[var(--appkit-color-border)]", padding: "y-xs", children: _jsx(BulkActionBar, { selectedCount: selection.selectedIds.length, onClearSelection: selection.clearSelection, actions: bulkActions }) })), _jsxs(Div, { paddingX: "x-sm-lg-md", padding: "y-md", children: [errorMessage && (_jsx(Alert, { variant: "error", className: "mb-4", children: errorMessage })), view !== "table" && (_jsx(SellerProductsCards, { view: view, rows: visibleRows, isLoading: isLoading, listingKind: listingKind, selectedIds: selection.selectedIdSet, toggle: selection.toggle, onEdit: handleEdit, onDuplicate: (row) => void handleDuplicate(row), onDelete: onDeleteProduct ? (row) => void handleDelete(row) : undefined })), view === "table" && (_jsx(DataTable, { columns: PRODUCT_COLUMNS, rows: visibleRows, isLoading: isLoading, emptyLabel: listingKind !== "all"
|