@mohasinac/appkit 2.7.49 → 2.7.50

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.
Files changed (110) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +15 -0
  3. package/dist/_internal/client/features/layout/index.d.ts +2 -0
  4. package/dist/_internal/client/features/layout/index.js +1 -0
  5. package/dist/_internal/server/features/checkout/actions.js +25 -2
  6. package/dist/_internal/server/features/promotions/actions.js +3 -1
  7. package/dist/_internal/shared/actions/action-registry.js +149 -0
  8. package/dist/client.d.ts +16 -0
  9. package/dist/client.js +11 -0
  10. package/dist/features/account/components/UserSidebar.js +2 -1
  11. package/dist/features/admin/components/AdminBlogEditorView.js +6 -2
  12. package/dist/features/admin/components/AdminSidebar.js +2 -1
  13. package/dist/features/auth/repository/session.repository.js +16 -1
  14. package/dist/features/cart/hooks/useAddToCart.d.ts +2 -0
  15. package/dist/features/cart/hooks/useAddToCart.js +2 -0
  16. package/dist/features/cart/hooks/useGuestCart.d.ts +2 -0
  17. package/dist/features/cart/utils/guest-cart.d.ts +7 -0
  18. package/dist/features/cart/utils/pending-ops.d.ts +6 -0
  19. package/dist/features/categories/components/CategoryBundlesListing.js +9 -1
  20. package/dist/features/categories/components/CategoryProductsListing.js +6 -10
  21. package/dist/features/categories/schemas/firestore.d.ts +9 -0
  22. package/dist/features/events/components/EventRaffleWinnerView.d.ts +9 -1
  23. package/dist/features/events/components/EventRaffleWinnerView.js +3 -3
  24. package/dist/features/events/components/SpinWheelView.js +3 -3
  25. package/dist/features/layout/BottomNavLayout.d.ts +5 -0
  26. package/dist/features/layout/BottomNavLayout.js +21 -0
  27. package/dist/features/layout/TitleBar.js +7 -1
  28. package/dist/features/layout/TitleBarLayout.d.ts +8 -2
  29. package/dist/features/layout/TitleBarLayout.js +11 -7
  30. package/dist/features/orders/schemas/index.d.ts +2 -2
  31. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +9 -5
  32. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  33. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +3 -1
  34. package/dist/features/products/components/PrizeDrawEntryActions.js +8 -12
  35. package/dist/features/products/components/ProductDetailActions.d.ts +5 -1
  36. package/dist/features/products/components/ProductDetailActions.js +3 -1
  37. package/dist/features/products/components/ProductDetailPageView.d.ts +2 -0
  38. package/dist/features/products/components/ProductDetailPageView.js +4 -0
  39. package/dist/features/products/components/ProductGrid.js +3 -3
  40. package/dist/features/products/components/ProductsIndexListing.js +15 -8
  41. package/dist/features/promotions/components/CouponCard.d.ts +11 -1
  42. package/dist/features/promotions/components/CouponCard.js +36 -2
  43. package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +27 -0
  44. package/dist/features/promotions/repository/claimed-coupons.repository.js +115 -0
  45. package/dist/features/promotions/schemas/firestore.d.ts +34 -0
  46. package/dist/features/promotions/schemas/firestore.js +13 -0
  47. package/dist/features/seller/components/SellerAddressesView.js +3 -2
  48. package/dist/features/seller/components/SellerBidsView.js +33 -3
  49. package/dist/features/seller/components/SellerOrdersView.js +35 -0
  50. package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
  51. package/dist/features/seller/components/SellerProductShell.js +26 -4
  52. package/dist/features/seller/components/SellerProductsCards.d.ts +19 -0
  53. package/dist/features/seller/components/SellerProductsCards.js +19 -0
  54. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +14 -0
  55. package/dist/features/seller/components/SellerProductsFilterDrawer.js +20 -0
  56. package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
  57. package/dist/features/seller/components/SellerProductsView.js +68 -19
  58. package/dist/features/seller/components/SellerReviewsView.js +67 -4
  59. package/dist/features/seller/components/SellerSidebar.js +2 -1
  60. package/dist/features/seller/components/seller-products-styles.d.ts +7 -0
  61. package/dist/features/seller/components/seller-products-styles.js +14 -0
  62. package/dist/features/shell/FormShell.d.ts +7 -1
  63. package/dist/features/shell/FormShell.js +5 -2
  64. package/dist/features/store-extensions/index.d.ts +4 -0
  65. package/dist/features/store-extensions/index.js +4 -0
  66. package/dist/features/store-extensions/repository/rbac.repositories.d.ts +23 -0
  67. package/dist/features/store-extensions/repository/rbac.repositories.js +32 -0
  68. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +91 -0
  69. package/dist/features/store-extensions/repository/store-extensions.repositories.js +127 -0
  70. package/dist/features/store-extensions/schemas/firestore.d.ts +244 -0
  71. package/dist/features/store-extensions/schemas/firestore.js +158 -0
  72. package/dist/features/store-extensions/schemas/rbac.d.ts +65 -0
  73. package/dist/features/store-extensions/schemas/rbac.js +43 -0
  74. package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
  75. package/dist/features/stores/components/StoreProductsListing.js +6 -10
  76. package/dist/features/stores/schemas/firestore.d.ts +2 -0
  77. package/dist/index.d.ts +6 -0
  78. package/dist/index.js +8 -0
  79. package/dist/next/routing/route-map.d.ts +79 -0
  80. package/dist/next/routing/route-map.js +34 -0
  81. package/dist/react/hooks/useFormStatePreservation.d.ts +17 -0
  82. package/dist/react/hooks/useFormStatePreservation.js +62 -0
  83. package/dist/react/hooks/useInlineRowEdit.d.ts +24 -0
  84. package/dist/react/hooks/useInlineRowEdit.js +68 -0
  85. package/dist/react/index.d.ts +4 -0
  86. package/dist/react/index.js +4 -0
  87. package/dist/repositories/index.d.ts +4 -0
  88. package/dist/repositories/index.js +5 -0
  89. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  90. package/dist/seed/claimed-coupons-seed-data.d.ts +13 -0
  91. package/dist/seed/claimed-coupons-seed-data.js +79 -0
  92. package/dist/seed/index.d.ts +2 -0
  93. package/dist/seed/index.js +3 -0
  94. package/dist/seed/manifest.js +17 -0
  95. package/dist/seed/store-extensions-seed-data.d.ts +19 -0
  96. package/dist/seed/store-extensions-seed-data.js +421 -0
  97. package/dist/tailwind-utilities.css +1 -1
  98. package/dist/ui/components/Button.style.css +20 -12
  99. package/dist/ui/components/ClaimCouponButton.d.ts +28 -0
  100. package/dist/ui/components/ClaimCouponButton.js +89 -0
  101. package/dist/ui/components/QuickCreateModal.d.ts +15 -0
  102. package/dist/ui/components/QuickCreateModal.js +48 -0
  103. package/dist/ui/components/QuickCreateModal.style.css +84 -0
  104. package/dist/ui/components/StickyBottomBar.d.ts +22 -0
  105. package/dist/ui/components/StickyBottomBar.js +20 -0
  106. package/dist/ui/components/VacationBanner.d.ts +11 -0
  107. package/dist/ui/components/VacationBanner.js +16 -0
  108. package/dist/ui/index.d.ts +8 -0
  109. package/dist/ui/index.js +4 -0
  110. package/package.json +1 -1
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Claimed Coupons Repository — plan §10
3
+ *
4
+ * One row per (userId, couponCode). Top-level collection so the user wallet
5
+ * page is a single indexed read. Status is lazily flipped to "expired" on
6
+ * read when `expiresAt < now`. Soft-delete only (status stays, no destroy).
7
+ */
8
+ import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
9
+ import { CLAIMED_COUPONS_COLLECTION, createClaimedCouponId, } from "../schemas";
10
+ import { DatabaseError } from "../../../errors";
11
+ export class ClaimedCouponsRepository extends BaseRepository {
12
+ constructor() {
13
+ super(CLAIMED_COUPONS_COLLECTION);
14
+ }
15
+ async findByUserAndCode(userId, couponCode) {
16
+ return this.findById(createClaimedCouponId(userId, couponCode));
17
+ }
18
+ /** Idempotent claim — returns the existing claim if one already exists. */
19
+ async claim(input) {
20
+ try {
21
+ const id = createClaimedCouponId(input.userId, input.couponCode);
22
+ const existing = await this.findById(id);
23
+ if (existing)
24
+ return existing;
25
+ const now = new Date();
26
+ const doc = {
27
+ id,
28
+ userId: input.userId,
29
+ couponId: input.couponId,
30
+ couponCode: input.couponCode.toUpperCase(),
31
+ source: input.source,
32
+ couponSnapshot: input.couponSnapshot,
33
+ status: "active",
34
+ expiresAt: input.expiresAt ?? null,
35
+ claimedAt: now,
36
+ updatedAt: now,
37
+ };
38
+ await this.db
39
+ .collection(this.collection)
40
+ .doc(id)
41
+ .set(prepareForFirestore(doc));
42
+ return doc;
43
+ }
44
+ catch (error) {
45
+ throw new DatabaseError("Failed to claim coupon", error);
46
+ }
47
+ }
48
+ async listForUser(userId) {
49
+ try {
50
+ const snap = await this.db
51
+ .collection(this.collection)
52
+ .where("userId", "==", userId)
53
+ .get();
54
+ const now = Date.now();
55
+ const rows = [];
56
+ for (const docSnap of snap.docs) {
57
+ const raw = this.mapDoc(docSnap);
58
+ // Lazy expire — flip status when validity has passed. Best-effort
59
+ // write-back; consumers see the corrected status immediately.
60
+ if (raw.status === "active" &&
61
+ raw.expiresAt &&
62
+ new Date(raw.expiresAt).getTime() < now) {
63
+ raw.status = "expired";
64
+ void this.db
65
+ .collection(this.collection)
66
+ .doc(raw.id)
67
+ .update({ status: "expired", updatedAt: new Date() })
68
+ .catch(() => {
69
+ /* best-effort */
70
+ });
71
+ }
72
+ rows.push(raw);
73
+ }
74
+ return rows;
75
+ }
76
+ catch (error) {
77
+ throw new DatabaseError("Failed to list claimed coupons", error);
78
+ }
79
+ }
80
+ async markUsed(userId, couponCode, orderId) {
81
+ const id = createClaimedCouponId(userId, couponCode);
82
+ try {
83
+ await this.db
84
+ .collection(this.collection)
85
+ .doc(id)
86
+ .update({
87
+ status: "used",
88
+ usedAt: new Date(),
89
+ usedOrderId: orderId,
90
+ updatedAt: new Date(),
91
+ });
92
+ }
93
+ catch (error) {
94
+ throw new DatabaseError("Failed to mark coupon used", error);
95
+ }
96
+ }
97
+ async softRemove(userId, couponCode) {
98
+ const id = createClaimedCouponId(userId, couponCode);
99
+ try {
100
+ // Soft-delete = bump status to expired with a marker. We never physically
101
+ // delete so the audit trail (claim source + dates) is preserved.
102
+ await this.db
103
+ .collection(this.collection)
104
+ .doc(id)
105
+ .update({
106
+ status: "expired",
107
+ updatedAt: new Date(),
108
+ });
109
+ }
110
+ catch (error) {
111
+ throw new DatabaseError("Failed to remove claimed coupon", error);
112
+ }
113
+ }
114
+ }
115
+ export const claimedCouponsRepository = new ClaimedCouponsRepository();
@@ -70,6 +70,40 @@ export interface CouponUsageDocument {
70
70
  lastUsedAt: Date;
71
71
  orders: string[];
72
72
  }
