@mohasinac/appkit 2.7.48 → 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.
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +19 -0
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +15 -0
- package/dist/_internal/client/features/layout/index.d.ts +2 -0
- package/dist/_internal/client/features/layout/index.js +1 -0
- package/dist/_internal/server/features/checkout/actions.js +25 -2
- package/dist/_internal/server/features/promotions/actions.js +3 -1
- package/dist/_internal/shared/actions/action-registry.js +149 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +13 -0
- package/dist/features/account/components/UserSidebar.js +2 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +6 -2
- package/dist/features/admin/components/AdminSidebar.js +2 -1
- package/dist/features/auth/repository/session.repository.js +16 -1
- package/dist/features/cart/hooks/useAddToCart.d.ts +2 -0
- package/dist/features/cart/hooks/useAddToCart.js +2 -0
- package/dist/features/cart/hooks/useGuestCart.d.ts +2 -0
- package/dist/features/cart/utils/guest-cart.d.ts +7 -0
- package/dist/features/cart/utils/pending-ops.d.ts +6 -0
- package/dist/features/categories/components/CategoryBundlesListing.js +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +6 -10
- package/dist/features/categories/schemas/firestore.d.ts +9 -0
- package/dist/features/events/components/EventRaffleWinnerView.d.ts +9 -1
- package/dist/features/events/components/EventRaffleWinnerView.js +3 -3
- package/dist/features/events/components/SpinWheelView.js +3 -3
- package/dist/features/layout/BottomNavLayout.d.ts +5 -0
- package/dist/features/layout/BottomNavLayout.js +21 -0
- package/dist/features/layout/TitleBar.js +7 -1
- package/dist/features/layout/TitleBarLayout.d.ts +8 -2
- package/dist/features/layout/TitleBarLayout.js +11 -7
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +9 -5
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +3 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +8 -12
- package/dist/features/products/components/ProductDetailActions.d.ts +5 -1
- package/dist/features/products/components/ProductDetailActions.js +3 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +2 -0
- package/dist/features/products/components/ProductDetailPageView.js +4 -0
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/components/ProductsIndexListing.js +15 -8
- package/dist/features/promotions/components/CouponCard.d.ts +11 -1
- package/dist/features/promotions/components/CouponCard.js +36 -2
- package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +27 -0
- package/dist/features/promotions/repository/claimed-coupons.repository.js +115 -0
- package/dist/features/promotions/schemas/firestore.d.ts +34 -0
- package/dist/features/promotions/schemas/firestore.js +13 -0
- package/dist/features/seller/components/SellerAddressesView.js +3 -2
- package/dist/features/seller/components/SellerBidsView.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +35 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +26 -4
- package/dist/features/seller/components/SellerProductsCards.d.ts +19 -0
- package/dist/features/seller/components/SellerProductsCards.js +19 -0
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +14 -0
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +20 -0
- package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
- package/dist/features/seller/components/SellerProductsView.js +68 -19
- package/dist/features/seller/components/SellerReviewsView.js +67 -4
- package/dist/features/seller/components/SellerSidebar.js +2 -1
- package/dist/features/seller/components/seller-products-styles.d.ts +7 -0
- package/dist/features/seller/components/seller-products-styles.js +14 -0
- package/dist/features/shell/FormShell.d.ts +7 -1
- package/dist/features/shell/FormShell.js +5 -2
- package/dist/features/store-extensions/index.d.ts +4 -0
- package/dist/features/store-extensions/index.js +4 -0
- package/dist/features/store-extensions/repository/rbac.repositories.d.ts +23 -0
- package/dist/features/store-extensions/repository/rbac.repositories.js +32 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +91 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +127 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +244 -0
- package/dist/features/store-extensions/schemas/firestore.js +158 -0
- package/dist/features/store-extensions/schemas/rbac.d.ts +65 -0
- package/dist/features/store-extensions/schemas/rbac.js +43 -0
- package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
- package/dist/features/stores/components/StoreProductsListing.js +6 -10
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/next/routing/route-map.d.ts +79 -0
- package/dist/next/routing/route-map.js +34 -0
- package/dist/react/hooks/useFormStatePreservation.d.ts +17 -0
- package/dist/react/hooks/useFormStatePreservation.js +62 -0
- package/dist/react/hooks/useInlineRowEdit.d.ts +24 -0
- package/dist/react/hooks/useInlineRowEdit.js +68 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +4 -0
- package/dist/repositories/index.d.ts +4 -0
- package/dist/repositories/index.js +5 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/claimed-coupons-seed-data.d.ts +13 -0
- package/dist/seed/claimed-coupons-seed-data.js +79 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/manifest.js +17 -0
- package/dist/seed/store-extensions-seed-data.d.ts +19 -0
- package/dist/seed/store-extensions-seed-data.js +421 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.style.css +20 -12
- package/dist/ui/components/ClaimCouponButton.d.ts +28 -0
- package/dist/ui/components/ClaimCouponButton.js +89 -0
- package/dist/ui/components/QuickCreateModal.d.ts +15 -0
- package/dist/ui/components/QuickCreateModal.js +48 -0
- package/dist/ui/components/QuickCreateModal.style.css +84 -0
- package/dist/ui/components/StickyBottomBar.d.ts +22 -0
- package/dist/ui/components/StickyBottomBar.js +20 -0
- package/dist/ui/components/VacationBanner.d.ts +11 -0
- package/dist/ui/components/VacationBanner.js +16 -0
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +4 -0
- 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
|
-
|
|
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) =>
|
|
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;
|