@mohasinac/appkit 3.5.5 → 3.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +24 -3
- package/dist/_internal/client/features/tour/TourProvider.d.ts +11 -3
- package/dist/_internal/client/features/tour/TourProvider.js +93 -4
- package/dist/_internal/client/scaffolds/index.d.ts +0 -2
- package/dist/_internal/client/scaffolds/index.js +0 -1
- package/dist/_internal/server/features/checkout/actions.js +65 -11
- package/dist/_internal/server/features/orders/adapters.js +3 -0
- package/dist/_internal/shared/actions/action-registry.js +20 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +21 -0
- package/dist/_internal/shared/checkout/order-math.js +24 -0
- package/dist/_internal/shared/features/orders/config.d.ts +1 -1
- package/dist/_internal/shared/features/orders/config.js +1 -1
- package/dist/_internal/shared/fees/calculator.d.ts +13 -0
- package/dist/_internal/shared/fees/calculator.js +8 -0
- package/dist/client-entry.d.ts +2 -2
- package/dist/client-entry.js +2 -2
- package/dist/client.d.ts +8 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +9 -0
- package/dist/constants/api-endpoints.js +3 -0
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/admin/components/AdminBundleEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminBundleEditorView.js +15 -11
- package/dist/features/admin/components/AdminFeaturesView.js +1 -1
- package/dist/features/admin/components/AdminPayoutsView.js +41 -4
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +49 -1
- package/dist/features/admin/components/DataTable.js +1 -1
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +8 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/before-after/components/BeforeAfterSlider.js +4 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +1 -1
- package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -9
- package/dist/features/events/components/EventCard.js +1 -1
- package/dist/features/events/components/EventRafflesSection.d.ts +1 -1
- package/dist/features/events/components/EventRafflesSection.js +3 -3
- package/dist/features/homepage/components/BeforeAfterCard.js +5 -0
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +3 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +5 -5
- package/dist/features/media/MediaLightbox.js +7 -1
- package/dist/features/media/components/MediaSlider.js +4 -1
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/upload/ImageUpload.js +24 -12
- package/dist/features/messages/hooks/useConversations.d.ts +7 -1
- package/dist/features/messages/hooks/useConversations.js +7 -6
- package/dist/features/orders/actions/order-actions.d.ts +10 -0
- package/dist/features/orders/actions/order-actions.js +57 -4
- package/dist/features/orders/components/MarketplaceOrderCard.js +1 -1
- package/dist/features/orders/schemas/firestore.d.ts +16 -0
- package/dist/features/orders/types/index.d.ts +2 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
- package/dist/features/products/components/PrizeDrawsSection.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawsSection.js +3 -2
- package/dist/features/products/components/ProductForm.js +4 -9
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/products/schemas/firestore.d.ts +4 -0
- package/dist/features/promotions/components/CouponCard.js +1 -1
- package/dist/features/scams/actions/scam-actions.d.ts +12 -0
- package/dist/features/scams/actions/scam-actions.js +31 -0
- package/dist/features/scams/components/SellerTrustBadge.d.ts +16 -0
- package/dist/features/scams/components/SellerTrustBadge.js +21 -0
- package/dist/features/scams/components/index.d.ts +2 -0
- package/dist/features/scams/components/index.js +1 -0
- package/dist/features/seller/components/SellerBundlesView.d.ts +5 -5
- package/dist/features/seller/components/SellerBundlesView.js +81 -84
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +11 -5
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shipments/components/ShipmentItemLinkModal.js +5 -5
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +8 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +9 -3
- package/dist/features/stores/components/StoreHeader.d.ts +4 -1
- package/dist/features/stores/components/StoreHeader.js +3 -2
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/jobs.d.ts +2 -0
- package/dist/jobs.js +6 -0
- package/dist/next/routing/route-map.d.ts +3 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/react/hooks/useClickOutside.js +3 -0
- package/dist/security/rate-limit.d.ts +5 -0
- package/dist/security/rate-limit.js +2 -0
- package/dist/seed/categories-seed-data.js +180 -1
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/site-settings-seed-data.js +9 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +11 -4
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/cobalt-night.d.ts +17 -0
- package/dist/tokens/themes/cobalt-night.js +50 -0
- package/dist/tokens/themes/index.d.ts +9 -1
- package/dist/tokens/themes/index.js +12 -1
- package/dist/tokens/themes/sunset.d.ts +18 -0
- package/dist/tokens/themes/sunset.js +51 -0
- package/dist/tokens/tokens.css +10 -3
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -4
- package/dist/ui/components/Card.d.ts +1 -1
- package/dist/ui/components/Card.js +2 -2
- package/dist/ui/components/Div.js +2 -2
- package/dist/ui/components/Form.d.ts +1 -1
- package/dist/ui/components/Form.js +2 -2
- package/dist/ui/components/Input.js +1 -0
- package/dist/ui/components/ListingToolbar.js +1 -1
- package/dist/ui/components/QuickCreateModal.style.css +1 -1
- package/dist/ui/components/Slider.js +5 -1
- package/package.json +3 -2
|
@@ -2,7 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
3
|
import { sieveFilter, sieveAnd, SIEVE_OP } from "@mohasinac/appkit";
|
|
4
4
|
import { sortBy } from "@mohasinac/appkit";
|
|
5
|
-
import Link from "next/link";
|
|
6
5
|
import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
|
|
7
6
|
import { ROUTES } from "../../../next";
|
|
8
7
|
import { productRepository } from "../repository/products.repository";
|
|
@@ -28,5 +27,7 @@ export async function PrizeDrawsSection({ config, }) {
|
|
|
28
27
|
void normalizeError(_err);
|
|
29
28
|
draws = [];
|
|
30
29
|
}
|
|
31
|
-
|
|
30
|
+
if (draws.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(Div, { gap: "3", className: "fluid-grid-card", children: draws.map((draw) => (_jsx(InteractiveProductCard, { product: draw, href: String(ROUTES.PUBLIC.PRODUCT_DETAIL(draw.slug ?? draw.id ?? "")) }, draw.id))) })] }) }) }));
|
|
32
33
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { Row } from "@mohasinac/appkit/ui";
|
|
4
4
|
import { useRef } from "react";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
-
import { Alert, Checkbox, Div, FormField, FormGroup, Heading, Label, RichTextEditor, Stack, Text, } from "../../../ui";
|
|
6
|
+
import { Alert, Checkbox, Div, FormField, FormGroup, Heading, Label, RichTextEditor, Stack, TagInput, Text, } from "../../../ui";
|
|
7
7
|
import { CustomSectionsEditor } from "./CustomSectionsEditor";
|
|
8
8
|
import { SublistingCategorySelect } from "./SublistingCategorySelect";
|
|
9
9
|
import { ProductFeaturesSelector } from "./ProductFeaturesSelector";
|
|
@@ -101,7 +101,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
101
101
|
onValueChange: (value) => update({ brand: value }),
|
|
102
102
|
onValuesChange: (values) => update({ brands: values }),
|
|
103
103
|
disabled: isReadonly,
|
|
104
|
-
})) : product.brandMode === "mixed" ? (_jsx(
|
|
104
|
+
})) : product.brandMode === "mixed" ? (_jsx(TagInput, { label: "Brands", value: product.brands || [], onChange: (brands) => update({ brands }), disabled: isReadonly, placeholder: "e.g. Nike, Adidas, Puma" })) : (_jsx(FormField, { name: "brand", label: t("formBrand"), type: "text", value: product.brand || "", onChange: (value) => update({ brand: value }), disabled: isReadonly, placeholder: "e.g. Apple" })) }))] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(Div, {}), " ", _jsx(FormField, { name: "status", label: t("formStatus"), type: "select", value: product.status || "draft", onChange: (value) => update({ status: value }), disabled: isReadonly, options: PRODUCT_STATUS_OPTIONS })] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "price", label: t("formPrice"), type: "number", value: String(product.price ?? ""), onChange: (value) => update({ price: Number(value) }), disabled: isReadonly, placeholder: "0" }), _jsx(FormField, { name: "stockQuantity", label: t("formStock"), type: "number", value: String(product.stockQuantity ?? ""), onChange: (value) => update({ stockQuantity: Number(value) }), disabled: isReadonly, placeholder: "0" })] }), !isReadonly && (_jsx(ImageUpload, { currentImage: product.mainImage, onUpload: (file) => upload(file, "products", true, {
|
|
105
105
|
type: "product-image",
|
|
106
106
|
index: 1,
|
|
107
107
|
name: product.title || "product",
|
|
@@ -132,12 +132,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
132
132
|
thumbnailUrl: media.thumbnailUrl,
|
|
133
133
|
},
|
|
134
134
|
});
|
|
135
|
-
}, onUpload: handleVideoUpload, accept: "video/*", maxSizeMB: 50, helperText: t("formVideoHelper"), onAbort: onMediaAbort })), _jsx(
|
|
136
|
-
tags: value
|
|
137
|
-
.split(",")
|
|
138
|
-
.map((tag) => tag.trim())
|
|
139
|
-
.filter(Boolean),
|
|
140
|
-
}), disabled: isReadonly, placeholder: t("formTagsPlaceholder") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(Checkbox, { label: t("formFeatured"), checked: !!product.featured, onChange: (e) => update({ featured: e.target.checked }), disabled: isReadonly }), _jsx(Checkbox, { label: t("formIsPromoted"), checked: !!product.isPromoted, onChange: (e) => update({ isPromoted: e.target.checked }), disabled: isReadonly }), _jsx(Checkbox, { label: t("formAllowShipBeforeEmiComplete"), checked: !!product.allowShipBeforeEmiComplete, onChange: (e) => update({ allowShipBeforeEmiComplete: e.target.checked }), disabled: isReadonly })] }), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionConditionShipping") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "condition", label: t("formCondition"), type: "select", value: product.condition || "new", onChange: (value) => update({
|
|
135
|
+
}, onUpload: handleVideoUpload, accept: "video/*", maxSizeMB: 50, helperText: t("formVideoHelper"), onAbort: onMediaAbort })), _jsx(TagInput, { label: t("formTags"), value: product.tags || [], onChange: (tags) => update({ tags }), disabled: isReadonly, placeholder: t("formTagsPlaceholder") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(Checkbox, { label: t("formFeatured"), checked: !!product.featured, onChange: (e) => update({ featured: e.target.checked }), disabled: isReadonly }), _jsx(Checkbox, { label: t("formIsPromoted"), checked: !!product.isPromoted, onChange: (e) => update({ isPromoted: e.target.checked }), disabled: isReadonly }), _jsx(Checkbox, { label: t("formAllowShipBeforeEmiComplete"), checked: !!product.allowShipBeforeEmiComplete, onChange: (e) => update({ allowShipBeforeEmiComplete: e.target.checked }), disabled: isReadonly })] }), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionConditionShipping") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "condition", label: t("formCondition"), type: "select", value: product.condition || "new", onChange: (value) => update({
|
|
141
136
|
condition: value,
|
|
142
137
|
}), disabled: isReadonly, options: [
|
|
143
138
|
{ value: "new", label: t("formConditionNew") },
|
|
@@ -209,7 +204,7 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
209
204
|
value: product.pickupAddressId || "",
|
|
210
205
|
onChange: (value) => update({ pickupAddressId: value }),
|
|
211
206
|
disabled: isReadonly,
|
|
212
|
-
}) })) : (_jsx(FormField, { name: "pickupAddressId", label: t("formPickupAddress"), type: "text", value: product.pickupAddressId || "", onChange: (value) => update({ pickupAddressId: value }), disabled: isReadonly })), _jsx(SublistingCategorySelect, { value: product.sublistingCategoryId ?? "", onChange: (id) => update({ sublistingCategoryId: id || undefined }), disabled: isReadonly }), !isAuctionListing(product) && renderGroupJoinField?.({
|
|
207
|
+
}) })) : (_jsx(FormField, { name: "pickupAddressId", label: t("formPickupAddress"), type: "text", value: product.pickupAddressId || "", onChange: (value) => update({ pickupAddressId: value }), disabled: isReadonly })), _jsx(SublistingCategorySelect, { value: product.sublistingCategoryId ?? "", onChange: (id) => update({ sublistingCategoryId: id || undefined }), disabled: isReadonly, allowCreate: true }), !isAuctionListing(product) && renderGroupJoinField?.({
|
|
213
208
|
label: "Group (optional)",
|
|
214
209
|
value: product.groupId,
|
|
215
210
|
onChange: (id) => update({ groupId: id || undefined }),
|
|
@@ -24,7 +24,7 @@ const CLS_BADGE_SALE = "rounded-full bg-success-surface px-[var(--appkit-space-2
|
|
|
24
24
|
const CLS_BADGE_TRENDING = "rounded-full bg-indigo-600 px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[10px] font-bold text-white shadow-sm";
|
|
25
25
|
const CLS_BADGE_LIMITED = "rounded-full bg-info-surface px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[10px] font-bold text-white shadow-sm";
|
|
26
26
|
const CLS_HEART_ACTIVE = "bg-error-surface text-white hover:bg-error-surface";
|
|
27
|
-
const CLS_HEART_IDLE = "bg-
|
|
27
|
+
const CLS_HEART_IDLE = "bg-[var(--appkit-color-surface-elevated)]/90 text-[var(--appkit-color-text-muted)] hover:text-error hover:bg-[var(--appkit-color-surface-elevated)]";
|
|
28
28
|
const CLS_STAR = "text-[11px] text-warning";
|
|
29
29
|
const CLS_DISCOUNT_TEXT = "mt-1 text-[11px] font-semibold text-error";
|
|
30
30
|
const CLS_DISCOUNT_TEXT_BARE = "text-[10px] text-error";
|
|
@@ -195,6 +195,10 @@ export interface ProductDocument extends BaseDocument {
|
|
|
195
195
|
isSold?: boolean;
|
|
196
196
|
/** When true, an EMI order for this product ships as soon as it's confirmed instead of waiting for every installment to be paid. Default false. */
|
|
197
197
|
allowShipBeforeEmiComplete?: boolean;
|
|
198
|
+
/** P-8 GST — buyer-facing tax rate on this product (%). Unset/0 = exempt. */
|
|
199
|
+
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
200
|
+
/** P-8 GST — Harmonized System of Nomenclature code for GST-compliant invoices. */
|
|
201
|
+
hsnCode?: string;
|
|
198
202
|
/** Print-specific metadata for "art" / "stickers" listings — printed-only physical goods. Optional on every listing type; only populated for art/stickers. */
|
|
199
203
|
printMeta?: ProductPrintMeta;
|
|
200
204
|
promotionEndDate?: Date;
|
|
@@ -167,7 +167,7 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
|
|
|
167
167
|
setBusy(null);
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
|
-
return (_jsxs(Stack, { className: `group relative h-full border-2 ${colors.card} ${isSelected ? "ring-2 ring-primary" : ""} ${!n.isActive ? "opacity-70" : ""} ${className}`, rounded: "xl", padding: "md", onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(n.id, !isSelected); }, label: isSelected ? "Deselect coupon" : "Select coupon", position: "top-2 right-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), (hasAdminActions || n.scope) && (_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Badge, { variant: n.isActive ? "active" : "inactive", size: "xs", className: "text-[10px] uppercase tracking-wide", children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { layout: "inline-flex", weight: "semibold", className: "bg-neutral-200/70 text-[10px] tracking-wide bg-[var(--appkit-color-border)]/60", rounded: "full", padding: "pill-xs", color: "primary", transform: "uppercase", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "font-extrabold tracking-tight leading-none", size: "2xl", children: discountLabel }), n.name && (_jsx(Text, { className: "mt-0.5 opacity-80", size: "sm", weight: "medium", children: n.name }))] }), n.description && (_jsx(Text, { className: "opacity-60 mb-3", size: "xs", children: n.description })), _jsxs(Row, { className: `border border-dashed ${colors.code}`, align: "center", gap: "sm", rounded: "lg", padding: "inlineSm", children: [_jsx(Span, { size: "sm", weight: "bold", className: "flex-1 font-mono tracking-widest select-all", transform: "uppercase", children: n.code || "—" }), n.code && (_jsx(Button, { variant: "outline", type: "button", onClick: handleCopy, rounded: "md", paddingX: "sm", paddingY: "none", textSize: "xs", weight: "semibold", className: "shrink-0 hover:opacity-80 active:scale-95 border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx(Div, { className: "mt-2", children: _jsx(Button, { variant: "primary", type: "button", onClick: handleClaim, disabled: claiming, rounded: "md", paddingX: "md", paddingY: "xs", textSize: "xs", weight: "semibold", className: "w-full active:scale-95 bg-[var(--appkit-color-primary-700)] hover:bg-[var(--appkit-color-primary-800)]", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { textSize: "xs", wrap: true, gap: "sm", className: "opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": \u20B9", (n.minOrderAmount / 100).toFixed(0)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", justify: "end", className: "mt-auto", padding: "t-sm", children: [onEdit && (_jsx(IconButton, { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx(IconButton, { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: n.isActive
|
|
170
|
+
return (_jsxs(Stack, { className: `group relative h-full border-2 ${colors.card} ${isSelected ? "ring-2 ring-primary" : ""} ${!n.isActive ? "opacity-70" : ""} ${className}`, rounded: "xl", padding: "md", onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(n.id, !isSelected); }, label: isSelected ? "Deselect coupon" : "Select coupon", position: "top-2 right-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), (hasAdminActions || n.scope) && (_jsxs(Row, { gap: "xs", wrap: true, className: "mb-2", children: [_jsx(Badge, { variant: n.isActive ? "active" : "inactive", size: "xs", className: "text-[10px] uppercase tracking-wide", children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { layout: "inline-flex", weight: "semibold", className: "bg-neutral-200/70 text-[10px] tracking-wide bg-[var(--appkit-color-border)]/60", rounded: "full", padding: "pill-xs", color: "primary", transform: "uppercase", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "font-extrabold tracking-tight leading-none", size: "2xl", children: discountLabel }), n.name && (_jsx(Text, { className: "mt-0.5 opacity-80", size: "sm", weight: "medium", children: n.name }))] }), n.description && (_jsx(Text, { className: "opacity-60 mb-3", size: "xs", children: n.description })), _jsxs(Row, { className: `border border-dashed ${colors.code}`, align: "center", gap: "sm", rounded: "lg", padding: "inlineSm", children: [_jsx(Span, { size: "sm", weight: "bold", className: "flex-1 font-mono tracking-widest select-all", transform: "uppercase", children: n.code || "—" }), n.code && (_jsx(Button, { variant: "outline", type: "button", onClick: handleCopy, rounded: "md", paddingX: "sm", paddingY: "none", textSize: "xs", weight: "semibold", className: "shrink-0 hover:opacity-80 active:scale-95 border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx(Div, { className: "mt-2", children: _jsx(Button, { variant: "primary", type: "button", onClick: handleClaim, disabled: claiming, rounded: "md", paddingX: "md", paddingY: "xs", textSize: "xs", weight: "semibold", className: "w-full active:scale-95 bg-[var(--appkit-color-primary-700)] hover:bg-[var(--appkit-color-primary-800)]", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { textSize: "xs", wrap: true, gap: "sm", className: "opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": \u20B9", (n.minOrderAmount / 100).toFixed(0)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", justify: "end", className: "mt-auto", padding: "t-sm", children: [onEdit && (_jsx(IconButton, { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx(IconButton, { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)]", children: n.isActive
|
|
171
171
|
? _jsx(ToggleRight, { className: CLS_TOGGLE_ON })
|
|
172
172
|
: _jsx(ToggleLeft, { className: "h-4 w-4" }) })), onDelete && (_jsx(IconButton, { type: "button", onClick: () => setDeleteConfirmOpen(true), disabled: busy === "delete", title: labels.delete, "aria-label": labels.delete, variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })), deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Coupon", message: labels.deleteConfirm, onConfirm: handleDeleteConfirmed, onClose: () => setDeleteConfirmOpen(false), isDeleting: busy === "delete" }))] }));
|
|
173
173
|
}
|
|
@@ -27,3 +27,15 @@ export declare function getPublicScammerById(id: string): Promise<ScammerDocumen
|
|
|
27
27
|
* Returns null if not found or not verified.
|
|
28
28
|
*/
|
|
29
29
|
export declare function getScammerProfilePageData(id: string): Promise<ScammerProfilePageData | null>;
|
|
30
|
+
export type SellerTrustStatus = "clear" | "flagged";
|
|
31
|
+
export interface SellerTrustResult {
|
|
32
|
+
status: SellerTrustStatus;
|
|
33
|
+
/** seoSlug of matched verified scammer profiles — links to /scams/[slug]. Empty when clear. */
|
|
34
|
+
matchedProfileSlugs: string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* P-12 — resolves a store owner's phone/email against the verified (admin-published)
|
|
38
|
+
* scammer registry only. Never surfaces "under review" reports to buyers — a report
|
|
39
|
+
* that hasn't been verified must not damage a seller's storefront reputation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getSellerTrustStatus(storeId: string): Promise<SellerTrustResult>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
3
|
import { scammerRepository } from "../repository/scammer.repository";
|
|
4
|
+
import { userRepository } from "../../auth/repository/user.repository";
|
|
5
|
+
import { storeRepository } from "../../stores/repository/store.repository";
|
|
4
6
|
import { safeFireAndForget } from "../../../utils/safe-fire-forget";
|
|
5
7
|
/**
|
|
6
8
|
* List verified scammer profiles for the public registry page.
|
|
@@ -62,3 +64,32 @@ export async function getScammerProfilePageData(id) {
|
|
|
62
64
|
]);
|
|
63
65
|
return { scammer, incidents, comments, relatedScammers };
|
|
64
66
|
}
|
|
67
|
+
const TRUST_STATUS_CLEAR = { status: "clear", matchedProfileSlugs: [] };
|
|
68
|
+
/**
|
|
69
|
+
* P-12 — resolves a store owner's phone/email against the verified (admin-published)
|
|
70
|
+
* scammer registry only. Never surfaces "under review" reports to buyers — a report
|
|
71
|
+
* that hasn't been verified must not damage a seller's storefront reputation.
|
|
72
|
+
*/
|
|
73
|
+
export async function getSellerTrustStatus(storeId) {
|
|
74
|
+
const store = await storeRepository.findById(storeId).catch(() => null);
|
|
75
|
+
if (!store?.ownerId)
|
|
76
|
+
return TRUST_STATUS_CLEAR;
|
|
77
|
+
const owner = await userRepository.findById(store.ownerId).catch(() => null);
|
|
78
|
+
if (!owner)
|
|
79
|
+
return TRUST_STATUS_CLEAR;
|
|
80
|
+
const lookups = [];
|
|
81
|
+
if (owner.phoneNumber)
|
|
82
|
+
lookups.push(scammerRepository.findByContactField("phones", owner.phoneNumber));
|
|
83
|
+
if (owner.email)
|
|
84
|
+
lookups.push(scammerRepository.findByContactField("emails", owner.email));
|
|
85
|
+
if (lookups.length === 0)
|
|
86
|
+
return TRUST_STATUS_CLEAR;
|
|
87
|
+
const results = await Promise.all(lookups.map((p) => p.catch(() => [])));
|
|
88
|
+
const verified = results.flat().filter((s) => s.status === "verified");
|
|
89
|
+
if (verified.length === 0)
|
|
90
|
+
return TRUST_STATUS_CLEAR;
|
|
91
|
+
return {
|
|
92
|
+
status: "flagged",
|
|
93
|
+
matchedProfileSlugs: [...new Set(verified.map((s) => s.seoSlug))],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SellerTrustResult } from "../actions/scam-actions";
|
|
2
|
+
export interface SellerTrustBadgeProps {
|
|
3
|
+
trust: SellerTrustResult;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* P-12 — renders next to a seller's storefront name. Only ever reflects
|
|
8
|
+
* admin-verified scammer profiles (see getSellerTrustStatus) — unverified
|
|
9
|
+
* reports never reach this component, so "clear" here means "no verified
|
|
10
|
+
* match," not "no reports filed."
|
|
11
|
+
*
|
|
12
|
+
* Named SellerTrustBadge (not TrustBadge) to avoid colliding with the
|
|
13
|
+
* unrelated homepage-section `TrustBadge` config type in
|
|
14
|
+
* features/homepage/index.ts (marketing trust-badge icons row).
|
|
15
|
+
*/
|
|
16
|
+
export declare function SellerTrustBadge({ trust, className }: SellerTrustBadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Badge } from "../../../ui/components/Badge";
|
|
3
|
+
import { TextLink } from "../../../ui/components/TextLink";
|
|
4
|
+
import { ROUTES } from "../../../next";
|
|
5
|
+
/**
|
|
6
|
+
* P-12 — renders next to a seller's storefront name. Only ever reflects
|
|
7
|
+
* admin-verified scammer profiles (see getSellerTrustStatus) — unverified
|
|
8
|
+
* reports never reach this component, so "clear" here means "no verified
|
|
9
|
+
* match," not "no reports filed."
|
|
10
|
+
*
|
|
11
|
+
* Named SellerTrustBadge (not TrustBadge) to avoid colliding with the
|
|
12
|
+
* unrelated homepage-section `TrustBadge` config type in
|
|
13
|
+
* features/homepage/index.ts (marketing trust-badge icons row).
|
|
14
|
+
*/
|
|
15
|
+
export function SellerTrustBadge({ trust, className = "" }) {
|
|
16
|
+
if (trust.status === "clear") {
|
|
17
|
+
return (_jsx(Badge, { variant: "success", className: className, children: "\u2713 Verified Safe" }));
|
|
18
|
+
}
|
|
19
|
+
const firstSlug = trust.matchedProfileSlugs[0];
|
|
20
|
+
return (_jsx(Badge, { variant: "danger", className: className, children: firstSlug ? (_jsx(TextLink, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(firstSlug)), children: "\u26A0 Flagged in Scam Registry" })) : ("⚠ Flagged in Scam Registry") }));
|
|
21
|
+
}
|
|
@@ -4,3 +4,5 @@ export { ScamProfileView } from "./ScamProfileView";
|
|
|
4
4
|
export type { ScamProfileViewProps } from "./ScamProfileView";
|
|
5
5
|
export { ScamAwarenessModal } from "./ScamAwarenessModal";
|
|
6
6
|
export type { ScamAwarenessModalProps } from "./ScamAwarenessModal";
|
|
7
|
+
export { SellerTrustBadge } from "./SellerTrustBadge";
|
|
8
|
+
export type { SellerTrustBadgeProps } from "./SellerTrustBadge";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
export interface SellerBundlesViewProps {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
getEditHref: (row: {
|
|
4
|
+
id: string;
|
|
5
|
+
}) => string;
|
|
6
|
+
newHref: string;
|
|
7
7
|
}
|
|
8
|
-
export declare function SellerBundlesView({
|
|
8
|
+
export declare function SellerBundlesView({ getEditHref, newHref }: SellerBundlesViewProps): React.JSX.Element;
|
|
@@ -1,111 +1,108 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
|
|
6
|
-
import { ConfirmDeleteModal, RowActionMenu, Text } from "../../../ui";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { SIEVE_OP, sieveFilter, sortBy } from "@mohasinac/appkit";
|
|
4
|
+
import { Badge, Button, FilterChipGroup, Stack, Text, TextLink } from "../../../ui";
|
|
7
5
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import { toRecordArray, toRelativeDate, toRupees, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
6
|
+
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
7
|
+
import { SELLER_BULK_ACTIONS } from "../../products/constants/action-defs";
|
|
8
|
+
import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
9
|
+
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData";
|
|
13
10
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
11
|
+
function formatPrice(paise) {
|
|
12
|
+
if (typeof paise !== "number" || paise <= 0)
|
|
13
|
+
return "—";
|
|
14
|
+
return `₹${Math.round(paise / 100).toLocaleString("en-IN")}`;
|
|
15
|
+
}
|
|
14
16
|
const COLUMNS = [
|
|
15
|
-
{ key: "title", header: "Bundle", render: (row) => _jsx(Text, { size: "sm", weight: "medium", children: row.title }) },
|
|
16
|
-
{ key: "price", header: "Price", render: (row) => _jsx(Text, { className: "tabular-nums", size: "sm", children: row.price }) },
|
|
17
17
|
{
|
|
18
|
-
key: "
|
|
19
|
-
header: "
|
|
20
|
-
render: (row) => (_jsx(
|
|
18
|
+
key: "primary",
|
|
19
|
+
header: "Name",
|
|
20
|
+
render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "medium", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
|
|
21
21
|
},
|
|
22
|
+
{ key: "price", header: "Price", className: "w-28" },
|
|
23
|
+
{ key: "members", header: "Members", className: "w-24" },
|
|
22
24
|
{
|
|
23
|
-
key: "
|
|
24
|
-
header: "
|
|
25
|
-
|
|
25
|
+
key: "stockStatus",
|
|
26
|
+
header: "Stock",
|
|
27
|
+
className: "w-28",
|
|
28
|
+
render: (row) => (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[row.stockStatus] ?? "default", children: row.stockStatus === "in_stock"
|
|
29
|
+
? BUNDLE_COPY.stockBadge.listVariantInStock
|
|
30
|
+
: BUNDLE_COPY.stockBadge.listVariantOutOfStock })),
|
|
26
31
|
},
|
|
27
32
|
{
|
|
28
|
-
key: "
|
|
29
|
-
header: "
|
|
30
|
-
|
|
33
|
+
key: "status",
|
|
34
|
+
header: "Status",
|
|
35
|
+
className: "w-24",
|
|
36
|
+
render: (row) => (_jsx(Badge, { variant: row.isActive ? "success" : "default", children: row.status })),
|
|
31
37
|
},
|
|
38
|
+
{ key: "updatedAt", header: "Updated", className: "w-28" },
|
|
32
39
|
];
|
|
33
|
-
export function SellerBundlesView({
|
|
34
|
-
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
35
|
-
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
36
|
-
endpoint: SELLER_ENDPOINTS.PRODUCT_BY_ID,
|
|
37
|
-
deleteFn: onDelete,
|
|
38
|
-
successMessage: "Bundle deleted.",
|
|
39
|
-
fetchOptions: { credentials: "include" },
|
|
40
|
-
});
|
|
41
|
-
const handleDelete = useCallback(async (id) => {
|
|
42
|
-
await performDelete(id);
|
|
43
|
-
setDeleteTargetId(null);
|
|
44
|
-
}, [performDelete]);
|
|
45
|
-
const handleEdit = useCallback((id) => {
|
|
46
|
-
if (onEditClick)
|
|
47
|
-
onEditClick(id);
|
|
48
|
-
else
|
|
49
|
-
window.location.href = String(ROUTES.STORE.PRODUCTS_EDIT(id));
|
|
50
|
-
}, [onEditClick]);
|
|
51
|
-
const handleCreate = useCallback(() => {
|
|
52
|
-
if (onCreateClick)
|
|
53
|
-
onCreateClick();
|
|
54
|
-
else
|
|
55
|
-
window.location.href = String(ROUTES.STORE.PRODUCTS_NEW);
|
|
56
|
-
}, [onCreateClick]);
|
|
40
|
+
export function SellerBundlesView({ getEditHref, newHref }) {
|
|
57
41
|
const config = {
|
|
58
42
|
portal: "seller",
|
|
59
43
|
title: "Bundles",
|
|
60
|
-
searchPlaceholder: "Search bundles
|
|
61
|
-
emptyLabel: "No bundles yet — create a bundle to group multiple products together",
|
|
62
|
-
filterKeys: [],
|
|
63
|
-
defaultSort: sortBy("
|
|
64
|
-
queryKey: ["seller", "bundles"],
|
|
65
|
-
endpoint: SELLER_ENDPOINTS.
|
|
44
|
+
searchPlaceholder: "Search bundles by name or slug…",
|
|
45
|
+
emptyLabel: "No bundles yet — create a bundle to group multiple of your products together",
|
|
46
|
+
filterKeys: ["isActive", "bundleStockStatus"],
|
|
47
|
+
defaultSort: sortBy("name", "ASC"),
|
|
48
|
+
queryKey: ["seller", "bundles", "listing"],
|
|
49
|
+
endpoint: SELLER_ENDPOINTS.BUNDLES,
|
|
66
50
|
sortOptions: [
|
|
51
|
+
{ value: sortBy("name", "ASC"), label: "Name A–Z" },
|
|
52
|
+
{ value: sortBy("name", "DESC"), label: "Name Z–A" },
|
|
53
|
+
{ value: sortBy("bundlePriceInPaise", "DESC"), label: "Price high→low" },
|
|
54
|
+
{ value: "bundlePriceInPaise", label: "Price low→high" },
|
|
67
55
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
68
56
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
69
|
-
{ value: "productTitle", label: "Name A–Z" },
|
|
70
|
-
{ value: sortBy("productTitle", "DESC"), label: "Name Z–A" },
|
|
71
|
-
{ value: sortBy("price", "ASC"), label: "Price: Low–High" },
|
|
72
|
-
{ value: sortBy("price", "DESC"), label: "Price: High–Low" },
|
|
73
57
|
],
|
|
74
58
|
columns: COLUMNS,
|
|
75
|
-
mapRows: (response) => toRecordArray(response.
|
|
59
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
76
60
|
id: toStringValue(item.id, `bundle-${index}`),
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
primary: toStringValue(item.name, "Untitled bundle"),
|
|
62
|
+
secondary: toStringValue(item.slug, "no-slug"),
|
|
63
|
+
price: formatPrice(item.bundlePriceInPaise),
|
|
64
|
+
members: String(Array.isArray(item.bundleProductIds) ? item.bundleProductIds.length : 0),
|
|
65
|
+
stockStatus: toStringValue(item.bundleStockStatus, "in_stock"),
|
|
66
|
+
isActive: item.isActive === true,
|
|
67
|
+
status: item.isActive === true
|
|
68
|
+
? BUNDLE_COPY.adminList.activeBadge
|
|
69
|
+
: BUNDLE_COPY.adminList.inactiveBadge,
|
|
70
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
84
71
|
})),
|
|
85
|
-
getTotal: (response, mappedRows) => typeof response.
|
|
86
|
-
buildFilters: () =>
|
|
87
|
-
|
|
72
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
73
|
+
buildFilters: (filterState) => {
|
|
74
|
+
const parts = [];
|
|
75
|
+
if (filterState.isActive)
|
|
76
|
+
parts.push(sieveFilter("isActive", SIEVE_OP.EQ, filterState.isActive));
|
|
77
|
+
if (filterState.bundleStockStatus)
|
|
78
|
+
parts.push(sieveFilter("bundleStockStatus", SIEVE_OP.EQ, filterState.bundleStockStatus));
|
|
79
|
+
return parts.join(",") || undefined;
|
|
80
|
+
},
|
|
81
|
+
getRowHref: getEditHref,
|
|
82
|
+
toolbarExtra: (_jsx(Button, { asChild: true, size: "sm", variant: "primary", children: _jsx(TextLink, { href: newHref, layout: "flex", align: "center", gap: "xs", children: "+ New Bundle" }) })),
|
|
88
83
|
// Rule #7: bulk-action array sourced from the SELLER_BULK_ACTIONS preset.
|
|
89
|
-
buildBulkActions:
|
|
90
|
-
|
|
84
|
+
buildBulkActions: (selection) => {
|
|
85
|
+
const handlers = {
|
|
86
|
+
[ROW_ACTION_ID.DELETE]: async () => {
|
|
87
|
+
await Promise.all(selection.selectedIds.map((id) => fetch(SELLER_ENDPOINTS.BUNDLE_BY_ID(id), { method: "DELETE" })));
|
|
88
|
+
selection.clearSelection();
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
return SELLER_BULK_ACTIONS.bundles.map((id) => ({
|
|
91
92
|
id,
|
|
92
93
|
label: ROW_ACTION_META[id].label,
|
|
93
94
|
destructive: ROW_ACTION_META[id].destructive,
|
|
94
|
-
onClick:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
onClick: () => setDeleteTargetId(row.id),
|
|
106
|
-
disabled: deletingId === row.id,
|
|
107
|
-
},
|
|
108
|
-
] })),
|
|
95
|
+
onClick: handlers[id],
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: [
|
|
99
|
+
{ id: "All", label: "All" },
|
|
100
|
+
{ id: "true", label: "Active" },
|
|
101
|
+
{ id: "false", label: "Inactive" },
|
|
102
|
+
], value: pendingFilters.isActive || "", onChange: (v) => setPendingFilters((p) => ({ ...p, isActive: v })) }), _jsx(FilterChipGroup, { label: "Stock", tabs: [
|
|
103
|
+
{ id: "All", label: "All" },
|
|
104
|
+
{ id: "out_of_stock", label: "Sold out" },
|
|
105
|
+
], value: pendingFilters.bundleStockStatus || "", onChange: (v) => setPendingFilters((p) => ({ ...p, bundleStockStatus: v })) })] })),
|
|
109
106
|
};
|
|
110
|
-
return
|
|
107
|
+
return _jsx(DataListingView, { config: config });
|
|
111
108
|
}
|
|
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { FormShell, StepForm, StepFormActions, useFormShell } from "../../shell";
|
|
7
7
|
import { FormShellProvider } from "../../../ui/forms";
|
|
8
|
-
import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, Text, Toggle, useToast } from "../../../ui";
|
|
8
|
+
import { Alert, Button, Div, FormField, FormGroup, Heading, Section, Stack, TagInput, Text, Toggle, useToast } from "../../../ui";
|
|
9
9
|
import { ImageUpload, MediaUploadField, MediaUploadList, useMediaUpload } from "../../media";
|
|
10
10
|
import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddressSelectorCreate";
|
|
11
11
|
import { QuickProductForm } from "./QuickProductForm";
|
|
@@ -54,7 +54,7 @@ function toPaise(rupeeStr) {
|
|
|
54
54
|
}
|
|
55
55
|
// ── Step 1: Basic ─────────────────────────────────────────────────────────────
|
|
56
56
|
function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelector, renderTemplateSelector, }) {
|
|
57
|
-
return (_jsxs(Stack, { gap: "md", children: [renderTemplateSelector?.({ onApply: onChange }), _jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsx(FormField, { name: "description", label: "Description", type: "textarea", value: values.description ?? "", onChange: (v) => onChange({ description: v }), placeholder: "Describe your listing in detail\u2026" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({ value: values.category ?? "", onChange: (v) => onChange({ category: v }) })] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards" })), _jsx(FormField, { name: "condition", label: "Condition", type: "select", value: values.condition ?? "new", onChange: (v) => onChange({ condition: v }), options: CONDITION_OPTIONS })] }), renderBrandSelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Brand" }), renderBrandSelector({ value: values.brand ?? "", onChange: (v) => onChange({ brand: v }) })] })) : (_jsx(FormField, { name: "brand", label: "Brand", type: "text", value: values.brand ?? "", onChange: (v) => onChange({ brand: v }), placeholder: "e.g. Pok\u00E9mon Company" })), _jsx(
|
|
57
|
+
return (_jsxs(Stack, { gap: "md", children: [renderTemplateSelector?.({ onApply: onChange }), _jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsx(FormField, { name: "description", label: "Description", type: "textarea", value: values.description ?? "", onChange: (v) => onChange({ description: v }), placeholder: "Describe your listing in detail\u2026" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({ value: values.category ?? "", onChange: (v) => onChange({ category: v }) })] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards" })), _jsx(FormField, { name: "condition", label: "Condition", type: "select", value: values.condition ?? "new", onChange: (v) => onChange({ condition: v }), options: CONDITION_OPTIONS })] }), renderBrandSelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Brand" }), renderBrandSelector({ value: values.brand ?? "", onChange: (v) => onChange({ brand: v }) })] })) : (_jsx(FormField, { name: "brand", label: "Brand", type: "text", value: values.brand ?? "", onChange: (v) => onChange({ brand: v }), placeholder: "e.g. Pok\u00E9mon Company" })), _jsx(TagInput, { label: "Tags", value: values.tags ?? [], onChange: (tags) => onChange({ tags }), placeholder: "pokemon, psa9, charizard, holo" }), _jsx(BarcodeField, { value: values.barcodeId ?? "", onChange: (v) => onChange({ barcodeId: v || undefined }), onScan: (v) => onChange({ barcodeId: v }), helperText: "Leave blank to auto-generate. Scan a pre-printed sticker to link it." })] }));
|
|
58
58
|
}
|
|
59
59
|
// ── Step 2: Media ─────────────────────────────────────────────────────────────
|
|
60
60
|
function StepMedia({ values, onChange, storeSlug = "store", }) {
|
|
@@ -70,7 +70,7 @@ function StepMedia({ values, onChange, storeSlug = "store", }) {
|
|
|
70
70
|
name: values.title ?? "product",
|
|
71
71
|
store: storeSlug,
|
|
72
72
|
category: values.category ?? "uncategorized",
|
|
73
|
-
}), onChange: (url) => onChange({ mainImage: url }), label: "Main Image", helperText: "Recommended: 800\u00D7800px square (JPG, PNG, WebP \u2014 max 10 MB)" }), _jsx(MediaUploadList, { label: "Gallery Images (up to 10)", value: galleryFields, onChange: (fields) => onChange({ images: fields.map((f) => f.url) }), onUpload: (file) => {
|
|
73
|
+
}), onChange: (url) => onChange({ mainImage: url }), label: "Main Image", helperText: "Recommended: 800\u00D7800px square (JPG, PNG, WebP \u2014 max 10 MB)", enableAdvancedCrop: true, cropAspectRatio: 1 }), _jsx(MediaUploadList, { label: "Gallery Images (up to 10)", value: galleryFields, onChange: (fields) => onChange({ images: fields.map((f) => f.url) }), onUpload: (file) => {
|
|
74
74
|
idxRef.current += 1;
|
|
75
75
|
return upload(file, "products", true, {
|
|
76
76
|
type: "product-image",
|
|
@@ -126,7 +126,7 @@ const LIVE_TRANSPORT_OPTIONS = [
|
|
|
126
126
|
{ value: "specialist", label: "Third-party specialist transport" },
|
|
127
127
|
];
|
|
128
128
|
function StepLiveItemSettings({ values, onChange, }) {
|
|
129
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "warning", children: "Live animal / plant listings must comply with all applicable laws, CITES regulations, and local jurisdiction rules. LetItRip is not responsible for legal compliance \u2014 this is solely the seller's responsibility." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "liveSpecies", label: "Species / Common Name", type: "text", value: values.liveSpecies ?? "", onChange: (v) => onChange({ liveSpecies: v }), placeholder: "e.g. Axolotl (Ambystoma mexicanum)" }), _jsx(FormField, { name: "liveAgeMonths", label: "Age (months, optional)", type: "number", value: String(values.liveAgeMonths ?? ""), onChange: (v) => onChange({ liveAgeMonths: v ? Number(v) : undefined }), placeholder: "e.g. 6" })] }), _jsx(FormField, { name: "liveSex", label: "Sex", type: "select", value: values.liveSex ?? "unknown", onChange: (v) => onChange({ liveSex: v }), options: LIVE_SEX_OPTIONS }), _jsx(FormField, { name: "liveCareInfo", label: "Care Instructions (optional)", type: "textarea", value: values.liveCareInfo ?? "", onChange: (v) => onChange({ liveCareInfo: v }), placeholder: "Diet, temperature, habitat requirements\u2026" }), _jsx(FormField, { name: "liveTransportMethod", label: "Transport Method", type: "select", value: values.liveTransportMethod ?? "in-person", onChange: (v) => onChange({ liveTransportMethod: v }), options: LIVE_TRANSPORT_OPTIONS }), values.liveTransportMethod !== "in-person" && (_jsx(FormField, { name: "liveHandlingFee", label: "Handling / Transport Fee (\u20B9, optional)", type: "number", value: toRupees(values.liveHandlingFee), onChange: (v) => onChange({ liveHandlingFee: v ? toPaise(v) : undefined }), placeholder: "0" })), _jsx(
|
|
129
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(Alert, { variant: "warning", children: "Live animal / plant listings must comply with all applicable laws, CITES regulations, and local jurisdiction rules. LetItRip is not responsible for legal compliance \u2014 this is solely the seller's responsibility." }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "liveSpecies", label: "Species / Common Name", type: "text", value: values.liveSpecies ?? "", onChange: (v) => onChange({ liveSpecies: v }), placeholder: "e.g. Axolotl (Ambystoma mexicanum)" }), _jsx(FormField, { name: "liveAgeMonths", label: "Age (months, optional)", type: "number", value: String(values.liveAgeMonths ?? ""), onChange: (v) => onChange({ liveAgeMonths: v ? Number(v) : undefined }), placeholder: "e.g. 6" })] }), _jsx(FormField, { name: "liveSex", label: "Sex", type: "select", value: values.liveSex ?? "unknown", onChange: (v) => onChange({ liveSex: v }), options: LIVE_SEX_OPTIONS }), _jsx(FormField, { name: "liveCareInfo", label: "Care Instructions (optional)", type: "textarea", value: values.liveCareInfo ?? "", onChange: (v) => onChange({ liveCareInfo: v }), placeholder: "Diet, temperature, habitat requirements\u2026" }), _jsx(FormField, { name: "liveTransportMethod", label: "Transport Method", type: "select", value: values.liveTransportMethod ?? "in-person", onChange: (v) => onChange({ liveTransportMethod: v }), options: LIVE_TRANSPORT_OPTIONS }), values.liveTransportMethod !== "in-person" && (_jsx(FormField, { name: "liveHandlingFee", label: "Handling / Transport Fee (\u20B9, optional)", type: "number", value: toRupees(values.liveHandlingFee), onChange: (v) => onChange({ liveHandlingFee: v ? toPaise(v) : undefined }), placeholder: "0" })), _jsx(TagInput, { label: "Jurisdictions where sale is permitted", value: values.liveJurisdictions ?? [], onChange: (liveJurisdictions) => onChange({ liveJurisdictions }), placeholder: "e.g. Maharashtra, Karnataka", helperText: "List all Indian states / UTs where you can legally sell" }), _jsx(Toggle, { checked: !!values.liveCites, onChange: (checked) => onChange({ liveCites: checked }), label: "This species requires CITES documentation" })] }));
|
|
130
130
|
}
|
|
131
131
|
// ── Step: Print Meta (art / stickers) ───────────────────────────────────────
|
|
132
132
|
// Shared by both "art" and "stickers" — identical field set, only the intro
|
|
@@ -190,7 +190,13 @@ function StepShipping({ values, onChange, renderAddressSelector, }) {
|
|
|
190
190
|
] }), renderAddressSelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Pickup Address (optional)" }), renderAddressSelector({
|
|
191
191
|
value: values.pickupAddressId ?? "",
|
|
192
192
|
onChange: (v) => onChange({ pickupAddressId: v }),
|
|
193
|
-
})] })) : (_jsx(StoreAddressSelectorCreate, { label: "Pickup Address (optional)", value: values.pickupAddressId ?? "", onChange: (id) => onChange({ pickupAddressId: id }) })), _jsx(Toggle, { checked: !!values.insurance, onChange: (checked) => onChange({ insurance: checked, insuranceCost: checked ? values.insuranceCost ?? 0 : undefined }), label: "Offer shipping insurance" }), values.insurance && (_jsx(FormField, { name: "insuranceCost", label: "Insurance Cost (\u20B9)", type: "number", value: toRupees(values.insuranceCost), onChange: (v) => onChange({ insuranceCost: toPaise(v) }), placeholder: "0" }))
|
|
193
|
+
})] })) : (_jsx(StoreAddressSelectorCreate, { label: "Pickup Address (optional)", value: values.pickupAddressId ?? "", onChange: (id) => onChange({ pickupAddressId: id }) })), _jsx(Toggle, { checked: !!values.insurance, onChange: (checked) => onChange({ insurance: checked, insuranceCost: checked ? values.insuranceCost ?? 0 : undefined }), label: "Offer shipping insurance" }), values.insurance && (_jsx(FormField, { name: "insuranceCost", label: "Insurance Cost (\u20B9)", type: "number", value: toRupees(values.insuranceCost), onChange: (v) => onChange({ insuranceCost: toPaise(v) }), placeholder: "0" })), _jsx(FormField, { name: "gstRate", label: "GST Rate", type: "select", value: String(values.gstRate ?? 0), onChange: (v) => onChange({ gstRate: Number(v) }), options: [
|
|
194
|
+
{ value: "0", label: "Exempt (0%)" },
|
|
195
|
+
{ value: "5", label: "5%" },
|
|
196
|
+
{ value: "12", label: "12%" },
|
|
197
|
+
{ value: "18", label: "18%" },
|
|
198
|
+
{ value: "28", label: "28%" },
|
|
199
|
+
] }), _jsx(FormField, { name: "hsnCode", label: "HSN Code", value: values.hsnCode ?? "", onChange: (v) => onChange({ hsnCode: v }), placeholder: "e.g. 9503" })] }));
|
|
194
200
|
}
|
|
195
201
|
// ── Step: Publish / SEO ───────────────────────────────────────────────────
|
|
196
202
|
function StepPublish({ values, onChange, }) {
|
|
@@ -23,7 +23,7 @@ function isNavItemActive(item, activeHref) {
|
|
|
23
23
|
function NavLink({ item, isActive, onClick }) {
|
|
24
24
|
return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
|
|
25
25
|
? CLS_NAV_ACTIVE
|
|
26
|
-
: "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, {
|
|
26
|
+
: "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "flex-1 truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
|
|
27
27
|
}
|
|
28
28
|
function FlatContent({ items, activeHref, storeName, storeLogoURL, onItemClick, }) {
|
|
29
29
|
return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsx(Nav, { "aria-label": "Store navigation", padding: "y-sm", children: _jsx(Ul, { paddingX: "x-sm", spacing: "2xs", children: items.map((item) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
|
-
import { Button, FieldInput, Modal, Row, Stack, Text, Toggle } from "../../../ui";
|
|
5
|
+
import { Button, FieldInput, Modal, Row, Stack, TagInput, Text, Toggle } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
8
|
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
@@ -13,7 +13,7 @@ import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect
|
|
|
13
13
|
export function ShipmentItemLinkModal({ shipmentId, lotId, itemId, itemTitle, isOpen, onClose, onLinked, }) {
|
|
14
14
|
const [mode, setMode] = React.useState("create");
|
|
15
15
|
const [existingProductId, setExistingProductId] = React.useState(null);
|
|
16
|
-
const [categorySlugs, setCategorySlugs] = React.useState(
|
|
16
|
+
const [categorySlugs, setCategorySlugs] = React.useState([]);
|
|
17
17
|
const [brandSlug, setBrandSlug] = React.useState("");
|
|
18
18
|
const linkMutation = useApiMutation({
|
|
19
19
|
successMessage: "Item linked to product",
|
|
@@ -22,7 +22,7 @@ export function ShipmentItemLinkModal({ shipmentId, lotId, itemId, itemTitle, is
|
|
|
22
22
|
? { mode: "link", productId: existingProductId }
|
|
23
23
|
: {
|
|
24
24
|
mode: "create",
|
|
25
|
-
categorySlugs
|
|
25
|
+
categorySlugs,
|
|
26
26
|
brandSlug: brandSlug.trim() || undefined,
|
|
27
27
|
};
|
|
28
28
|
return apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_LINK(shipmentId, lotId, itemId), body);
|
|
@@ -32,6 +32,6 @@ export function ShipmentItemLinkModal({ shipmentId, lotId, itemId, itemTitle, is
|
|
|
32
32
|
onClose();
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
|
-
const canSubmit = mode === "link" ? !!existingProductId : categorySlugs.
|
|
36
|
-
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: `Create pre-order link — ${itemTitle}`, size: "md", children: _jsxs(Stack, { gap: "md", children: [_jsx(Text, { variant: "secondary", children: "Links this item to a real product listing. This is a deliberate, one-time action \u2014 nothing links automatically." }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Toggle, { checked: mode === "create", onChange: (checked) => setMode(checked ? "create" : "link"), size: "sm" }), _jsx(Text, { children: mode === "create" ? "Create a new pre-order product" : "Link to an existing product" })] }), mode === "link" ? (_jsx(ProductInlineSelect, { scope: "admin", value: existingProductId, onChange: setExistingProductId, placeholder: "Search products\u2026" })) : (_jsxs(Stack, { gap: "sm", children: [_jsx(
|
|
35
|
+
const canSubmit = mode === "link" ? !!existingProductId : categorySlugs.length > 0;
|
|
36
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: `Create pre-order link — ${itemTitle}`, size: "md", children: _jsxs(Stack, { gap: "md", children: [_jsx(Text, { variant: "secondary", children: "Links this item to a real product listing. This is a deliberate, one-time action \u2014 nothing links automatically." }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Toggle, { checked: mode === "create", onChange: (checked) => setMode(checked ? "create" : "link"), size: "sm" }), _jsx(Text, { children: mode === "create" ? "Create a new pre-order product" : "Link to an existing product" })] }), mode === "link" ? (_jsx(ProductInlineSelect, { scope: "admin", value: existingProductId, onChange: setExistingProductId, placeholder: "Search products\u2026" })) : (_jsxs(Stack, { gap: "sm", children: [_jsx(TagInput, { label: "Category slugs", value: categorySlugs, onChange: setCategorySlugs, placeholder: "category-action-figures" }), _jsx(FieldInput, { name: "brandSlug", label: "Brand slug (optional)", value: brandSlug, onChange: setBrandSlug })] })), _jsx(Button, { isLoading: linkMutation.isPending, disabled: !canSubmit, onClick: () => linkMutation.mutate(), children: mode === "create" ? "Create & Link" : "Link" })] }) }));
|
|
37
37
|
}
|