73
+ export type ClaimedCouponStatus = "active" | "expired" | "used";
74
+ export type ClaimedCouponSource = "manual" | "promo" | "spin" | "raffle" | "prize-draw";
75
+ export interface ClaimedCouponSnapshot {
76
+ name: string;
77
+ description?: string;
78
+ type: CouponType;
79
+ scope: "admin" | "seller";
80
+ storeId?: string;
81
+ discount: DiscountConfig;
82
+ restrictions: RestrictionsConfig;
83
+ }
84
+ export interface ClaimedCouponDocument {
85
+ id: string;
86
+ userId: string;
87
+ couponId: string;
88
+ couponCode: string;
89
+ /** Source surface that initiated the claim — useful for analytics + UX. */
90
+ source: ClaimedCouponSource;
91
+ /** Denormalised snapshot so the wallet list renders without joins. */
92
+ couponSnapshot: ClaimedCouponSnapshot;
93
+ /** Lifecycle: active → used after redemption, → expired when validity.endDate passes. */
94
+ status: ClaimedCouponStatus;
95
+ /** Mirrors coupon.validity.endDate; null when the coupon is open-ended. */
96
+ expiresAt?: Date | null;
97
+ /** Set when status transitions to "used". */
98
+ usedAt?: Date;
99
+ /** Set when status transitions to "used". */
100
+ usedOrderId?: string;
101
+ claimedAt: Date;
102
+ updatedAt: Date;
103
+ }
104
+ export declare const CLAIMED_COUPONS_COLLECTION: "claimedCoupons";
105
+ export declare const CLAIMED_COUPONS_INDEXED_FIELDS: readonly ["userId", "couponCode", "status", "expiresAt"];
106
+ export declare function createClaimedCouponId(userId: string, couponCode: string): string;
73
107
  export declare const COUPONS_COLLECTION: "coupons";
