@mohasinac/appkit 4.27.0 → 4.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +27 -13
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
- package/dist/features/reviews/repository/reviews.repository.js +53 -13
- package/dist/features/reviews/schemas/firestore.d.ts +6 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +20 -4
- package/dist/security/pii-schemas.js +23 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@ import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/const
|
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize";
|
|
12
|
+
import { TextLink } from "../../../ui";
|
|
13
|
+
import { ROUTES } from "../../../constants/index";
|
|
12
14
|
const __O = {
|
|
13
15
|
xAuto: "overflow-x-auto",
|
|
14
16
|
};
|
|
@@ -141,7 +143,7 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
141
143
|
const handleDelete = (addr) => setDeleteTargetAddr(addr);
|
|
142
144
|
const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
|
|
143
145
|
const setField = (key) => (value) => set(key, value);
|
|
144
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-
|
|
146
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "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(Grid, { gap: "sm", 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: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
145
147
|
const isBanned = addr.banStatus === "banned";
|
|
146
148
|
return (_jsxs(Tr, { className: `border-t border-[var(--appkit-color-border)] ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx(Td, { padding: "sm", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { children: addr.label }), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }) }), _jsx(Td, { padding: "sm", children: addr.fullName }), _jsxs(Td, { padding: "sm", children: [addr.city, ", ", addr.state] }), _jsx(Td, { className: "tabular-nums", padding: "sm", children: addr.phone }), _jsx(Td, { className: "text-right", padding: "sm", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(Row, { justify: "end", gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label })] })) })] }, addr.id));
|
|
147
149
|
}) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs(Row, { gap: "xs", 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(Stack, { gap: "md", padding: "y-2xs", children: [saveError && (_jsx(Div, { textSize: "sm", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError })), _jsx(FieldInput, { name: "label", label: "Label *", hint: "e.g. Warehouse, Shop, Home", type: "text", value: draft.label, onChange: setField("label"), placeholder: "Warehouse", maxLength: 60 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "fullName", label: "Full Name *", type: "text", value: draft.fullName, onChange: setField("fullName"), placeholder: "Ravi Kumar", maxLength: 100 }), _jsx(FieldInput, { name: "phone", label: "Phone *", type: "tel", value: draft.phone, onChange: setField("phone"), placeholder: "+91 98765 43210", maxLength: 20 })] }), _jsx(FieldInput, { name: "addressLine1", label: "Address Line 1 *", type: "text", value: draft.addressLine1, onChange: setField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200 }), _jsx(FieldInput, { name: "addressLine2", label: "Address Line 2", type: "text", value: draft.addressLine2, onChange: setField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200 }), _jsx(FieldInput, { name: "landmark", label: "Landmark", type: "text", value: draft.landmark, onChange: setField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100 }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "city", label: "City *", type: "text", value: draft.city, onChange: setField("city"), placeholder: "Mumbai", maxLength: 100 }), _jsx(FieldInput, { name: "state", label: "State *", type: "text", value: draft.state, onChange: setField("state"), placeholder: "Maharashtra", maxLength: 100 })] }), _jsxs(Grid, { cols: 2, gap: "sm", children: [_jsx(FieldInput, { name: "postalCode", label: "Postal Code *", type: "text", value: draft.postalCode, onChange: setField("postalCode"), placeholder: "400001", maxLength: 10 }), _jsx(FieldInput, { name: "country", label: "Country *", type: "text", value: draft.country, onChange: setField("country"), placeholder: "India", maxLength: 60 })] }), _jsx(FieldCheckbox, { name: "isDefault", label: "Set as default pickup address", checked: draft.isDefault, onChange: (c) => set("isDefault", c) })] }) }), deleteTargetAddr && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Address", message: `Delete address "${deleteTargetAddr.label}"? This cannot be undone.`, onConfirm: () => { deleteById(deleteTargetAddr.id); setDeleteTargetAddr(null); }, onClose: () => setDeleteTargetAddr(null), isDeleting: deletingId === deleteTargetAddr.id }))] }));
|
|
@@ -13,6 +13,8 @@ import { SELLER_BID_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
|
13
13
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue } from "../hooks/useSellerListingData";
|
|
14
14
|
import { DataTable } from "../../admin/components/DataTable";
|
|
15
15
|
import { DataListingView } from "../../admin/components/DataListingView";
|
|
16
|
+
import { TextLink } from "../../../ui";
|
|
17
|
+
import { ROUTES } from "../../../constants/index";
|
|
16
18
|
const DEFAULT_SORT = "-bidDate";
|
|
17
19
|
const SORT_OPTIONS = [
|
|
18
20
|
{ value: sortBy("bidDate", "DESC"), label: "Newest" },
|
|
@@ -141,5 +143,8 @@ export function SellerBidsView({ endpoint = SELLER_ENDPOINTS.BIDS }) {
|
|
|
141
143
|
},
|
|
142
144
|
};
|
|
143
145
|
const detailBadge = detail ? bidStatusBadge(detail.status) : null;
|
|
144
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined
|
|
146
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(RecordDetailModal, { isOpen: detail !== null, onClose: () => setDetail(null), title: detail?.productTitle || "Bid", badges: detailBadge ? [{ label: detailBadge.label }] : undefined, fields: detail ? buildBidDetailFields(detail, "seller") : undefined, footer: detail ? (
|
|
147
|
+
// The full page — a modal cannot be linked, bookmarked or reloaded
|
|
148
|
+
// into, which is the whole reason the page exists.
|
|
149
|
+
_jsx(TextLink, { href: String(ROUTES.STORE.BID_DETAIL(detail.id)), children: "Open full page \u2192" })) : undefined })] }));
|
|
145
150
|
}
|
|
@@ -24,7 +24,7 @@ const CLS_NAV_ICON_ACTIVE = "text-warning dark:text-warning";
|
|
|
24
24
|
function NavLink({ item, isActive, onClick }) {
|
|
25
25
|
return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end 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
|
|
26
26
|
? CLS_NAV_ACTIVE
|
|
27
|
-
: "text-[var(--appkit-color-text-muted)] hover:bg-
|
|
27
|
+
: "text-[var(--appkit-color-text-muted)] hover:bg-surface-hover 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: "truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
|
|
28
28
|
}
|
|
29
29
|
function FlatContent({ items, activeHref, storeName, storeLogoURL, onItemClick, }) {
|
|
30
30
|
const activeItem = findActiveNavItem(items, activeHref);
|
|
@@ -70,6 +70,26 @@ export interface SectionFormProps<T extends object = Record<string, JsonValue>>
|
|
|
70
70
|
*/
|
|
71
71
|
onCancel?: () => void;
|
|
72
72
|
cancelLabel?: string;
|
|
73
|
+
/**
|
|
74
|
+
* A destructive action for this record — almost always Delete.
|
|
75
|
+
*
|
|
76
|
+
* Added because an edit page nearly always has one and `SectionForm` had
|
|
77
|
+
* nowhere to put it, which left two bad options: hand-roll a row and pass
|
|
78
|
+
* `hideActions` (which ALSO silences the pinned mobile bar, since it is
|
|
79
|
+
* gated `bottomBar && !hideActions` — and that bar is the main reason to
|
|
80
|
+
* sectionise a short form), or render it outside the form, where every page
|
|
81
|
+
* invents its own placement.
|
|
82
|
+
*
|
|
83
|
+
* Rendered LEFT-aligned, opposite Save, so it is not adjacent to the
|
|
84
|
+
* primary action — a Delete beside Save is a misclick waiting to happen.
|
|
85
|
+
* `confirm` is expected to come from an ActionDef's `confirmation` config
|
|
86
|
+
* (Rule #7); this only carries the label and handler.
|
|
87
|
+
*/
|
|
88
|
+
destructiveAction?: {
|
|
89
|
+
label: string;
|
|
90
|
+
onClick: () => void;
|
|
91
|
+
disabled?: boolean;
|
|
92
|
+
};
|
|
73
93
|
/**
|
|
74
94
|
* Publish Save/Cancel and the error sheet into the mobile bottom-chrome
|
|
75
95
|
* tier. On by default: a SectionForm is a page-level form, which is exactly
|
|
@@ -109,5 +129,5 @@ export interface UseSectionFormNavResult<T extends object> {
|
|
|
109
129
|
export declare function useSectionFormNav<T extends object = Record<string, JsonValue>>(sections: SectionDef<T>[], values?: T): UseSectionFormNavResult<T>;
|
|
110
130
|
/** DOM id for a section panel — kept in one place so nav and render agree. */
|
|
111
131
|
export declare function sectionAnchorId(id: string): string;
|
|
112
|
-
export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
|
|
132
|
+
export declare function SectionForm<T extends object = Record<string, JsonValue>>({ sections, values, onChange, onSubmit, submitLabel, isLoading, schema, openIds: controlledOpenIds, onOpenChange, expandMode, hideActions, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar, }: SectionFormProps<T>): import("react").JSX.Element;
|
|
113
133
|
export default SectionForm;
|
|
@@ -4,7 +4,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "r
|
|
|
4
4
|
import { Button } from "../../ui/components/Button";
|
|
5
5
|
import { CollapsibleSection } from "../../ui/components/CollapsibleSection";
|
|
6
6
|
import { Badge } from "../../ui/components/Badge";
|
|
7
|
-
import { Row, Span, Stack } from "../../ui";
|
|
7
|
+
import { Div, Row, Span, Stack } from "../../ui";
|
|
8
8
|
import { FormShellContext } from "../../ui/forms/FormShell";
|
|
9
9
|
import { useFormBottomActions } from "../layout/hooks/useFormBottomActions";
|
|
10
10
|
import { FormSchemaContext } from "./FormShell";
|
|
@@ -55,7 +55,7 @@ export function useSectionFormNav(sections, values) {
|
|
|
55
55
|
export function sectionAnchorId(id) {
|
|
56
56
|
return `section-${id}`;
|
|
57
57
|
}
|
|
58
|
-
export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
|
|
58
|
+
export function SectionForm({ sections, values, onChange, onSubmit, submitLabel = "Save", isLoading = false, schema, openIds: controlledOpenIds, onOpenChange, expandMode = "multi", hideActions = false, destructiveAction, onValidationChange, onCancel, cancelLabel, bottomBar = true, }) {
|
|
59
59
|
const inheritedSchema = useContext(FormSchemaContext);
|
|
60
60
|
/*
|
|
61
61
|
* SectionForm deliberately does not OWN a FormShellContext (the owning view
|
|
@@ -124,6 +124,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
124
124
|
submitLabel,
|
|
125
125
|
cancelLabel,
|
|
126
126
|
isLoading,
|
|
127
|
+
destructiveAction,
|
|
127
128
|
enabled: bottomBar && !hideActions,
|
|
128
129
|
});
|
|
129
130
|
const toggle = useCallback((id) => {
|
|
@@ -149,6 +150,17 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
149
150
|
toggle(section.id); }, keepMounted: section.keepMounted, renderHeaderExtra: errorCount > 0
|
|
150
151
|
? () => (_jsxs(Badge, { variant: "danger", children: [errorCount, " ", errorCount === 1 ? "issue" : "issues"] }))
|
|
151
152
|
: undefined, children: section.render({ values, onChange: handleFieldChange, errors: fieldErrors }) }, section.id));
|
|
152
|
-
}), !hideActions && (
|
|
153
|
+
}), !hideActions && (
|
|
154
|
+
/*
|
|
155
|
+
* `justify="between"` so a destructive action sits opposite Save
|
|
156
|
+
* rather than beside it. With no destructive action the empty <Div>
|
|
157
|
+
* still holds the left edge, keeping Save right-aligned.
|
|
158
|
+
*
|
|
159
|
+
* Cancel is rendered HERE as well as in the mobile bar. It used to
|
|
160
|
+
* exist only in the bar, so every page converted to SectionForm
|
|
161
|
+
* silently lost its Cancel button on desktop — found converting
|
|
162
|
+
* `store/categories/new`, which had one before.
|
|
163
|
+
*/
|
|
164
|
+
_jsxs(Row, { justify: "between", align: "center", paddingY: "y-sm", gap: "sm", wrap: true, children: [_jsx(Div, { children: destructiveAction && (_jsx(Button, { variant: "danger", type: "button", onClick: destructiveAction.onClick, disabled: destructiveAction.disabled || isLoading, children: destructiveAction.label })) }), _jsxs(Row, { gap: "sm", children: [onCancel && (_jsx(Button, { variant: "ghost", type: "button", onClick: onCancel, disabled: isLoading, children: cancelLabel })), _jsx(Button, { variant: "primary", type: "submit", onClick: handleSubmit, isLoading: isLoading, disabled: isLoading, children: submitLabel })] })] }))] }));
|
|
153
165
|
}
|
|
154
166
|
export default SectionForm;
|
|
@@ -165,7 +165,16 @@ export function buildSectionsFromSchema(schema, opts = {}) {
|
|
|
165
165
|
// in-flight media upload. Inference sets this for every media field.
|
|
166
166
|
keepMounted: sectionFields.some((f) => f.meta.sectionKeepMounted),
|
|
167
167
|
fields: sectionFields.map((f) => f.name),
|
|
168
|
-
render: ({ values, onChange, errors }) =>
|
|
168
|
+
render: ({ values, onChange, errors }) => {
|
|
169
|
+
/*
|
|
170
|
+
* Filtered per render, not once at build time — the predicate reads
|
|
171
|
+
* the CURRENT values, so a field appears the moment its condition
|
|
172
|
+
* becomes true. Rows are packed after filtering so a hidden field
|
|
173
|
+
* does not leave a gap in a two-up row.
|
|
174
|
+
*/
|
|
175
|
+
const visible = sectionFields.filter((f) => !f.meta.when || f.meta.when(values));
|
|
176
|
+
return (_jsx(_Fragment, { children: packRows(visible).map((row, i) => (_jsx(FormGroup, { columns: ROW_COLUMNS[row[0].meta.row ?? "pair"], children: row.map((field) => renderField(field, { values, onChange, errors }, opts)) }, `${sectionId}-row-${i}`))) }));
|
|
177
|
+
},
|
|
169
178
|
};
|
|
170
179
|
});
|
|
171
180
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ZodTypeAny } from "zod";
|
|
2
|
+
import type { FormValues } from "../../schemas/types";
|
|
2
3
|
/** How much of a row a field claims on desktop. Everything is 1-up on mobile. */
|
|
3
4
|
export type FieldRow = "full" | "pair" | "third" | "quarter";
|
|
4
5
|
/** Which input primitive renders the field, and its default row span. */
|
|
@@ -29,6 +30,26 @@ export interface FieldUiMeta {
|
|
|
29
30
|
* which discards an in-flight media upload.
|
|
30
31
|
*/
|
|
31
32
|
sectionKeepMounted?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Render this field only when the predicate holds.
|
|
35
|
+
*
|
|
36
|
+
* `SectionDef` has had a section-level `when` since W0; this is the
|
|
37
|
+
* per-FIELD equivalent, and without it a whole class of form cannot be
|
|
38
|
+
* derived at all — a shipping config shows "Price per kg" for a flat rate,
|
|
39
|
+
* a payment method shows bank fields for a UPI account. Those pages were
|
|
40
|
+
* stuck hand-writing their fields.
|
|
41
|
+
*
|
|
42
|
+
* A predicate is a UI concern living on a schema that the ROUTE also parses.
|
|
43
|
+
* That is already true of every key here (`section`, `row`, `kind`), and the
|
|
44
|
+
* server ignores the registry entirely — annotations key a WeakMap, they are
|
|
45
|
+
* not part of the parsed shape.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 Hiding a field does NOT relax its schema. A required field hidden by
|
|
48
|
+
* `when` still fails validation, invisibly. Pair it with `.optional()` plus
|
|
49
|
+
* a `superRefine` that requires it under the same condition — the way
|
|
50
|
+
* `supportTicketCreateSchema` handles `orderId` for `order_issue`.
|
|
51
|
+
*/
|
|
52
|
+
when?: (values: FormValues) => boolean;
|
|
32
53
|
/** Row span. Drives the `<FormGroup columns>` packing. */
|
|
33
54
|
row?: FieldRow;
|
|
34
55
|
/** Control kind. Picks the primitive and the default row span. */
|
|
@@ -4,6 +4,8 @@ import React from "react";
|
|
|
4
4
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
5
|
import { Alert, Badge, Button, Div, FieldCheckbox, FieldInput, Grid, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
|
+
import { createShipmentItemSchema } from "../schemas/validation";
|
|
8
|
+
import { ValidationError } from "../../../errors/validation-error";
|
|
7
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
9
11
|
import { useShipmentItems } from "../hooks/useShipments";
|
|
@@ -42,12 +44,27 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
42
44
|
const [linkTarget, setLinkTarget] = React.useState(null);
|
|
43
45
|
const addItemMutation = useApiMutation({
|
|
44
46
|
successMessage: "Item added",
|
|
45
|
-
mutationFn: () =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
mutationFn: () => {
|
|
48
|
+
/*
|
|
49
|
+
* `createShipmentItemSchema` already validated this route and had no
|
|
50
|
+
* client consumer, so its rule — a projected sale price is required
|
|
51
|
+
* unless the item is for self use — only ever surfaced as a 400 that
|
|
52
|
+
* named no field. `Number(priceRupees)` also yields NaN on a typo,
|
|
53
|
+
* which the schema rejects and the old payload sent.
|
|
54
|
+
*/
|
|
55
|
+
const parsed = createShipmentItemSchema.safeParse({
|
|
56
|
+
title,
|
|
57
|
+
quantity: Number(quantity),
|
|
58
|
+
isForSelfUse,
|
|
59
|
+
price: isForSelfUse ? undefined : Math.round(Number(priceRupees) * 100) / 100,
|
|
60
|
+
});
|
|
61
|
+
if (!parsed.success) {
|
|
62
|
+
// ValidationError carries the issue list; a bare Error flattens it to
|
|
63
|
+
// one string and loses which field failed.
|
|
64
|
+
throw new ValidationError(parsed.error.issues[0]?.message ?? "Check the item details.", parsed.error.issues);
|
|
65
|
+
}
|
|
66
|
+
return apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEMS(shipmentId, lotId), parsed.data);
|
|
67
|
+
},
|
|
51
68
|
onSuccess: () => {
|
|
52
69
|
setTitle("");
|
|
53
70
|
setQuantity("1");
|
|
@@ -57,7 +57,7 @@ const CATEGORY_COLORS = {
|
|
|
57
57
|
"Messaging": "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300",
|
|
58
58
|
"Seller": "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
|
|
59
59
|
"Orders": "bg-lime-100 text-lime-800 dark:bg-lime-900/30 dark:text-lime-300",
|
|
60
|
-
"Account": "bg-[var(--appkit-color-surface)]
|
|
60
|
+
"Account": "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
|
|
61
61
|
};
|
|
62
62
|
const ALL_ACTION_IDS = Object.values(ACTION_ID);
|
|
63
63
|
export function ActionPermissionsManager({ initialConfig, onUpdate }) {
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S-STORE Extension Repositories
|
|
3
3
|
*
|
|
4
|
-
* 11 lightweight BaseRepository subclasses.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
4
|
+
* 11 lightweight BaseRepository subclasses.
|
|
5
|
+
*
|
|
6
|
+
* This header used to claim "none of these collections store PII directly" —
|
|
7
|
+
* that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
|
|
8
|
+
* `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
|
|
9
|
+
* and BaseRepository's write hooks encrypt them on every write path, so no
|
|
10
|
+
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
|
+
* described overrides that never existed).
|
|
12
|
+
*
|
|
13
|
+
* The other ten hold no PII and use the BaseRepository public API only.
|
|
8
14
|
*/
|
|
9
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
|
+
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
10
17
|
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
11
18
|
export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
|
|
19
|
+
/**
|
|
20
|
+
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
21
|
+
*
|
|
22
|
+
* This repository had no encryption at all. Nothing queries these by equality
|
|
23
|
+
* (`listByStore` filters on `storeId`), so no read path depended on their
|
|
24
|
+
* plaintext.
|
|
25
|
+
*/
|
|
26
|
+
protected piiFields: readonly ["upiVpa", "accountNumber", "ifscCode"];
|
|
12
27
|
constructor();
|
|
28
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
29
|
+
protected mapDoc<D = PayoutMethodDocument>(snap: DocumentSnapshot): D;
|
|
13
30
|
listByStore(storeId: string): Promise<{
|
|
14
31
|
items: PayoutMethodDocument[];
|
|
15
32
|
}>;
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S-STORE Extension Repositories
|
|
3
3
|
*
|
|
4
|
-
* 11 lightweight BaseRepository subclasses.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
4
|
+
* 11 lightweight BaseRepository subclasses.
|
|
5
|
+
*
|
|
6
|
+
* This header used to claim "none of these collections store PII directly" —
|
|
7
|
+
* that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
|
|
8
|
+
* `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
|
|
9
|
+
* and BaseRepository's write hooks encrypt them on every write path, so no
|
|
10
|
+
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
|
+
* described overrides that never existed).
|
|
12
|
+
*
|
|
13
|
+
* The other ten hold no PII and use the BaseRepository public API only.
|
|
8
14
|
*/
|
|
9
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
|
+
import { decryptPiiFields } from "../../../security/pii-encrypt";
|
|
10
17
|
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore";
|
|
11
18
|
export class PayoutMethodsRepository extends BaseRepository {
|
|
12
19
|
constructor() {
|
|
13
20
|
super(PAYOUT_METHODS_COLLECTION);
|
|
21
|
+
/**
|
|
22
|
+
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
23
|
+
*
|
|
24
|
+
* This repository had no encryption at all. Nothing queries these by equality
|
|
25
|
+
* (`listByStore` filters on `storeId`), so no read path depended on their
|
|
26
|
+
* plaintext.
|
|
27
|
+
*/
|
|
28
|
+
this.piiFields = ["upiVpa", "accountNumber", "ifscCode"];
|
|
29
|
+
}
|
|
30
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
31
|
+
mapDoc(snap) {
|
|
32
|
+
const raw = super.mapDoc(snap);
|
|
33
|
+
return decryptPiiFields(raw, [...this.piiFields]);
|
|
14
34
|
}
|
|
15
35
|
async listByStore(storeId) {
|
|
16
36
|
const items = await this.findBy("storeId", storeId);
|
|
@@ -66,17 +66,30 @@ export const shippingConfigFormSchema = z
|
|
|
66
66
|
method: annotate(shippingMethodSchema, {
|
|
67
67
|
section: "basics", quick: true, order: 2, row: "pair",
|
|
68
68
|
}),
|
|
69
|
+
/*
|
|
70
|
+
* Each rate field shows only for the method it belongs to. A flat-rate
|
|
71
|
+
* rule has no price-per-kg, and showing one invites a seller to fill in a
|
|
72
|
+
* number the checkout will never read.
|
|
73
|
+
*
|
|
74
|
+
* These pair with the `superRefine` below, which requires the field under
|
|
75
|
+
* the SAME condition — hiding a required field would otherwise fail
|
|
76
|
+
* validation invisibly.
|
|
77
|
+
*/
|
|
69
78
|
flatRate: annotate(amount("Flat rate").optional(), {
|
|
70
79
|
section: "rates", sectionLabel: "Rates", order: 1, row: "pair",
|
|
80
|
+
when: (v) => v.method === "flat",
|
|
71
81
|
}),
|
|
72
82
|
pricePerKg: annotate(amount("Price per kg").optional(), {
|
|
73
83
|
section: "rates", order: 2, row: "pair",
|
|
84
|
+
when: (v) => v.method === "weight",
|
|
74
85
|
}),
|
|
75
86
|
freeAbove: annotate(amount("Free above").optional(), {
|
|
76
87
|
section: "rates", order: 3, row: "pair",
|
|
88
|
+
when: (v) => v.method === "free",
|
|
77
89
|
}),
|
|
78
90
|
expressSurcharge: annotate(amount("Express surcharge").optional(), {
|
|
79
91
|
section: "rates", order: 4, row: "pair",
|
|
92
|
+
when: (v) => v.method === "express",
|
|
80
93
|
}),
|
|
81
94
|
estimatedDays: annotate(z.coerce.number().int("Estimated days must be a whole number.").min(0).max(365).optional(), { section: "rates", order: 5, row: "pair" }),
|
|
82
95
|
isDefault: annotate(z.boolean(), {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* parses this: `storeId` is set from the session, never from the body, so a
|
|
18
18
|
* caller cannot write a category into someone else's store by adding a field.
|
|
19
19
|
*
|
|
20
|
-
* `productIds`
|
|
20
|
+
* `productIds` is deliberately absent: it is managed after
|
|
21
21
|
* creation (assigning products, toggling visibility), not typed into the
|
|
22
22
|
* create form, and the repository supplies their defaults.
|
|
23
23
|
*
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Public and seller-facing read functions for store listings, products,
|
|
5
5
|
* auctions and reviews. No auth — callers decide whether auth is needed.
|
|
6
6
|
*/
|
|
7
|
+
import { cache } from "react";
|
|
7
8
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
8
9
|
import { NotFoundError } from "../../../errors";
|
|
9
10
|
import { filterTestDataForViewer, filterSingleTestData } from "../../../_internal/server/features/tester/visibility";
|
|
@@ -26,8 +27,31 @@ export async function listStores(params = {}, viewer) {
|
|
|
26
27
|
const result = await storeRepository.listStores(model);
|
|
27
28
|
return { ...result, items: filterTestDataForViewer(result.items, viewer) };
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Request-scoped memo of the raw store fetch.
|
|
32
|
+
*
|
|
33
|
+
* CLAUDE.md § SSR Architecture requires `React.cache` on any data-fetch shared
|
|
34
|
+
* by a page and its `generateMetadata`. `stores/[storeSlug]/layout.tsx` awaits
|
|
35
|
+
* `getStoreBySlug` in BOTH — so every store page view was doing two identical
|
|
36
|
+
* Firestore reads.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 The cache wraps the REPOSITORY call, not `getStoreBySlug`, and that is
|
|
39
|
+
* deliberate. `React.cache` memoises per argument list, and the two call sites
|
|
40
|
+
* pass different arguments — `getStoreBySlug(slug)` in `generateMetadata`,
|
|
41
|
+
* `getStoreBySlug(slug, viewer)` in the layout body. Wrapping the exported
|
|
42
|
+
* function would produce two cache entries and dedupe nothing.
|
|
43
|
+
*
|
|
44
|
+
* Making them match by reading the session in `generateMetadata` would be worse
|
|
45
|
+
* still: `getServerSessionUser()` is a dynamic API, and a dynamic API reached
|
|
46
|
+
* from metadata opts the route out of static rendering — the exact defect that
|
|
47
|
+
* made the whole site uncacheable (see `src/app/[locale]/layout.tsx`).
|
|
48
|
+
*
|
|
49
|
+
* So the read is cached on `storeSlug` alone and the viewer-specific
|
|
50
|
+
* `filterSingleTestData` is applied outside it, per caller.
|
|
51
|
+
*/
|
|
52
|
+
const fetchStoreBySlug = cache((storeSlug) => storeRepository.findBySlug(storeSlug));
|
|
29
53
|
export async function getStoreBySlug(storeSlug, viewer) {
|
|
30
|
-
const store = await
|
|
54
|
+
const store = await fetchStoreBySlug(storeSlug);
|
|
31
55
|
return filterSingleTestData(store, viewer);
|
|
32
56
|
}
|
|
33
57
|
export async function getStoreProducts(storeSlug, params = {}, viewer) {
|
|
@@ -25,5 +25,5 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
|
|
|
25
25
|
].join(" "), onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
|
|
26
26
|
e.preventDefault();
|
|
27
27
|
onSelect(store.id, !isSelected);
|
|
28
|
-
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-
|
|
28
|
+
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
|
|
29
29
|
}
|
|
@@ -8,7 +8,7 @@ import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge";
|
|
|
8
8
|
const __O = {
|
|
9
9
|
hidden: "overflow-hidden",
|
|
10
10
|
};
|
|
11
|
-
const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-
|
|
11
|
+
const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-[var(--appkit-color-border-subtle)] bg-warning-surface dark:bg-warning-surface flex items-center justify-center text-warning dark:text-warning font-bold text-[length:var(--appkit-text-2xl)] shadow-sm";
|
|
12
12
|
const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-warning";
|
|
13
13
|
const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
|
|
14
14
|
const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Fragment as _Fragment, jsx as _jsx
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useRouter } from "next/navigation";
|
|
4
|
-
import {
|
|
5
|
-
const CLS_ACTIVE_TAB = "border-primary text-primary";
|
|
4
|
+
import { TabBarButton, TabBarShell, TabsNavSelect, TextLink } from "../../../ui";
|
|
6
5
|
export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
|
|
7
6
|
const router = useRouter();
|
|
8
7
|
const handleChange = (v) => onTabChange?.(v);
|
|
@@ -18,9 +17,22 @@ export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "B
|
|
|
18
17
|
router.push(tab.href);
|
|
19
18
|
handleChange(tab.value);
|
|
20
19
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
/*
|
|
21
|
+
* The listing-type picker goes in `leading` — i.e. INSIDE the shell but
|
|
22
|
+
* OUTSIDE the role="tablist" element. It used to be a direct child of the
|
|
23
|
+
* tablist, which is invalid ARIA: a <select> is not a role="tab". Its old
|
|
24
|
+
* `max-w-[12rem] flex-shrink-0` sizing also went onto the inner <select>,
|
|
25
|
+
* the wrong element for sizing utilities (Root Cause #29); those now live
|
|
26
|
+
* on `.appkit-tabs-dropdown` in Tabs.style.css.
|
|
27
|
+
*/
|
|
28
|
+
const leading = dropdownTabs.length > 0 ? (_jsx(TabsNavSelect, { ariaLabel: dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, selected: Boolean(activeDropdownTab), onValueChange: handleDropdownChange })) : undefined;
|
|
29
|
+
/*
|
|
30
|
+
* Triggers carry `.appkit-tabs-trigger` and NOTHING else — no padding /
|
|
31
|
+
* size / weight props, no conditional colour classes. `important: true` is
|
|
32
|
+
* set in both tailwind configs, so any surviving utility would beat the
|
|
33
|
+
* component CSS unconditionally. The old inactive string also contained
|
|
34
|
+
* contradictory duplicates (`hover:text-neutral-800` immediately overridden
|
|
35
|
+
* by `hover:text-…text-muted`), so hover had been a no-op.
|
|
36
|
+
*/
|
|
37
|
+
return (_jsx(TabBarShell, { className: className, ariaLabel: "Store sections", leading: leading, activeKey: activeValue, children: tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, className: "appkit-tabs-trigger", children: tab.label }, tab.value)) : (_jsx(TabBarButton, { selected: activeValue === tab.value, onClick: () => handleChange(tab.value), children: tab.label }, tab.value))) }));
|
|
26
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
2
|
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
|
+
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
3
4
|
import { type SupportTicketDocument, type SupportTicketCreateInput, type SupportTicketUpdateInput, type TicketMessage, type TicketStatus } from "../schemas/firestore";
|
|
4
5
|
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
5
6
|
/** Who/why for a write that lands in `statusHistory`. */
|
|
@@ -11,7 +12,18 @@ export interface TicketWriteContext {
|
|
|
11
12
|
}
|
|
12
13
|
export declare class SupportRepository extends BaseRepository<SupportTicketDocument> {
|
|
13
14
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
15
|
+
/**
|
|
16
|
+
* Ticket PII, encrypted by BaseRepository's write hooks.
|
|
17
|
+
*
|
|
18
|
+
* `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
|
|
19
|
+
* history — the documents themselves were never encrypted. Safe to enable:
|
|
20
|
+
* nothing queries these by equality (the admin list searches `subject`), so
|
|
21
|
+
* no read path depends on their plaintext.
|
|
22
|
+
*/
|
|
23
|
+
protected piiFields: readonly ["userEmail", "userDisplayName", "assignedToName"];
|
|
14
24
|
constructor();
|
|
25
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
26
|
+
protected mapDoc<D = SupportTicketDocument>(snap: DocumentSnapshot): D;
|
|
15
27
|
createTicket(input: SupportTicketCreateInput): Promise<SupportTicketDocument>;
|
|
16
28
|
getUserTickets(userId: string, page?: number, pageSize?: number): Promise<FirebaseSieveResult<SupportTicketDocument>>;
|
|
17
29
|
getTicketById(ticketId: string): Promise<SupportTicketDocument | null>;
|
|
@@ -3,11 +3,26 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { DatabaseError } from "../../../errors";
|
|
5
5
|
import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names";
|
|
6
|
+
import { decryptPiiFields } from "../../../security/pii-encrypt";
|
|
6
7
|
import { SUPPORT_TICKET_COLLECTION, SUPPORT_TICKET_PII_FIELDS, SUPPORT_TICKET_TRACKED_FIELDS, TicketStatusValues, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore";
|
|
7
8
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
8
9
|
export class SupportRepository extends BaseRepository {
|
|
9
10
|
constructor() {
|
|
10
11
|
super(SUPPORT_TICKET_COLLECTION);
|
|
12
|
+
/**
|
|
13
|
+
* Ticket PII, encrypted by BaseRepository's write hooks.
|
|
14
|
+
*
|
|
15
|
+
* `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
|
|
16
|
+
* history — the documents themselves were never encrypted. Safe to enable:
|
|
17
|
+
* nothing queries these by equality (the admin list searches `subject`), so
|
|
18
|
+
* no read path depends on their plaintext.
|
|
19
|
+
*/
|
|
20
|
+
this.piiFields = SUPPORT_TICKET_PII_FIELDS;
|
|
21
|
+
}
|
|
22
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
23
|
+
mapDoc(snap) {
|
|
24
|
+
const raw = super.mapDoc(snap);
|
|
25
|
+
return decryptPiiFields(raw, [...SUPPORT_TICKET_PII_FIELDS]);
|
|
11
26
|
}
|
|
12
27
|
async createTicket(input) {
|
|
13
28
|
try {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type TicketCategory } from "./firestore";
|
|
3
|
+
/**
|
|
4
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
5
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
6
|
+
*/
|
|
7
|
+
export declare const supportTicketCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
8
|
+
category: z.ZodEnum<[TicketCategory, ...TicketCategory[]]>;
|
|
9
|
+
subject: z.ZodString;
|
|
10
|
+
description: z.ZodString;
|
|
11
|
+
orderId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
category: TicketCategory;
|
|
14
|
+
description: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
orderId?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
category: TicketCategory;
|
|
19
|
+
description: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
orderId?: string | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
category: TicketCategory;
|
|
24
|
+
description: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
orderId?: string | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
category: TicketCategory;
|
|
29
|
+
description: string;
|
|
30
|
+
subject: string;
|
|
31
|
+
orderId?: string | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export type SupportTicketCreateValues = z.infer<typeof supportTicketCreateSchema>;
|