74
108
  export declare const COUPON_USAGE_SUBCOLLECTION: "couponUsage";
75
109
  export declare const COUPONS_INDEXED_FIELDS: readonly ["code", "validity.isActive", "validity.startDate", "validity.endDate", "type", "createdBy"];
@@ -2,6 +2,19 @@
2
2
  * Coupons/Promotions Firestore Document Types & Constants
3
3
  */
4
4
  import { generateCouponId } from "../../../utils/id-generators";
5
+ export const CLAIMED_COUPONS_COLLECTION = "claimedCoupons";
6
+ export const CLAIMED_COUPONS_INDEXED_FIELDS = [
7
+ "userId",
8
+ "couponCode",
9
+ "status",
10
+ "expiresAt",
11
+ ];
12
+ export function createClaimedCouponId(userId, couponCode) {
13
+ // Mirrors the wishlist/history "claimed-{userSlug}-{code}" prefix convention.
14
+ const userSlug = userId.replace(/^user-/, "").toLowerCase();
15
+ const code = couponCode.toLowerCase().replace(/[^a-z0-9]+/g, "-");
16
+ return `claimed-${userSlug}-${code}`;
17
+ }
5
18
  export const COUPONS_COLLECTION = "coupons";
6
19
  export const COUPON_USAGE_SUBCOLLECTION = "couponUsage";
7
20
  export const COUPONS_INDEXED_FIELDS = [
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import { MapPin, Pencil, Plus, Trash2, Star } from "lucide-react";
5
- import { Button, Div, Heading, SideDrawer, Text } from "../../../ui";
5
+ import { Button, Div, Heading, Row, SideDrawer, Text } from "../../../ui";
6
6
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
7
7
  const INPUT_CLS = "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]";
8
8
  const CLS_GRID_2_COL = "grid grid-cols-2 gap-3";
@@ -48,6 +48,7 @@ function AddressCard({ address, onEdit, onDelete, }) {
48
48
  // ---------------------------------------------------------------------------
49
49
  export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES, }) {
50
50
  const [addresses, setAddresses] = useState([]);
51
+ const [listView, setListView] = useState("table");
51
52
  const [isLoading, setIsLoading] = useState(true);
52
53
  const [errorMessage, setErrorMessage] = useState(null);
53
54
  const [drawerOpen, setDrawerOpen] = useState(false);
@@ -143,5 +144,5 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
143
144
  };
144
145
  const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
145
146
  const handleTextField = (key) => (e) => set(key, e.target.value);
146
- return (_jsxs("div", { className: "min-h-screen", children: [_jsxs("div", { className: "sticky z-10 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between", style: { top: "var(--header-height, 0px)" }, children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Pickup Addresses" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { size: "sm", onClick: openAdd, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add Address" })] })] }), _jsxs("div", { className: "py-6 px-4 sm:px-6 max-w-2xl", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), isLoading ? (_jsx("div", { className: "flex justify-center py-16", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-[var(--appkit-color-primary)] border-t-transparent" }) })) : addresses.length === 0 ? (_jsxs(Div, { className: "rounded-xl border-2 border-dashed border-zinc-200 dark:border-slate-700 py-16 flex flex-col items-center gap-3", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsx("div", { className: "grid gap-3", children: addresses.map((addr) => (_jsx("div", { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs("div", { className: "space-y-4 py-1", children: [saveError && (_jsx(Div, { className: "rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-3 py-2 text-sm text-red-700 dark:text-red-200", children: saveError })), _jsx(Field, { label: "Label *", hint: "e.g. Warehouse, Shop, Home", children: _jsx("input", { type: "text", value: draft.label, onChange: handleTextField("label"), placeholder: "Warehouse", maxLength: 60, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Full Name *", children: _jsx("input", { type: "text", value: draft.fullName, onChange: handleTextField("fullName"), placeholder: "Ravi Kumar", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "Phone *", children: _jsx("input", { type: "tel", value: draft.phone, onChange: handleTextField("phone"), placeholder: "+91 98765 43210", maxLength: 20, className: INPUT_CLS }) })] }), _jsx(Field, { label: "Address Line 1 *", children: _jsx("input", { type: "text", value: draft.addressLine1, onChange: handleTextField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Address Line 2", children: _jsx("input", { type: "text", value: draft.addressLine2, onChange: handleTextField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Landmark", children: _jsx("input", { type: "text", value: draft.landmark, onChange: handleTextField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "City *", children: _jsx("input", { type: "text", value: draft.city, onChange: handleTextField("city"), placeholder: "Mumbai", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "State *", children: _jsx("input", { type: "text", value: draft.state, onChange: handleTextField("state"), placeholder: "Maharashtra", maxLength: 100, className: INPUT_CLS }) })] }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Postal Code *", children: _jsx("input", { type: "text", value: draft.postalCode, onChange: handleTextField("postalCode"), placeholder: "400001", maxLength: 10, className: INPUT_CLS }) }), _jsx(Field, { label: "Country *", children: _jsx("input", { type: "text", value: draft.country, onChange: handleTextField("country"), placeholder: "India", maxLength: 60, className: INPUT_CLS }) })] }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.isDefault, onChange: (e) => set("isDefault", e.target.checked), className: "h-4 w-4 rounded border-zinc-300 dark:border-slate-600 text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: "Set as default pickup address" })] })] }) })] }));
147
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsxs("div", { className: "sticky z-10 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between", style: { top: "var(--header-height, 0px)" }, children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Pickup Addresses" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { size: "sm", onClick: openAdd, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add Address" })] })] }), _jsxs("div", { className: "py-6 px-4 sm:px-6 max-w-2xl", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), isLoading ? (_jsx("div", { className: "flex justify-center py-16", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-[var(--appkit-color-primary)] border-t-transparent" }) })) : addresses.length === 0 ? (_jsxs(Div, { className: "rounded-xl border-2 border-dashed border-zinc-200 dark:border-slate-700 py-16 flex flex-col items-center gap-3", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "justify-end mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx("div", { className: "grid gap-3", children: addresses.map((addr) => (_jsx("div", { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx("div", { className: "overflow-x-auto rounded-lg border border-zinc-200 dark:border-slate-700", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-zinc-50 dark:bg-slate-800", children: _jsxs("tr", { children: [_jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Label" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Name" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "City" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Phone" }), _jsx("th", { className: "text-right px-3 py-2 font-semibold", children: "Actions" })] }) }), _jsx("tbody", { children: addresses.map((addr) => (_jsxs("tr", { className: `border-t border-zinc-100 dark:border-slate-700 ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx("td", { className: "px-3 py-2", children: addr.label }), _jsx("td", { className: "px-3 py-2", children: addr.fullName }), _jsxs("td", { className: "px-3 py-2", children: [addr.city, ", ", addr.state] }), _jsx("td", { className: "px-3 py-2 tabular-nums", children: addr.phone }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs(Row, { className: "gap-1 justify-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: "Edit" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: "Delete" })] }) })] }, addr.id))) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs("div", { className: "space-y-4 py-1", children: [saveError && (_jsx(Div, { className: "rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-3 py-2 text-sm text-red-700 dark:text-red-200", children: saveError })), _jsx(Field, { label: "Label *", hint: "e.g. Warehouse, Shop, Home", children: _jsx("input", { type: "text", value: draft.label, onChange: handleTextField("label"), placeholder: "Warehouse", maxLength: 60, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Full Name *", children: _jsx("input", { type: "text", value: draft.fullName, onChange: handleTextField("fullName"), placeholder: "Ravi Kumar", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "Phone *", children: _jsx("input", { type: "tel", value: draft.phone, onChange: handleTextField("phone"), placeholder: "+91 98765 43210", maxLength: 20, className: INPUT_CLS }) })] }), _jsx(Field, { label: "Address Line 1 *", children: _jsx("input", { type: "text", value: draft.addressLine1, onChange: handleTextField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Address Line 2", children: _jsx("input", { type: "text", value: draft.addressLine2, onChange: handleTextField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Landmark", children: _jsx("input", { type: "text", value: draft.landmark, onChange: handleTextField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "City *", children: _jsx("input", { type: "text", value: draft.city, onChange: handleTextField("city"), placeholder: "Mumbai", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "State *", children: _jsx("input", { type: "text", value: draft.state, onChange: handleTextField("state"), placeholder: "Maharashtra", maxLength: 100, className: INPUT_CLS }) })] }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Postal Code *", children: _jsx("input", { type: "text", value: draft.postalCode, onChange: handleTextField("postalCode"), placeholder: "400001", maxLength: 10, className: INPUT_CLS }) }), _jsx(Field, { label: "Country *", children: _jsx("input", { type: "text", value: draft.country, onChange: handleTextField("country"), placeholder: "India", maxLength: 60, className: INPUT_CLS }) })] }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.isDefault, onChange: (e) => set("isDefault", e.target.checked), className: "h-4 w-4 rounded border-zinc-300 dark:border-slate-600 text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: "Set as default pickup address" })] })] }) })] }));
147
148
  }
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useState, useCallback } from "react";
3
+ import React, { useState, useCallback } from "react";
4
4
  import { X } from "lucide-react";
5
5
  import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
6
  import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
7
- import { Badge, Div, FilterChipGroup, ListingToolbar, Pagination, Text } from "../../../ui";
7
+ import { Badge, BulkActionBar, Div, FilterChipGroup, ListingToolbar, Pagination, Text } from "../../../ui";
8
8
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
9
9
  import { SELLER_BID_STATUS_TABS } from "../../admin/constants/filter-tabs";
10
10
  import { toRecordArray, toRelativeDate, toRupees, toStringValue, useSellerListingData, } from "../hooks/useSellerListingData";
@@ -122,5 +122,35 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
122
122
  },
123
123
  ];
124
124
  const selection = useBulkSelection({ items: rows ?? [], keyExtractor: (r) => r.id });
125
- return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by bidder 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("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, columns: columns, isLoading: isLoading, emptyLabel: "No bids found for your auctions" })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: _jsx(FilterChipGroup, { label: "Bid Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })), allId: "" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-[var(--appkit-color-primary)] py-2.5 text-sm font-semibold text-white transition-colors active:scale-[0.98]", children: ["Apply", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
125
+ // S-STORE-4-B group bids by productId (auction) with collapsible sections.
126
+ const [collapsedGroups, setCollapsedGroups] = useState(new Set());
127
+ const groupedRows = React.useMemo(() => {
128
+ const groups = new Map();
129
+ for (const row of rows ?? []) {
130
+ const key = row.productId || row.productTitle;
131
+ const g = groups.get(key) ?? { title: row.productTitle, bids: [] };
132
+ g.bids.push(row);
133
+ groups.set(key, g);
134
+ }
135
+ return Array.from(groups.entries()).map(([id, g]) => ({ id, ...g }));
136
+ }, [rows]);
137
+ const toggleGroup = (id) => setCollapsedGroups((s) => {
138
+ const next = new Set(s);
139
+ next.has(id) ? next.delete(id) : next.add(id);
140
+ return next;
141
+ });
142
+ // S-STORE-4-B — bulk actions: cancel / retract.
143
+ const bulkCancel = useCallback(async () => {
144
+ if (!selection.selectedIds.length)
145
+ return;
146
+ await Promise.all(selection.selectedIds.map((id) => fetch(`/api/store/bids/${id}`, { method: "DELETE" }).catch(() => null)));
147
+ selection.clearSelection();
148
+ }, [selection]);
149
+ const bulkActions = [
150
+ { id: "cancel", label: "Cancel selected", onClick: () => void bulkCancel(), variant: "danger" },
151
+ ];
152
+ return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by bidder 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("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), selection.selectedIds.length > 0 && (_jsx("div", { className: "sticky z-20 px-3 sm:px-4 py-2 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700", style: { top: "calc(var(--header-height, 0px) + 88px)" }, children: _jsx(BulkActionBar, { selectedCount: selection.selectedIds.length, onClearSelection: selection.clearSelection, actions: bulkActions }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), table.get("grouped") === "0" ? (_jsx(DataTable, { rows: rows, columns: columns, isLoading: isLoading, emptyLabel: "No bids found for your auctions", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: () => selection.toggleAll() })) : (_jsxs("div", { className: "space-y-3", children: [groupedRows.length === 0 && !isLoading && (_jsx(Text, { className: "text-sm text-zinc-500", children: "No bids found for your auctions." })), groupedRows.map((group) => {
153
+ const collapsed = collapsedGroups.has(group.id);
154
+ return (_jsxs(Div, { className: "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsxs("button", { type: "button", onClick: () => toggleGroup(group.id), className: "w-full flex items-center justify-between px-4 py-2.5 text-sm font-semibold text-[var(--appkit-color-text)] hover:bg-[var(--appkit-color-surface-raised)]", children: [_jsxs("span", { className: "truncate", children: [group.title, " ", _jsxs("span", { className: "text-xs text-[var(--appkit-color-text-muted)] font-normal", children: ["\u00B7 ", group.bids.length, " bid", group.bids.length === 1 ? "" : "s"] })] }), _jsx("span", { "aria-hidden": true, className: "text-[var(--appkit-color-text-muted)]", children: collapsed ? "▸" : "▾" })] }), !collapsed && (_jsx("div", { className: "border-t border-[var(--appkit-color-border)]", children: _jsx(DataTable, { rows: group.bids, columns: columns, isLoading: false, emptyLabel: "", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle }) }))] }, group.id));
155
+ })] }))] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: _jsx(FilterChipGroup, { label: "Bid Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })), allId: "" }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-[var(--appkit-color-primary)] py-2.5 text-sm font-semibold text-white transition-colors active:scale-[0.98]", children: ["Apply", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
126
156
  }
@@ -200,6 +200,24 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
200
200
  className: "w-28",
201
201
  render: (row) => row.physicalLocation ? (_jsxs("span", { className: "text-xs font-mono text-zinc-500 dark:text-zinc-400", children: [row.physicalLocation.zone, "/", row.physicalLocation.shelf, "/", row.physicalLocation.bin] })) : (_jsx("span", { className: "text-xs text-zinc-400 dark:text-zinc-600", children: "\u2014" })),
202
202
  },
203
+ {
204
+ key: "shipping",
205
+ header: "Shipping",
206
+ className: "w-32",
207
+ render: (row) => {
208
+ const r = row;
209
+ return (_jsx("span", { className: "text-xs text-zinc-600 dark:text-zinc-300", children: r.shippingMethod ?? r.carrier ?? "—" }));
210
+ },
211
+ },
212
+ {
213
+ key: "weight",
214
+ header: "Weight",
215
+ className: "w-20 text-right",
216
+ render: (row) => {
217
+ const r = row;
218
+ return (_jsx("span", { className: "text-xs text-zinc-500 dark:text-zinc-400 tabular-nums", children: r.weightGrams ? `${r.weightGrams} g` : "—" }));
219
+ },
220
+ },
203
221
  {
204
222
  key: "updatedAt",
205
223
  header: "Date",
@@ -224,6 +242,17 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
224
242
  });
225
243
  setSetLocationOpen(false);
226
244
  }, [selection.selectedIds]);
245
+ // S-STORE-5-A — bulk order selection → single payout request.
246
+ const requestPayoutForSelection = useCallback(async () => {
247
+ if (!selection.selectedIds.length)
248
+ return;
249
+ await fetch("/api/store/payouts/request", {
250
+ method: "POST",
251
+ headers: { "Content-Type": "application/json" },
252
+ body: JSON.stringify({ orderIds: selection.selectedIds }),
253
+ }).catch(() => null);
254
+ selection.clearSelection();
255
+ }, [selection]);
227
256
  const bulkActions = [
228
257
  {
229
258
  id: ACTIONS.STORE["print-packing-slips"].id,
@@ -237,6 +266,12 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
237
266
  icon: _jsx(MapPin, { className: "w-4 h-4" }),
238
267
  onClick: () => setSetLocationOpen(true),
239
268
  },
269
+ {
270
+ id: ACTIONS.STORE["request-payout"].id,
271
+ label: ACTIONS.STORE["request-payout"].label,
272
+ onClick: () => void requestPayoutForSelection(),
273
+ variant: "primary",
274
+ },
240
275
  ];
241
276
  if (hasChildren) {
242
277
  return _jsx(ListingViewShell, { portal: "seller", ...props, children: children });
@@ -26,6 +26,8 @@ export interface SellerProductDraft {
26
26
  status?: "draft" | "published";
27
27
  seoTitle?: string;
28
28
  seoDescription?: string;
29
+ /** S-STORE-3-B — 3rd-party hosted video URL. Queues moderation on save. */
30
+ externalVideoUrl?: string;
29
31
  startingBid?: number;
30
32
  reservePrice?: number;
31
33
  buyNowPrice?: number;
@@ -57,7 +57,7 @@ function StepMedia({ values, onChange, storeSlug = "store", }) {
57
57
  type: "product-video",
58
58
  name: values.title ?? "product",
59
59
  store: storeSlug,
60
- }), kind: "video", helperText: "MP4, WebM or QuickTime \u2014 max 50 MB" }), _jsx(FormField, { name: "youtubeId", label: "YouTube Video ID (optional)", type: "text", value: values.youtubeId ?? "", onChange: (v) => onChange({ youtubeId: v }), placeholder: "e.g. dQw4w9WgXcQ", helpText: "Paste the 11-character ID from the YouTube URL" })] }));
60
+ }), kind: "video", helperText: "MP4, WebM or QuickTime \u2014 max 50 MB" }), _jsx(FormField, { name: "externalVideoUrl", label: "3rd-party video URL (YouTube / Vimeo)", type: "text", value: values.externalVideoUrl ?? "", onChange: (v) => onChange({ externalVideoUrl: v }), placeholder: "https://www.youtube.com/watch?v=\u2026", hint: "External video links are queued for moderation and become visible to buyers after admin approval." }), _jsx(FormField, { name: "youtubeId", label: "YouTube Video ID (optional)", type: "text", value: values.youtubeId ?? "", onChange: (v) => onChange({ youtubeId: v }), placeholder: "e.g. dQw4w9WgXcQ", helpText: "Paste the 11-character ID from the YouTube URL" })] }));
61
61
  }
62
62
  // ── Step 3: Auction Settings ───────────────────────────────────────────────
63
63
  function StepAuctionSettings({ values, onChange, }) {
@@ -167,9 +167,31 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
167
167
  clearTimeout(autoSaveTimer.current); };
168
168
  }, [draft, isDirty, mode]);
169
169
  const update = useCallback((partial) => {
170
- setDraft((prev) => ({ ...prev, ...partial }));
170
+ setDraft((prev) => {
171
+ const next = { ...prev, ...partial };
172
+ // S-STORE-3-C — auto-fill slug + SEO from title on create when not manually set.
173
+ if (mode === "create" && partial.title) {
174
+ const autoSlug = partial.title
175
+ .toLowerCase()
176
+ .trim()
177
+ .replace(/[^a-z0-9]+/g, "-")
178
+ .replace(/^-|-$/g, "")
179
+ .slice(0, 80);
180
+ if (!prev.slug || prev.slug === "")
181
+ next.slug = autoSlug;
182
+ if (!prev.seoTitle || prev.seoTitle === "")
183
+ next.seoTitle = partial.title.slice(0, 60);
184
+ }
185
+ if (mode === "create" && partial.description) {
186
+ const text = String(partial.description).replace(/<[^>]+>/g, "").trim();
187
+ if (!prev.seoDescription || prev.seoDescription === "") {
188
+ next.seoDescription = text.slice(0, 160);
189
+ }
190
+ }
191
+ return next;
192
+ });
171
193
  markDirty();
172
- }, [markDirty]);
194
+ }, [markDirty, mode]);
173
195
  const handleDiscard = useCallback(() => {
174
196
  if (onDiscard)
175
197
  onDiscard();
@@ -276,7 +298,7 @@ export function SellerProductShell({ mode, listingType = "standard", initialValu
276
298
  ? `New ${listingTypeLabel}`
277
299
  : draft.title ?? `Edit ${listingTypeLabel}`;
278
300
  if (mode === "create") {
279
- return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true, stepErrors: stepValidationErrors }) }) }));
301
+ return (_jsx(FormShell, { isOpen: true, onClose: handleDiscard, title: title, breadcrumb: breadcrumb, isDirty: isDirty, isLoading: isLoading, previewSlot: previewSlot, splitPreview: !!previewSlot, renderBottomBar: () => (_jsxs("div", { className: "flex-shrink-0 border-t border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", children: [_jsx(StepFormActions, { currentStep: currentStep, totalSteps: steps.length, onNext: () => void handleNext(), onPrev: currentStep > 0 ? () => setCurrentStep((c) => c - 1) : undefined, completeLabel: `Publish ${listingTypeLabel}`, isLoading: isLoading && currentStep === steps.length - 1, disabled: isLoading }), currentStep === 0 && (_jsx("div", { className: "px-5 pb-3", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => void handleSave(), disabled: isLoading || !draft.title?.trim(), children: "Save as draft & finish later" }) })), stepError && (_jsx(Text, { className: "px-5 pb-3 text-sm text-[var(--appkit-color-error)]", children: stepError }))] })), children: _jsx(FormShellProvider, { isDirty: isDirty, values: draft, children: _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: handlePublish, completeLabel: `Publish ${listingTypeLabel}`, currentStep: currentStep, onStepChange: setCurrentStep, isLoading: isLoading, hideActions: true, stepErrors: stepValidationErrors }) }) }));
280
302
  }
281
303
  // Edit mode — FormShell with section nav + full form
282
304
  const editSections = [
@@ -0,0 +1,19 @@
1
+ export interface SellerProductsCardsRowShape {
2
+ id: string;
3
+ primary: string;
4
+ secondary: string;
5
+ imageUrl?: string;
6
+ listingKind: string;
7
+ }
8
+ export interface SellerProductsCardsProps<TRow extends SellerProductsCardsRowShape> {
9
+ view: "grid" | "list";
10
+ rows: TRow[];
11
+ isLoading: boolean;
12
+ listingKind: string;
13
+ selectedIds: Set<string>;
14
+ toggle: (id: string) => void;
15
+ onEdit: (row: TRow) => void;
16
+ onDuplicate: (row: TRow) => void;
17
+ onDelete?: (row: TRow) => void;
18
+ }
19
+ export declare function SellerProductsCards<TRow extends SellerProductsCardsRowShape>({ view, rows, isLoading, listingKind, selectedIds, toggle, onEdit, onDuplicate, onDelete, }: SellerProductsCardsProps<TRow>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Button, Div, Row, Text } from "../../../ui";
4
+ import { CARD_BORDER, CARD_BORDER_ACTIVE, CARD_GRID_CLS, CARD_LIST_CLS, KIND_BADGE_VARIANT } from "./seller-products-styles";
5
+ function buildHref(row) {
6
+ if (row.listingKind === "auction")
7
+ return `/auctions/${row.id}`;
8
+ if (row.listingKind === "pre-order")
9
+ return `/pre-orders/${row.id}`;
10
+ return `/products/${row.id}`;
11
+ }
12
+ export function SellerProductsCards({ view, rows, isLoading, listingKind, selectedIds, toggle, onEdit, onDuplicate, onDelete, }) {
13
+ return (_jsxs(Div, { className: view === "grid" ? CARD_GRID_CLS : CARD_LIST_CLS, children: [rows.length === 0 && !isLoading && (_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)] col-span-full", children: listingKind !== "all" ? `No ${listingKind} listings found` : "No products listed yet" })), rows.map((row) => {
14
+ const isSelected = selectedIds.has(row.id);
15
+ const href = buildHref(row);
16
+ const borderCls = isSelected ? CARD_BORDER_ACTIVE : CARD_BORDER;
17
+ return view === "grid" ? (_jsxs(Div, { className: `group relative rounded-xl border bg-[var(--appkit-color-surface)] overflow-hidden hover:shadow-md transition-shadow ${borderCls}`, children: [_jsx(Div, { className: "absolute top-2 left-2 z-10", children: _jsx("input", { type: "checkbox", checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", onClick: (e) => e.stopPropagation(), "aria-label": "Select" }) }), _jsxs("a", { href: href, className: "block", children: [row.imageUrl ? (_jsx("img", { src: row.imageUrl, alt: "", className: "w-full aspect-square object-cover" })) : (_jsx(Div, { className: "w-full aspect-square bg-[var(--appkit-color-surface-raised)] flex items-center justify-center text-[var(--appkit-color-text-faint)]", children: "\u2014" })), _jsxs(Div, { className: "p-3 space-y-1", children: [_jsx(Text, { className: "font-medium text-sm line-clamp-1", children: row.primary }), _jsxs(Row, { className: "gap-2 items-center", children: [_jsx(Badge, { variant: KIND_BADGE_VARIANT[row.listingKind] ?? "default", children: row.listingKind }), _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] line-clamp-1", children: row.secondary })] })] })] }), _jsxs(Div, { className: "border-t border-[var(--appkit-color-border)] flex justify-end gap-1 p-2", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onEdit(row); }, "aria-label": "Edit", children: "Edit" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDuplicate(row); }, "aria-label": "Duplicate", children: "Duplicate" }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); onDelete(row); }, "aria-label": "Delete", children: "Delete" }))] })] }, row.id)) : (_jsxs(Div, { className: `flex items-center gap-3 rounded-lg border bg-[var(--appkit-color-surface)] px-3 py-2 hover:bg-[var(--appkit-color-surface-raised)] ${borderCls}`, children: [_jsx("input", { type: "checkbox", checked: isSelected, onChange: () => toggle(row.id), className: "h-4 w-4 rounded border-[var(--appkit-color-border)]", "aria-label": "Select" }), row.imageUrl ? (_jsx("img", { src: row.imageUrl, alt: "", className: "w-12 h-12 rounded object-cover" })) : (_jsx(Div, { className: "w-12 h-12 rounded bg-[var(--appkit-color-surface-raised)]" })), _jsxs("a", { href: href, className: "flex-1 min-w-0", children: [_jsx(Text, { className: "font-medium text-sm line-clamp-1", children: row.primary }), _jsxs(Row, { className: "gap-2 items-center", children: [_jsx(Badge, { variant: KIND_BADGE_VARIANT[row.listingKind] ?? "default", children: row.listingKind }), _jsx(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] line-clamp-1", children: row.secondary })] })] }), _jsxs(Row, { className: "gap-1 flex-shrink-0", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onEdit(row), "aria-label": "Edit", children: "Edit" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDuplicate(row), "aria-label": "Duplicate", children: "Duplicate" }), onDelete && (_jsx(Button, { variant: "ghost", size: "sm", onClick: () => onDelete(row), "aria-label": "Delete", children: "Delete" }))] })] }, row.id));
18
+ })] }));
19
+ }
@@ -0,0 +1,14 @@
1
+ export interface SellerProductsFilterDrawerProps {
2
+ isOpen: boolean;
3
+ pendingFilters: Record<string, string>;
4
+ statusOptions: ReadonlyArray<{
5
+ id: string;
6
+ label: string;
7
+ }>;
8
+ activeFilterCount: number;
9
+ onChange: (next: Record<string, string>) => void;
10
+ onClear: () => void;
11
+ onApply: () => void;
12
+ onClose: () => void;
13
+ }
14
+ export declare function SellerProductsFilterDrawer({ isOpen, pendingFilters, statusOptions, activeFilterCount, onChange, onClear, onApply, onClose, }: SellerProductsFilterDrawerProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { X } from "lucide-react";
4
+ import { Button, Div, FilterChipGroup, Row, Text } from "../../../ui";
5
+ import { INPUT_CLS, FILTER_LABEL_CLS } from "./seller-products-styles";
6
+ const HALF_INPUT_CLS = INPUT_CLS.replace("w-full", "w-1/2");
7
+ const CONDITION_OPTIONS = [
8
+ { value: "", label: "Any" },
9
+ { value: "mint", label: "Mint" },
10
+ { value: "new", label: "New" },
11
+ { value: "used", label: "Used" },
12
+ { value: "refurbished", label: "Refurbished" },
13
+ { value: "for-parts", label: "For parts" },
14
+ ];
15
+ export function SellerProductsFilterDrawer({ isOpen, pendingFilters, statusOptions, activeFilterCount, onChange, onClear, onApply, onClose, }) {
16
+ if (!isOpen)
17
+ return null;
18
+ const patch = (k, v) => onChange({ ...pendingFilters, [k]: v });
19
+ return (_jsxs(_Fragment, { children: [_jsx(Div, { role: "presentation", className: "fixed inset-0 z-40 bg-black/40", onClick: onClose }), _jsxs(Div, { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-[var(--appkit-color-surface)] shadow-2xl", children: [_jsxs(Row, { justify: "between", className: "border-b border-[var(--appkit-color-border)] px-4 py-3.5", children: [_jsx(Text, { className: "text-base font-semibold text-[var(--appkit-color-text)]", children: "Filters" }), _jsxs(Row, { className: "gap-2", children: [activeFilterCount > 0 && (_jsx(Button, { variant: "ghost", size: "sm", onClick: onClear, className: "text-xs text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-error)]", children: "Clear all" })), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close filters", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs(Div, { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: statusOptions, value: pendingFilters.status ?? "", onChange: (id) => patch("status", id) }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Category" }), _jsx("input", { type: "text", value: pendingFilters.category ?? "", onChange: (e) => patch("category", e.target.value), placeholder: "category slug", className: INPUT_CLS })] }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Brand" }), _jsx("input", { type: "text", value: pendingFilters.brand ?? "", onChange: (e) => patch("brand", e.target.value), placeholder: "brand slug", className: INPUT_CLS })] }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Condition" }), _jsx("select", { value: pendingFilters.condition ?? "", onChange: (e) => patch("condition", e.target.value), className: INPUT_CLS, children: CONDITION_OPTIONS.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })] }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Price (paise)" }), _jsxs(Row, { className: "gap-2", children: [_jsx("input", { type: "number", min: 0, value: pendingFilters.minPrice ?? "", onChange: (e) => patch("minPrice", e.target.value), placeholder: "min", className: HALF_INPUT_CLS }), _jsx("input", { type: "number", min: 0, value: pendingFilters.maxPrice ?? "", onChange: (e) => patch("maxPrice", e.target.value), placeholder: "max", className: HALF_INPUT_CLS })] })] }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Tags (comma-separated)" }), _jsx("input", { type: "text", value: pendingFilters.tags ?? "", onChange: (e) => patch("tags", e.target.value), placeholder: "pokemon, vintage, holo", className: INPUT_CLS })] }), _jsxs(Div, { className: "space-y-1.5", children: [_jsx(Text, { className: FILTER_LABEL_CLS, children: "Badges (feature slugs)" }), _jsx("input", { type: "text", value: pendingFilters.badges ?? "", onChange: (e) => patch("badges", e.target.value), placeholder: "feature-free-shipping, feature-verified", className: INPUT_CLS })] })] }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)] px-4 py-3.5", children: _jsxs(Button, { variant: "primary", onClick: onApply, className: "w-full rounded-lg py-2.5 active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }));
20
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ListingViewShellProps } from "../../../ui";
2
2
  export interface SellerProductsViewProps extends ListingViewShellProps {
3
3
  onDeleteProduct?: (id: string) => Promise<void>;
4
+ /** S-STORE-2-E — "New Listing" toolbar button. */
5
+ onCreateClick?: () => void;
4
6
  }
5
- export declare function SellerProductsView({ onDeleteProduct, children, ...props }: SellerProductsViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function SellerProductsView({ onDeleteProduct, onCreateClick, children, ...props }: SellerProductsViewProps): import("react/jsx-runtime").JSX.Element;