@mohasinac/appkit 4.10.0 → 4.11.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/layout/DashboardLayoutClient.d.ts +6 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +1 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +2 -2
- package/dist/_internal/server/features/admin/daily-digest-actions.d.ts +18 -0
- package/dist/_internal/server/features/admin/daily-digest-actions.js +37 -0
- package/dist/_internal/server/features/audit-log/actions.d.ts +25 -0
- package/dist/_internal/server/features/audit-log/actions.js +38 -0
- package/dist/_internal/server/features/brands/actions.js +0 -2
- package/dist/_internal/server/features/brands/og.d.ts +4 -1
- package/dist/_internal/server/features/brands/og.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +4 -0
- package/dist/_internal/server/features/grouped/data.js +24 -0
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +9 -0
- package/dist/_internal/server/features/orders/adapters.js +25 -0
- package/dist/_internal/server/functions/scheduled.d.ts +3 -1
- package/dist/_internal/server/functions/scheduled.js +17 -1
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +40 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +164 -0
- package/dist/_internal/server/jobs/core/hardBanCascade.js +11 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +12 -0
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +85 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +16 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.js +4 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/index.js +3 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +27 -0
- package/dist/_internal/server/notifications/channel-health.js +101 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +0 -6
- package/dist/_internal/shared/features/brands/schema.js +0 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.js +3 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/client.d.ts +21 -6
- package/dist/client.js +13 -3
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +2 -3
- package/dist/contracts/client-auth.d.ts +3 -7
- package/dist/contracts/email.d.ts +1 -0
- package/dist/features/account/components/AddressForm.js +24 -2
- package/dist/features/account/components/BecomeSellerView.d.ts +1 -2
- package/dist/features/account/components/BecomeSellerView.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserBidsView.d.ts +1 -0
- package/dist/features/account/components/UserBidsView.js +47 -0
- package/dist/features/account/components/UserOrdersView.d.ts +4 -11
- package/dist/features/account/components/UserOrdersView.js +54 -4
- package/dist/features/account/components/UserReturnsView.d.ts +4 -9
- package/dist/features/account/components/UserReturnsView.js +37 -3
- package/dist/features/account/components/index.d.ts +3 -2
- package/dist/features/account/components/index.js +1 -0
- package/dist/features/admin/actions/admin-actions.js +22 -0
- package/dist/features/admin/actions/admin-coupon-actions.js +10 -0
- package/dist/features/admin/actions/notification-actions.js +4 -3
- package/dist/features/admin/components/AdminAdsView.d.ts +2 -3
- package/dist/features/admin/components/AdminAdsView.js +86 -89
- package/dist/features/admin/components/AdminAllEventEntriesView.js +18 -3
- package/dist/features/admin/components/AdminAuditLogView.d.ts +11 -0
- package/dist/features/admin/components/AdminAuditLogView.js +95 -0
- package/dist/features/admin/components/AdminBlogEditorView.js +37 -3
- package/dist/features/admin/components/AdminBrandEditorView.js +10 -6
- package/dist/features/admin/components/AdminBundleEditorView.js +18 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +2 -2
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +2 -2
- package/dist/features/admin/components/AdminCartsView.js +34 -4
- package/dist/features/admin/components/AdminCategoryEditorView.js +5 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
- package/dist/features/admin/components/AdminHistoryView.js +11 -1
- package/dist/features/admin/components/AdminNotificationsView.js +27 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +10 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +92 -24
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.d.ts +7 -0
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +47 -0
- package/dist/features/admin/components/AdminPayoutsView.js +15 -31
- package/dist/features/admin/components/AdminProductEditorView.js +26 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +22 -2
- package/dist/features/admin/components/AdminStoresView.js +7 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -2
- package/dist/features/admin/components/AdminUserEditorView.d.ts +2 -2
- package/dist/features/admin/components/AdminUserEditorView.js +4 -2
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/AdminWishlistsView.js +14 -1
- package/dist/features/admin/components/DataListingView.d.ts +2 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.d.ts +19 -0
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +22 -0
- package/dist/features/admin/components/ViewNotificationModal.d.ts +21 -0
- package/dist/features/admin/components/ViewNotificationModal.js +15 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +4 -7
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +56 -36
- package/dist/features/admin/components/index.d.ts +8 -1
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +49 -0
- package/dist/features/admin/constants/filter-tabs.js +27 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +6 -0
- package/dist/features/admin/hooks/useAdminListing.js +3 -1
- package/dist/features/admin/hooks/useAdminListingData.d.ts +9 -1
- package/dist/features/admin/hooks/useAdminListingData.js +9 -2
- package/dist/features/admin/repository/site-settings.repository.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.d.ts +12 -0
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +8 -0
- package/dist/features/auctions/schemas/index.d.ts +5 -0
- package/dist/features/audit-log/repository/audit-log.repository.d.ts +45 -0
- package/dist/features/audit-log/repository/audit-log.repository.js +30 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +59 -0
- package/dist/features/audit-log/schemas/firestore.js +35 -0
- package/dist/features/auth/actions/index.d.ts +0 -1
- package/dist/features/auth/actions/index.js +0 -1
- package/dist/features/auth/hooks/useAuth.d.ts +8 -35
- package/dist/features/auth/hooks/useAuth.js +21 -37
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +2 -0
- package/dist/features/blog/schemas/index.d.ts +32 -0
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +31 -3
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +2 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +7 -3
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.d.ts +14 -0
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.js +22 -0
- package/dist/features/categories/components/CategoryProductsView.d.ts +1 -5
- package/dist/features/categories/components/CategoryProductsView.js +1 -1
- package/dist/features/categories/components/CategorySelectorCreate.js +16 -2
- package/dist/features/categories/schemas/firestore.d.ts +19 -2
- package/dist/features/categories/schemas/index.d.ts +24 -3
- package/dist/features/categories/schemas/index.js +3 -1
- package/dist/features/categories/types/index.d.ts +7 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +17 -3
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +101 -22
- package/dist/features/contact/components/ContactForm.js +17 -4
- package/dist/features/contact/email.d.ts +1 -0
- package/dist/features/contact/email.js +3 -0
- package/dist/features/events/components/AdminEventEditorView.js +54 -2
- package/dist/features/events/components/AdminEventEntriesView.d.ts +2 -16
- package/dist/features/events/components/AdminEventEntriesView.js +56 -89
- package/dist/features/events/components/EventFormDrawer.js +5 -1
- package/dist/features/events/components/EventRaffleEntryForm.d.ts +1 -3
- package/dist/features/events/components/EventRaffleEntryForm.js +1 -1
- package/dist/features/events/schemas/index.d.ts +22 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +4 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +18 -0
- package/dist/features/homepage/components/CharacterHotspotForm.js +202 -187
- package/dist/features/homepage/components/SectionCarousel.d.ts +1 -7
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/media/MediaImage.js +39 -3
- package/dist/features/media/types/index.d.ts +10 -0
- package/dist/features/media/types/index.js +1 -0
- package/dist/features/media/upload/MediaUploadField.d.ts +7 -1
- package/dist/features/media/upload/MediaUploadField.js +11 -7
- package/dist/features/media/upload/video-poster.d.ts +2 -0
- package/dist/features/media/upload/video-poster.js +16 -1
- package/dist/features/media/upload/video-poster.test.js +1 -1
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +12 -0
- package/dist/features/orders/components/OrderStatusTimeline.js +55 -0
- package/dist/features/orders/components/index.d.ts +2 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/constants/payment-window.d.ts +25 -0
- package/dist/features/orders/constants/payment-window.js +25 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +56 -0
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +33 -0
- package/dist/features/pre-orders/schemas/index.d.ts +5 -0
- package/dist/features/products/components/BidHistory.js +9 -1
- package/dist/features/products/components/GroupSettingsPanel.js +25 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -4
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailActions.d.ts +1 -2
- package/dist/features/products/components/ProductDetailActions.js +1 -1
- package/dist/features/products/components/ProductGrid.js +15 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/schemas/index.d.ts +5 -0
- package/dist/features/promotions/components/CouponsIndexListing.d.ts +1 -3
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/search/components/SearchView.d.ts +1 -3
- package/dist/features/search/components/SearchView.js +1 -1
- package/dist/features/seller/components/QuickProductForm.js +2 -1
- package/dist/features/seller/components/SellerBidsView.js +66 -107
- package/dist/features/seller/components/SellerGoogleReviewsView.js +17 -1
- package/dist/features/seller/components/SellerOrdersView.d.ts +15 -3
- package/dist/features/seller/components/SellerOrdersView.js +134 -163
- package/dist/features/seller/components/SellerPayoutMethodsView.js +60 -48
- package/dist/features/seller/components/SellerPayoutSettingsView.js +69 -4
- package/dist/features/seller/components/SellerPayoutsView.d.ts +12 -0
- package/dist/features/seller/components/SellerPayoutsView.js +25 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +10 -3
- package/dist/features/seller/components/SellerProductShell.js +89 -36
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +2 -7
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +4 -4
- package/dist/features/seller/components/SellerProductsView.d.ts +2 -4
- package/dist/features/seller/components/SellerProductsView.js +131 -189
- package/dist/features/seller/components/SellerReviewsView.js +95 -133
- package/dist/features/seller/components/SellerShippingConfigsView.js +47 -67
- package/dist/features/seller/components/SellerShippingView.js +41 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +51 -59
- package/dist/features/seller/components/SellerStorefrontView.d.ts +6 -1
- package/dist/features/seller/components/SellerStorefrontView.js +42 -5
- package/dist/features/seller/components/index.d.ts +2 -2
- package/dist/features/seller/components/index.js +2 -2
- package/dist/features/shell/FormShell.d.ts +24 -4
- package/dist/features/shell/FormShell.js +12 -6
- package/dist/features/shell/QuickFormDrawer.js +67 -12
- package/dist/features/shell/StepForm.d.ts +30 -1
- package/dist/features/shell/StepForm.js +47 -4
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +1 -1
- package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -2
- package/dist/features/shipments/schemas/validation.d.ts +14 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +23 -2
- package/dist/features/tester/components/TesterChecklistStepRow.js +1 -1
- package/dist/features/tester/components/TesterHubView.js +3 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +9 -1
- package/dist/features/tester/seed-data/products-tester-seed-data.js +43 -32
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +404 -15
- package/dist/index.d.ts +15 -3
- package/dist/index.js +22 -5
- package/dist/next/routing/route-map.d.ts +11 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/email-resend/provider.js +1 -0
- package/dist/providers/firebase-client/auth.d.ts +1 -0
- package/dist/providers/firebase-client/auth.js +7 -1
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +61 -25
- package/dist/security/pii-mask.js +1 -1
- package/dist/seed/categories-seed-data.js +69 -0
- package/dist/seed/products-auctions-seed-data.js +11 -0
- package/dist/seed/products-standard-seed-data.js +93 -4
- package/dist/seed/site-settings-seed-data.js +13 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +20 -2
- package/dist/server.d.ts +7 -2
- package/dist/server.js +8 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Form.d.ts +11 -2
- package/dist/ui/components/Form.js +4 -3
- package/dist/ui/components/FormField.d.ts +3 -1
- package/dist/ui/components/FormField.js +22 -4
- package/dist/ui/components/ListingLayout.d.ts +1 -2
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +68 -0
- package/dist/ui/components/RecordDetailModal.js +8 -0
- package/dist/ui/components/SlottedListingView.js +9 -1
- package/dist/ui/forms/FormErrorSummary.d.ts +25 -0
- package/dist/ui/forms/FormErrorSummary.js +37 -0
- package/dist/ui/forms/FormShell.d.ts +37 -2
- package/dist/ui/forms/FormShell.js +37 -7
- package/dist/ui/forms/FormShell.style.css +17 -0
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +5 -0
- package/dist/utils/media-url.d.ts +3 -1
- package/dist/utils/media-url.js +20 -1
- package/dist/validation/schemas.d.ts +4 -4
- package/package.json +1 -1
- package/dist/_internal/client/features/classified/ClassifiedDetailView.d.ts +0 -9
- package/dist/_internal/client/features/classified/ClassifiedDetailView.js +0 -55
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.d.ts +0 -18
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.js +0 -21
- package/dist/_internal/client/features/live/LiveItemDetailView.d.ts +0 -9
- package/dist/_internal/client/features/live/LiveItemDetailView.js +0 -25
- package/dist/_internal/server/jobs/core/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +0 -28
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +0 -61
- package/dist/_internal/server/jobs/core/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +0 -68
- package/dist/_internal/server/jobs/core/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +0 -49
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +0 -2
- package/dist/features/auth/consent-otp.d.ts +0 -57
- package/dist/features/auth/consent-otp.js +0 -127
- package/dist/features/cart/components/CheckoutOtpModal.d.ts +0 -15
- package/dist/features/cart/components/CheckoutOtpModal.js +0 -12
- package/dist/features/checkout/actions/checkout-actions.d.ts +0 -31
- package/dist/features/checkout/actions/checkout-actions.js +0 -124
- package/dist/features/classified/components/ClassifiedIndexListing.d.ts +0 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +0 -123
- package/dist/features/classified/components/ClassifiedListView.d.ts +0 -6
- package/dist/features/classified/components/ClassifiedListView.js +0 -44
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +0 -5
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +0 -114
- package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +0 -6
- package/dist/features/digital-codes/components/DigitalCodesListView.js +0 -38
- package/dist/features/live/components/LiveItemsIndexListing.d.ts +0 -5
- package/dist/features/live/components/LiveItemsIndexListing.js +0 -115
- package/dist/features/live/components/LiveItemsListView.d.ts +0 -6
- package/dist/features/live/components/LiveItemsListView.js +0 -44
- package/dist/features/seller/components/SellerTemplatesView.d.ts +0 -12
- package/dist/features/seller/components/SellerTemplatesView.js +0 -276
- package/dist/react/hooks/useVisibleItems.d.ts +0 -37
- package/dist/react/hooks/useVisibleItems.js +0 -59
- package/dist/ui/components/TabStrip.d.ts +0 -34
- package/dist/ui/components/TabStrip.js +0 -56
- package/dist/ui/components/TabStrip.style.css +0 -97
|
@@ -10,10 +10,58 @@ import { QuickEditMenu } from "./QuickEditMenu";
|
|
|
10
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
12
12
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
13
|
-
import { ADMIN_ORDER_STATUS_TABS } from "../constants/filter-tabs";
|
|
13
|
+
import { ADMIN_ORDER_STATUS_TABS, ADMIN_ORDER_PAYMENT_REVIEW_TABS } from "../constants/filter-tabs";
|
|
14
|
+
import { isManualPaymentMethod, isPaymentReviewQueueMode } from "../../orders/constants/payment-window";
|
|
14
15
|
import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../hooks/useAdminListingData";
|
|
15
16
|
import { DataListingView } from "./DataListingView";
|
|
16
17
|
import { AdminOrderEditorView } from "./AdminOrderEditorView";
|
|
18
|
+
/**
|
|
19
|
+
* `OrderDocument` has no `orderNumber` field — the real item info an order
|
|
20
|
+
* list row should show lives in the denormalized `items[]` snapshot
|
|
21
|
+
* (`productTitle`/`image`), written at order time specifically so list/detail
|
|
22
|
+
* UI never needs an extra product fetch. Falling back to `Order {id}` (a raw
|
|
23
|
+
* GUID) instead was Root Cause Pattern #(order-guid) — see CLAUDE.md.
|
|
24
|
+
*/
|
|
25
|
+
function toOrderItemRows(item) {
|
|
26
|
+
const items = Array.isArray(item.items) ? item.items : [];
|
|
27
|
+
return items
|
|
28
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
29
|
+
.map((entry) => ({
|
|
30
|
+
productId: typeof entry.productId === "string" ? entry.productId : "",
|
|
31
|
+
title: typeof entry.productTitle === "string" ? entry.productTitle : "Item",
|
|
32
|
+
image: typeof entry.image === "string" ? entry.image : undefined,
|
|
33
|
+
quantity: typeof entry.quantity === "number" ? entry.quantity : 1,
|
|
34
|
+
unitPrice: typeof entry.unitPrice === "number" ? entry.unitPrice : 0,
|
|
35
|
+
totalPrice: typeof entry.totalPrice === "number" ? entry.totalPrice : 0,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
function firstOrderItem(item) {
|
|
39
|
+
const items = toOrderItemRows(item);
|
|
40
|
+
return {
|
|
41
|
+
image: items[0]?.image,
|
|
42
|
+
title: items[0]?.title,
|
|
43
|
+
extraCount: Math.max(0, items.length - 1),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Short marker appended to a row's secondary line so a manual payment needing
|
|
48
|
+
* action is visible in the list itself, not only after opening the drawer.
|
|
49
|
+
* Mirrors the same `paymentProofUrl` / `paymentReviewOutcome` derivation the
|
|
50
|
+
* server-side queue uses (`listPaymentReviewQueue`) — kept in sync by reading
|
|
51
|
+
* the same two fields rather than a denormalised flag (Root Cause #42).
|
|
52
|
+
*/
|
|
53
|
+
function paymentReviewLabel(item) {
|
|
54
|
+
if (!isManualPaymentMethod(toStringValue(item.paymentMethod, "")))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (toStringValue(item.paymentStatus, "") === "paid")
|
|
57
|
+
return "Payment verified";
|
|
58
|
+
const outcome = toStringValue(item.paymentReviewOutcome, "");
|
|
59
|
+
if (outcome === "rejected_fraud")
|
|
60
|
+
return "Payment rejected";
|
|
61
|
+
if (outcome === "reupload_requested")
|
|
62
|
+
return "Re-upload requested";
|
|
63
|
+
return item.paymentProofUrl ? "Awaiting verification" : "Awaiting payment";
|
|
64
|
+
}
|
|
17
65
|
export function AdminOrdersView({ children, ...props }) {
|
|
18
66
|
const { showToast } = useToast();
|
|
19
67
|
const router = useRouter();
|
|
@@ -37,7 +85,7 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
37
85
|
title: "Orders",
|
|
38
86
|
searchPlaceholder: "Search orders, buyers, or tracking IDs",
|
|
39
87
|
emptyLabel: "No orders found",
|
|
40
|
-
filterKeys: ["status"],
|
|
88
|
+
filterKeys: ["status", "paymentReview"],
|
|
41
89
|
defaultSort: sortBy("createdAt", "DESC"),
|
|
42
90
|
queryKey: ["admin", "orders", "listing"],
|
|
43
91
|
endpoint: ADMIN_ENDPOINTS.ORDERS,
|
|
@@ -45,27 +93,47 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
45
93
|
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
46
94
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
47
95
|
],
|
|
48
|
-
mapRows: (response) => toRecordArray(response.orders).map((item, index) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
mapRows: (response) => toRecordArray(response.orders).map((item, index) => {
|
|
97
|
+
const orderId = toStringValue(item.id, `order-${index}`);
|
|
98
|
+
const { image, title, extraCount } = firstOrderItem(item);
|
|
99
|
+
return {
|
|
100
|
+
id: orderId,
|
|
101
|
+
primary: title
|
|
102
|
+
? `${title}${extraCount > 0 ? ` +${extraCount} more` : ""}`
|
|
103
|
+
: `Order ${orderId}`,
|
|
104
|
+
secondary: [
|
|
105
|
+
toStringValue(item.buyerName ?? item.customerName, "Unknown buyer"),
|
|
106
|
+
toCurrency(item.totalAmount ?? item.total ?? item.amount),
|
|
107
|
+
orderId.slice(0, 14),
|
|
108
|
+
paymentReviewLabel(item),
|
|
109
|
+
]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(" · "),
|
|
112
|
+
status: toStringValue(item.status, "Unknown"),
|
|
113
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
114
|
+
image,
|
|
115
|
+
items: toOrderItemRows(item),
|
|
116
|
+
paymentProofUrl: toStringValue(item.paymentProofUrl, "") || undefined,
|
|
117
|
+
paymentTransactionId: toStringValue(item.paymentTransactionId, "") || undefined,
|
|
118
|
+
paymentMethod: toStringValue(item.paymentMethod, "") || undefined,
|
|
119
|
+
paymentStatus: toStringValue(item.paymentStatus, "") || undefined,
|
|
120
|
+
displayedUpiId: toStringValue(item.displayedUpiId, "") || undefined,
|
|
121
|
+
buyerReportedUpiId: toStringValue(item.buyerReportedUpiId, "") || undefined,
|
|
122
|
+
paymentUpiMismatch: Boolean(item.paymentUpiMismatch),
|
|
123
|
+
buyerMarkedPaid: Boolean(item.buyerMarkedPaid),
|
|
124
|
+
buyerFraudAgreementAccepted: Boolean(item.buyerFraudAgreementAccepted),
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
67
127
|
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
68
|
-
|
|
128
|
+
// The payment-review queue is always `status == pending` by definition, so
|
|
129
|
+
// a status chip on top of it could only ever narrow to zero rows. When the
|
|
130
|
+
// queue is active the status filter is dropped rather than combined.
|
|
131
|
+
buildFilters: (f) => {
|
|
132
|
+
if (isPaymentReviewQueueMode(f.paymentReview ?? ""))
|
|
133
|
+
return undefined;
|
|
134
|
+
return f.status && f.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, f.status) : undefined;
|
|
135
|
+
},
|
|
136
|
+
buildExtraParams: (f) => isPaymentReviewQueueMode(f.paymentReview ?? "") ? { paymentReview: f.paymentReview } : undefined,
|
|
69
137
|
// Mirrors the row action's "View full details" entry so the row itself
|
|
70
138
|
// is clickable, not just the overflow menu.
|
|
71
139
|
onRowClick: (row) => {
|
|
@@ -128,7 +196,7 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
128
196
|
submitLabel: "Update",
|
|
129
197
|
},
|
|
130
198
|
] })),
|
|
131
|
-
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
|
|
199
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id, paymentReview: "" })) }), _jsx(FilterChipGroup, { label: "Manual payment", tabs: ADMIN_ORDER_PAYMENT_REVIEW_TABS, value: pendingFilters.paymentReview ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, paymentReview: id, status: "" })) })] })),
|
|
132
200
|
};
|
|
133
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted })] }));
|
|
201
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted })] }));
|
|
134
202
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface AdminPayoutMarkPaidModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
payoutId: string | null;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
onSuccess?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function AdminPayoutMarkPaidModal({ isOpen, payoutId, onClose, onSuccess }: AdminPayoutMarkPaidModalProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* AdminPayoutMarkPaidModal — the single "mark payout as paid" form + mutation,
|
|
5
|
+
* shared by the payouts list row action and the standalone payout detail
|
|
6
|
+
* page so the two surfaces can't drift (same class of fix as
|
|
7
|
+
* SellerOrderDetailPanel for orders — see CLAUDE.md Root Cause list).
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { Button, Form, FormActions, Input, Modal, useToast } from "../../../ui";
|
|
13
|
+
import { apiClient } from "../../../http";
|
|
14
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
15
|
+
export function AdminPayoutMarkPaidModal({ isOpen, payoutId, onClose, onSuccess }) {
|
|
16
|
+
const [transactionId, setTransactionId] = useState("");
|
|
17
|
+
const { showToast } = useToast();
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
const markPaid = useApiMutation({
|
|
20
|
+
mutationFn: () => {
|
|
21
|
+
if (!payoutId)
|
|
22
|
+
throw new Error("No payout selected");
|
|
23
|
+
return apiClient.patch(ADMIN_ENDPOINTS.PAYOUT_BY_ID(payoutId), {
|
|
24
|
+
status: "paid",
|
|
25
|
+
transactionId: transactionId.trim() || undefined,
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
onSuccess: () => {
|
|
29
|
+
showToast("Payout marked as paid.", "success");
|
|
30
|
+
setTransactionId("");
|
|
31
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "payouts", "listing"] });
|
|
32
|
+
onSuccess?.();
|
|
33
|
+
onClose();
|
|
34
|
+
},
|
|
35
|
+
onError: () => {
|
|
36
|
+
showToast("Failed to update payout.", "error");
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const handleClose = () => {
|
|
40
|
+
setTransactionId("");
|
|
41
|
+
onClose();
|
|
42
|
+
};
|
|
43
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: handleClose, title: "Mark payout as paid", children: _jsxs(Form, { onSubmit: (e) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
markPaid.mutate();
|
|
46
|
+
}, spacing: "md", children: [_jsx(Input, { label: "Transaction / reference ID", value: transactionId, onChange: (e) => setTransactionId(e.target.value), placeholder: "UTR, UPI ref, or bank transfer ID (optional)" }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "outline", onClick: handleClose, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "primary", disabled: markPaid.isPending, children: markPaid.isPending ? "Saving..." : "Confirm paid" })] })] }) }));
|
|
47
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
-
import {
|
|
4
|
+
import { useBulkEvent, RTDB_PATHS } from "@mohasinac/appkit/client";
|
|
5
5
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
|
|
6
6
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
7
7
|
import React, { useState, useEffect } from "react";
|
|
8
8
|
import { useQueryClient } from "@tanstack/react-query";
|
|
9
|
-
import { Button, FilterChipGroup,
|
|
9
|
+
import { Button, FilterChipGroup, ListingLayout, RowActionMenu, useToast, } from "../../../ui";
|
|
10
|
+
import { AdminPayoutMarkPaidModal } from "./AdminPayoutMarkPaidModal";
|
|
11
|
+
import { useRouter } from "next/navigation";
|
|
10
12
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
11
14
|
import { ADMIN_PAYOUT_STATUS_TABS } from "../constants/filter-tabs";
|
|
12
15
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
13
16
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
|
|
@@ -15,9 +18,9 @@ import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../ho
|
|
|
15
18
|
import { DataListingView } from "./DataListingView";
|
|
16
19
|
import { apiClient } from "../../../http";
|
|
17
20
|
export function AdminPayoutsView({ children, ...props }) {
|
|
21
|
+
const router = useRouter();
|
|
18
22
|
const [markPaidOpen, setMarkPaidOpen] = useState(false);
|
|
19
23
|
const [selectedPayoutId, setSelectedPayoutId] = useState(null);
|
|
20
|
-
const [transactionId, setTransactionId] = useState("");
|
|
21
24
|
const [calculating, setCalculating] = useState(false);
|
|
22
25
|
const toast = useToast();
|
|
23
26
|
const queryClient = useQueryClient();
|
|
@@ -57,26 +60,6 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
57
60
|
}
|
|
58
61
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
62
|
}, [bulkEvent.status]);
|
|
60
|
-
const markPaid = useApiMutation({
|
|
61
|
-
mutationFn: () => {
|
|
62
|
-
if (!selectedPayoutId)
|
|
63
|
-
throw new Error("No payout selected");
|
|
64
|
-
return apiClient.patch(ADMIN_ENDPOINTS.PAYOUT_BY_ID(selectedPayoutId), {
|
|
65
|
-
status: "paid",
|
|
66
|
-
transactionId: transactionId.trim() || undefined,
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
onSuccess: () => {
|
|
70
|
-
toast.showToast("Payout marked as paid.", "success");
|
|
71
|
-
setMarkPaidOpen(false);
|
|
72
|
-
setSelectedPayoutId(null);
|
|
73
|
-
setTransactionId("");
|
|
74
|
-
void queryClient.invalidateQueries({ queryKey: ["admin", "payouts", "listing"] });
|
|
75
|
-
},
|
|
76
|
-
onError: () => {
|
|
77
|
-
toast.showToast("Failed to update payout.", "error");
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
63
|
const handleExportCsv = async () => {
|
|
81
64
|
try {
|
|
82
65
|
const blob = await apiClient.blob(ADMIN_ENDPOINTS.PAYOUTS_EXPORT);
|
|
@@ -92,10 +75,6 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
92
75
|
toast.showToast("CSV export failed.", "error");
|
|
93
76
|
}
|
|
94
77
|
};
|
|
95
|
-
const closePaidModal = () => {
|
|
96
|
-
setMarkPaidOpen(false);
|
|
97
|
-
setTransactionId("");
|
|
98
|
-
};
|
|
99
78
|
if (React.Children.count(children) > 0) {
|
|
100
79
|
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
101
80
|
}
|
|
@@ -121,6 +100,7 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
121
100
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
122
101
|
})),
|
|
123
102
|
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
103
|
+
onRowClick: (row) => router.push(String(ROUTES.ADMIN.PAYOUT_DETAIL(row.id))),
|
|
124
104
|
buildFilters: (state) => {
|
|
125
105
|
// An explicit status chip always wins. Otherwise, hide the noise
|
|
126
106
|
// (already-paid payouts) by default — mirrors the isSold-hide-by-
|
|
@@ -143,6 +123,10 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
143
123
|
onClick: () => selection.clearSelection(),
|
|
144
124
|
})),
|
|
145
125
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
126
|
+
{
|
|
127
|
+
label: "View full page",
|
|
128
|
+
onClick: () => router.push(String(ROUTES.ADMIN.PAYOUT_DETAIL(row.id))),
|
|
129
|
+
},
|
|
146
130
|
{
|
|
147
131
|
label: ACTIONS.ADMIN["grant-payout"].label,
|
|
148
132
|
onClick: () => {
|
|
@@ -157,8 +141,8 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
157
141
|
{ id: "true", label: "Show all" },
|
|
158
142
|
], value: pendingFilters.showAllPayouts ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, showAllPayouts: id })), allId: "" })] })),
|
|
159
143
|
};
|
|
160
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
144
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminPayoutMarkPaidModal, { isOpen: markPaidOpen, payoutId: selectedPayoutId, onClose: () => {
|
|
145
|
+
setMarkPaidOpen(false);
|
|
146
|
+
setSelectedPayoutId(null);
|
|
147
|
+
} })] }));
|
|
164
148
|
}
|
|
@@ -6,8 +6,21 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
import { Row } from "@mohasinac/appkit/ui";
|
|
7
7
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
8
8
|
import React from "react";
|
|
9
|
+
import { z } from "zod";
|
|
9
10
|
import { useQuery } from "@tanstack/react-query";
|
|
10
11
|
import { Button, Card, CardBody, ConfirmDeleteModal, Div, PaginatedSelect, Form, Stack, StackedViewShell, Tabs, TabsContent, TabsList, TabsTrigger, Text, useToast, Show, StickyToolbar, } from "../../../ui";
|
|
12
|
+
import { useFormShellState, applyZodIssues, FormErrorSummary, FormShellContext } from "../../../ui/forms";
|
|
13
|
+
// Admin form-input validation — deliberately a small local schema, not the
|
|
14
|
+
// broader `productItemSchema` (that one requires `id`/`price`/`status` as
|
|
15
|
+
// non-optional, since it's the full read/display shape used across many
|
|
16
|
+
// features — applying it directly here would show false "required" errors
|
|
17
|
+
// on every keystroke while creating a new product, before an id exists).
|
|
18
|
+
const adminProductFormSchema = z.object({
|
|
19
|
+
title: z.string().min(1, "Product title is required"),
|
|
20
|
+
price: z.number().min(0).optional(),
|
|
21
|
+
images: z.array(z.string()).max(5, "Up to 5 gallery images allowed").optional(),
|
|
22
|
+
video: z.object({ url: z.string(), thumbnailUrl: z.string().optional() }).optional(),
|
|
23
|
+
}).passthrough();
|
|
11
24
|
import { apiClient } from "../../../http";
|
|
12
25
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
26
|
import { ProductForm } from "../../products/components/ProductForm";
|
|
@@ -106,6 +119,12 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
|
|
|
106
119
|
const [mode, setMode] = React.useState("standard");
|
|
107
120
|
const [deleteOpen, setDeleteOpen] = React.useState(false);
|
|
108
121
|
const { showToast } = useToast();
|
|
122
|
+
const { shellCtx, setFieldError, validate } = useFormShellState(adminProductFormSchema);
|
|
123
|
+
// Live validation — re-run on every product change, not just on submit.
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
validate(product);
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
}, [product, validate]);
|
|
109
128
|
const productQuery = useQuery({
|
|
110
129
|
queryKey: ["admin", "product", productId],
|
|
111
130
|
queryFn: async () => {
|
|
@@ -153,6 +172,10 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
|
|
|
153
172
|
onSaved(id);
|
|
154
173
|
},
|
|
155
174
|
onError: (err) => {
|
|
175
|
+
const issues = err?.issues;
|
|
176
|
+
if (issues && issues.length > 0) {
|
|
177
|
+
applyZodIssues(issues, setFieldError);
|
|
178
|
+
}
|
|
156
179
|
showToast(err?.message ?? "Failed to save product.", "error");
|
|
157
180
|
},
|
|
158
181
|
});
|
|
@@ -168,13 +191,13 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
|
|
|
168
191
|
},
|
|
169
192
|
});
|
|
170
193
|
const isSubmitting = saveMutation.isPending || productQuery.isLoading;
|
|
171
|
-
const actionSidebar = (_jsxs(Card, { variant: "outlined", padding: "md", spacing: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: isEdit ? product.status ?? "—" : "New" }), _jsx(Button, { type: "submit", form: "product-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] }));
|
|
172
|
-
const formContent = (_jsxs(Form, { id: "product-editor-form", onSubmit: (e) => {
|
|
194
|
+
const actionSidebar = (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Card, { variant: "outlined", padding: "md", spacing: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: isEdit ? product.status ?? "—" : "New" }), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "submit", form: "product-editor-form", className: "w-full", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", className: "w-full", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] }) }));
|
|
195
|
+
const formContent = (_jsxs(Form, { id: "product-editor-form", shellCtx: shellCtx, onSubmit: (e) => {
|
|
173
196
|
e.preventDefault();
|
|
174
197
|
saveMutation.mutate();
|
|
175
198
|
}, spacing: "lg", children: [_jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Listing Type" }), _jsxs(Tabs, { value: mode, onChange: handleModeChange, children: [_jsxs(TabsList, { children: [(!enabledListingTypes || enabledListingTypes.includes("standard")) && (_jsx(TabsTrigger, { value: "standard", children: "Standard" })), (!enabledListingTypes || enabledListingTypes.includes("auction")) && (_jsx(TabsTrigger, { value: "auction", children: "Auction" })), (!enabledListingTypes || enabledListingTypes.includes("pre-order")) && (_jsx(TabsTrigger, { value: "preorder", children: "Pre-order" })), (!enabledListingTypes || enabledListingTypes.includes("prize-draw")) && (_jsx(TabsTrigger, { value: "prize-draw", children: "Prize Draw" })), (!enabledListingTypes || enabledListingTypes.includes("classified")) && (_jsx(TabsTrigger, { value: "classified", children: "Classified" })), (!enabledListingTypes || enabledListingTypes.includes("digital-code")) && (_jsx(TabsTrigger, { value: "digital-code", children: "Digital Code" })), (!enabledListingTypes || enabledListingTypes.includes("live")) && (_jsx(TabsTrigger, { value: "live", children: "Live Item" }))] }), _jsx(TabsContent, { value: "standard" }), _jsx(TabsContent, { value: "auction" }), _jsx(TabsContent, { value: "preorder" }), _jsx(TabsContent, { value: "prize-draw" }), _jsx(TabsContent, { value: "classified" }), _jsx(TabsContent, { value: "digital-code" }), _jsx(TabsContent, { value: "live" })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Classification" }), _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: "Store" }), _jsx(PaginatedSelect, { value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct, renderGroupSettings: isEdit && productId
|
|
176
199
|
? (p) => (_jsx(GroupSettingsPanel, { productId: productId, productSlug: p.slug ?? productId, groupId: p.groupId, isGroupParent: p.isGroupParent, groupParentSlug: p.groupParentSlug, groupChildSlugs: p.groupChildSlugs, groupTitle: p.groupTitle, isAuction: modeFromProduct(p) === "auction", storeProductsEndpoint: ADMIN_ENDPOINTS.PRODUCTS, onGroupChanged: () => productQuery.refetch() }))
|
|
177
|
-
: undefined, renderGroupJoinField: ({ label, value, onChange, disabled }) => (_jsx(GroupInlineSelect, { scope: "admin", value: value ?? "", onChange: (id) => onChange(id || undefined), disabled: disabled, label: label, placeholder: "None (standalone product)", allowCreate: false })), renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: label }), _jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: args.label }), _jsx(PaginatedSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Inventory" }), _jsx(BarcodeField, { value: product.barcodeId ?? "", onChange: (v) => setProduct((prev) => ({ ...prev, barcodeId: v || undefined })), onScan: (v) => setProduct((prev) => ({ ...prev, barcodeId: v })), helperText: "Leave blank to auto-generate. Scan a pre-printed sticker to link it." })] }), _jsxs(Row, { gap: "3", className: "lg:hidden", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] })] }, "product-form"));
|
|
200
|
+
: undefined, renderGroupJoinField: ({ label, value, onChange, disabled }) => (_jsx(GroupInlineSelect, { scope: "admin", value: value ?? "", onChange: (id) => onChange(id || undefined), disabled: disabled, label: label, placeholder: "None (standalone product)", allowCreate: false })), renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: label }), _jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: args.label }), _jsx(PaginatedSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }), _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Inventory" }), _jsx(BarcodeField, { value: product.barcodeId ?? "", onChange: (v) => setProduct((prev) => ({ ...prev, barcodeId: v || undefined })), onScan: (v) => setProduct((prev) => ({ ...prev, barcodeId: v })), helperText: "Leave blank to auto-generate. Scan a pre-printed sticker to link it." })] }), _jsx(FormErrorSummary, {}), _jsxs(Row, { gap: "3", className: "lg:hidden", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !product.title || isSubmitting, children: isEdit ? "Save changes" : "Create product" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => setDeleteOpen(true), children: "Delete product" }))] })] }, "product-form"));
|
|
178
201
|
if (embedded) {
|
|
179
202
|
return _jsx(Div, { className: `${__O.yAuto} ${__P.p4}`, children: formContent });
|
|
180
203
|
}
|
|
@@ -19,6 +19,13 @@ const __O = {
|
|
|
19
19
|
hidden: "overflow-hidden",
|
|
20
20
|
};
|
|
21
21
|
// --- Helpers -----------------------------------------------------------------
|
|
22
|
+
/** Parses a newline- or comma-separated address list into a clean string[]. */
|
|
23
|
+
function splitEmailList(raw) {
|
|
24
|
+
return raw
|
|
25
|
+
.split(/[\n,]/)
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter((entry) => entry.length > 0);
|
|
28
|
+
}
|
|
22
29
|
function MaskedInput({ label, value, onChange, placeholder, helperText, }) {
|
|
23
30
|
const [revealed, setRevealed] = React.useState(false);
|
|
24
31
|
return (_jsxs(Div, { className: "relative", children: [_jsx(Input, { label: label, value: value, onChange: (e) => onChange(e.target.value), type: revealed ? "text" : "password", placeholder: placeholder, helperText: helperText }), _jsx(Button, { type: "button", variant: "ghost", textSize: "xs", textColor: "faint", paddingX: "none", paddingY: "none", onClick: () => setRevealed((r) => !r), className: "absolute right-3 top-8", children: revealed ? "Hide" : "Reveal" })] }));
|
|
@@ -250,6 +257,10 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
250
257
|
const [resendApiKey, setResendApiKey] = React.useState("");
|
|
251
258
|
const [notifFromEmail, setNotifFromEmail] = React.useState("");
|
|
252
259
|
const [notifFromName, setNotifFromName] = React.useState("");
|
|
260
|
+
// Daily ops digest — recipients are edited as one-per-line text, split on save.
|
|
261
|
+
const [digestEnabled, setDigestEnabled] = React.useState(false);
|
|
262
|
+
const [digestRecipients, setDigestRecipients] = React.useState("");
|
|
263
|
+
const [digestCcRecipients, setDigestCcRecipients] = React.useState("");
|
|
253
264
|
// Snapshot of the masked placeholder strings the server returned for every
|
|
254
265
|
// credentials.* field, captured once per load. A combined single-save has
|
|
255
266
|
// to send the FULL credentials object on every save (not just the group the
|
|
@@ -428,6 +439,9 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
428
439
|
setResendApiKey(s.credentialsMasked?.resendApiKey ?? "");
|
|
429
440
|
setNotifFromEmail(s.emailSettings?.fromEmail ?? "");
|
|
430
441
|
setNotifFromName(s.emailSettings?.fromName ?? "");
|
|
442
|
+
setDigestEnabled(s.emailSettings?.dailyDigest?.enabled ?? false);
|
|
443
|
+
setDigestRecipients((s.emailSettings?.dailyDigest?.recipients ?? []).join("\n"));
|
|
444
|
+
setDigestCcRecipients((s.emailSettings?.dailyDigest?.ccRecipients ?? []).join("\n"));
|
|
431
445
|
originalMaskedRef.current = {
|
|
432
446
|
razorpayKeyId: s.credentialsMasked?.razorpayKeyId ?? "",
|
|
433
447
|
razorpaySecret: s.credentialsMasked?.razorpaySecret ?? "",
|
|
@@ -558,6 +572,11 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
558
572
|
emailSettings: {
|
|
559
573
|
host: smtpHost, port: Number(smtpPort), user: smtpUser, fromAddress: smtpFrom,
|
|
560
574
|
fromEmail: notifFromEmail, fromName: notifFromName,
|
|
575
|
+
dailyDigest: {
|
|
576
|
+
enabled: digestEnabled,
|
|
577
|
+
recipients: splitEmailList(digestRecipients),
|
|
578
|
+
ccRecipients: splitEmailList(digestCcRecipients),
|
|
579
|
+
},
|
|
561
580
|
},
|
|
562
581
|
integrations: { googleAnalyticsId: gaMeasurementId, facebookPixelId: fbPixelId, gtmContainerId },
|
|
563
582
|
shipping: { freeShippingThreshold, defaultCarrier, maxDeliveryRadius },
|
|
@@ -656,7 +675,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
656
675
|
...bidIncrementTiers.slice(0, -1),
|
|
657
676
|
{ upTo: 100, increment: 10 },
|
|
658
677
|
bidIncrementTiers[bidIncrementTiers.length - 1] ?? { upTo: null, increment: 1000 },
|
|
659
|
-
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max images per product", value: String(maxImagesPerProduct), onChange: (e) => setMaxImagesPerProduct(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max video size (MB)", value: String(maxVideoSizeMb), onChange: (e) => setMaxVideoSizeMb(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "lg", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: [
|
|
678
|
+
]), children: "+ Add tier" })] }), _jsx(Input, { label: "Auto-extend window (minutes before end)", value: String(autoExtendWindow), onChange: (e) => setAutoExtendWindow(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Extend auction end time if a bid arrives within this window." }), _jsx(Input, { label: "Settlement grace period (hours)", value: String(settlementGrace), onChange: (e) => setSettlementGrace(parseInt(e.target.value) || 0), type: "number", min: 1, helperText: "Time winner has to pay before the auction is re-listed." })] }) }), _jsx(TabsContent, { value: "limits", children: _jsx(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Max products per store", value: String(maxProductsPerStore), onChange: (e) => setMaxProductsPerStore(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max images per product", value: String(maxImagesPerProduct), onChange: (e) => setMaxImagesPerProduct(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max video size (MB)", value: String(maxVideoSizeMb), onChange: (e) => setMaxVideoSizeMb(parseInt(e.target.value) || 0), type: "number", min: 1 }), _jsx(Input, { label: "Max custom fields per product", value: String(maxCustomFields), onChange: (e) => setMaxCustomFields(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Max custom sections per product", value: String(maxCustomSections), onChange: (e) => setMaxCustomSections(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Order cancellation window (hours)", value: String(orderCancelWindow), onChange: (e) => setOrderCancelWindow(parseInt(e.target.value) || 0), type: "number", min: 0 })] }) }) }), _jsx(TabsContent, { value: "whatsapp", children: _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Platform-level WhatsApp Business Cloud API credentials. Used for automated purchase announcements to admin numbers when orders are placed. Store owners configure their own credentials in Store \u2192 WhatsApp." }), _jsx(Input, { label: "Phone Number ID", value: waPhoneNumberId, onChange: (e) => setWaPhoneNumberId(e.target.value), placeholder: "987654321098765", helperText: "From Meta for Developers \u2192 App \u2192 WhatsApp \u2192 API Setup" }), _jsx(MaskedInput, { label: "Cloud API System User Token", value: waCloudApiToken, onChange: setWaCloudApiToken, placeholder: "EAAxxxxxxxx\u2026", helperText: "Long-lived system user access token with WhatsApp Business permissions" }), _jsx(Input, { label: "Admin Notify Numbers", value: waAdminNotifyNumbers, onChange: (e) => setWaAdminNotifyNumbers(e.target.value), placeholder: "919876543210,918765432109", helperText: "Comma-separated, digits-only, include country code. These receive a WhatsApp message when any order is placed." }), _jsx(Text, { size: "xs", color: "muted", children: "Approved Meta message templates for the buyer-facing WhatsApp order-updates addon (Fees tab). Meta rejects business-initiated free-text WhatsApp messages outside a 24h customer-service window \u2014 a real template name here is required for that notification type to actually deliver. Leave blank to skip WhatsApp for that type (in-app + email still fire)." }), _jsx(Input, { label: "Template language code", value: waTemplateLanguage, onChange: (e) => setWaTemplateLanguage(e.target.value), placeholder: "en", helperText: "BCP-47 code the templates below were approved in, e.g. en or en_US." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Order placed template name", value: waTemplateOrderPlaced, onChange: (e) => setWaTemplateOrderPlaced(e.target.value), placeholder: "order_placed_update" }), _jsx(Input, { label: "Order confirmed template name", value: waTemplateOrderConfirmed, onChange: (e) => setWaTemplateOrderConfirmed(e.target.value), placeholder: "order_confirmed_update" }), _jsx(Input, { label: "Order shipped template name", value: waTemplateOrderShipped, onChange: (e) => setWaTemplateOrderShipped(e.target.value), placeholder: "order_shipped_update" }), _jsx(Input, { label: "Order delivered template name", value: waTemplateOrderDelivered, onChange: (e) => setWaTemplateOrderDelivered(e.target.value), placeholder: "order_delivered_update" }), _jsx(Input, { label: "Order cancelled template name", value: waTemplateOrderCancelled, onChange: (e) => setWaTemplateOrderCancelled(e.target.value), placeholder: "order_cancelled_update" }), _jsx(Input, { label: "Refund initiated template name", value: waTemplateRefundInitiated, onChange: (e) => setWaTemplateRefundInitiated(e.target.value), placeholder: "refund_initiated_update" })] })] }) }), _jsx(TabsContent, { value: "notifications", children: _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "lg", children: [_jsx(Text, { size: "xs", color: "muted", children: "In-app notifications are always on. Enable external channels below to let the platform fan out to email, WhatsApp, or SMS. Users can further restrict which types they receive." }), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsxs(Row, { justify: "between", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "In-app (notification bell)" }), _jsx(Span, { color: "success", surface: "success-surface", size: "xs", weight: "semibold", rounded: "full", padding: "pill-xs", children: "Always on" })] }), _jsx(Text, { size: "xs", color: "muted", children: "Displayed in the notification bell and inbox. Cannot be disabled." })] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "Email notifications", checked: notifEmailEnabled, onChange: setNotifEmailEnabled }), notifEmailEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send email", options: PRIORITY_OPTIONS, value: notifEmailMinPriority, onValueChange: setNotifEmailMinPriority }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Resend API (for transactional email)" }), _jsx(MaskedInput, { label: "Resend API Key", value: resendApiKey, onChange: setResendApiKey, placeholder: "re_live_\u2026", helperText: "Get your key at resend.com \u2014 used for all transactional notifications." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "From email", value: notifFromEmail, onChange: (e) => setNotifFromEmail(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Input, { label: "From name", value: notifFromName, onChange: (e) => setNotifFromName(e.target.value), placeholder: "LetItRip" })] }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifEmailTypes, onChange: (values) => setNotifEmailTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "Daily status digest", checked: digestEnabled, onChange: setDigestEnabled }), digestEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Text, { size: "xs", color: "muted", children: "Sent every morning at 10:00 IST with the previous 24 hours of order activity. Its arrival doubles as the platform health check \u2014 if it stops landing, something needs a look." }), _jsx(Textarea, { label: "Recipients (To)", value: digestRecipients, onChange: (e) => setDigestRecipients(e.target.value), placeholder: "support@letitrip.in\nmohasin@letitrip.in", rows: 4, helperText: "One address per line." }), _jsx(Textarea, { label: "CC recipients", value: digestCcRecipients, onChange: (e) => setDigestCcRecipients(e.target.value), placeholder: "One address per line", rows: 3, helperText: "Optional \u2014 anyone else who should receive a copy." })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "WhatsApp notifications", checked: notifWhatsappEnabled, onChange: setNotifWhatsappEnabled }), notifWhatsappEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send WhatsApp", options: PRIORITY_OPTIONS, value: notifWhatsappMinPriority, onValueChange: setNotifWhatsappMinPriority }), _jsx(Toggle, { label: "Enable WhatsApp OTP (for login and verification)", checked: notifWhatsappOtpEnabled, onChange: setNotifWhatsappOtpEnabled }), _jsx(Text, { size: "xs", color: "muted", children: "WhatsApp credentials are configured in the WhatsApp tab (\u246C). OTP messages use the same phone number." }), _jsx(Text, { size: "xs", weight: "medium", color: "muted", className: "pt-[0.25rem]", children: "Notification types (leave empty to send all types)" }), _jsx(PaginatedSelect, { multiple: true, options: NOTIF_TYPE_OPTIONS, value: notifWhatsappTypes, onChange: (values) => setNotifWhatsappTypes(values), placeholder: "All notification types" })] }))] }), _jsxs(Stack, { gap: "md", rounded: "lg", border: "default", padding: "md", children: [_jsx(Toggle, { label: "SMS notifications", checked: notifSmsEnabled, onChange: setNotifSmsEnabled }), notifSmsEnabled && (_jsxs(Stack, { gap: "md", className: NOTIF_CHANNEL_INDENT, children: [_jsx(Select, { label: "Minimum priority to send SMS", options: PRIORITY_OPTIONS, value: notifSmsMinPriority, onValueChange: setNotifSmsMinPriority }), _jsx(Text, { size: "xs", color: "muted", children: "SMS gateway credentials (e.g. Twilio, MSG91) can be configured in the Integrations tab once an SMS provider is connected." })] }))] })] }) }), _jsx(TabsContent, { value: "legal", children: _jsx(Form, { onSubmit: (e) => { e.preventDefault(); saveAllMutation.mutate(); }, className: "pt-[var(--appkit-space-4)]", spacing: "md", children: [
|
|
660
679
|
["Terms of Service", termsHtml, setTermsHtml],
|
|
661
680
|
["Privacy Policy", privacyHtml, setPrivacyHtml],
|
|
662
681
|
["Refund Policy", refundHtml, setRefundHtml],
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
4
|
+
import { useState } from "react";
|
|
4
5
|
import { toRecordArray, toStringValue, } from "../hooks/useAdminListingData";
|
|
5
6
|
import { DataListingView } from "./DataListingView";
|
|
6
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
import { RecordDetailModal } from "../../../ui";
|
|
7
9
|
const ADMIN_STORE_ADDRESSES_CONFIG = {
|
|
8
10
|
portal: "admin",
|
|
9
11
|
title: "Store Addresses",
|
|
@@ -34,10 +36,28 @@ const ADMIN_STORE_ADDRESSES_CONFIG = {
|
|
|
34
36
|
.join(" · "),
|
|
35
37
|
status: item.isDefault ? "default" : "standard",
|
|
36
38
|
updatedAt: toStringValue(item.storeId, ""),
|
|
39
|
+
_raw: item,
|
|
37
40
|
})),
|
|
38
41
|
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
39
42
|
buildFilters: () => undefined,
|
|
40
43
|
};
|
|
41
44
|
export function AdminStoreAddressesView(_props) {
|
|
42
|
-
|
|
45
|
+
const [selected, setSelected] = useState(null);
|
|
46
|
+
const raw = selected?._raw ?? {};
|
|
47
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: { ...ADMIN_STORE_ADDRESSES_CONFIG, onRowClick: (row) => setSelected(row) } }), _jsx(RecordDetailModal, { isOpen: Boolean(selected), onClose: () => setSelected(null), title: toStringValue(raw.label, "Store Address"), badges: raw.isDefault
|
|
48
|
+
? [{ label: "Default pickup location", variant: "info" }]
|
|
49
|
+
: undefined, fields: [
|
|
50
|
+
{ label: "Store", value: toStringValue(raw.storeName ?? raw.storeId, "—") },
|
|
51
|
+
{ label: "Contact", value: toStringValue(raw.fullName, "—") },
|
|
52
|
+
{ label: "Phone", value: toStringValue(raw.phone, "—") },
|
|
53
|
+
{ label: "Address", value: toStringValue(raw.addressLine1, "—") },
|
|
54
|
+
...(raw.addressLine2
|
|
55
|
+
? [{ label: "Address line 2", value: toStringValue(raw.addressLine2, "—") }]
|
|
56
|
+
: []),
|
|
57
|
+
...(raw.landmark ? [{ label: "Landmark", value: toStringValue(raw.landmark, "—") }] : []),
|
|
58
|
+
{ label: "City", value: toStringValue(raw.city, "—") },
|
|
59
|
+
{ label: "State", value: toStringValue(raw.state, "—") },
|
|
60
|
+
{ label: "Postal code", value: toStringValue(raw.postalCode ?? raw.pincode, "—") },
|
|
61
|
+
{ label: "Country", value: toStringValue(raw.country, "India") },
|
|
62
|
+
] })] }));
|
|
43
63
|
}
|
|
@@ -4,9 +4,11 @@ import { useApiMutation } from "@mohasinac/appkit/client";
|
|
|
4
4
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit/client";
|
|
5
5
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
6
6
|
import React, { useState } from "react";
|
|
7
|
+
import { useRouter } from "next/navigation";
|
|
7
8
|
import { useQueryClient } from "@tanstack/react-query";
|
|
8
9
|
import { FilterChipGroup, ListingLayout, RowActionMenu, useToast } from "../../../ui";
|
|
9
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
10
12
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
13
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
12
14
|
import { ADMIN_STORE_STATUS_TABS } from "../constants/filter-tabs";
|
|
@@ -18,6 +20,7 @@ export function AdminStoresView({ children, ...props }) {
|
|
|
18
20
|
const [editorRow, setEditorRow] = useState(null);
|
|
19
21
|
const toast = useToast();
|
|
20
22
|
const queryClient = useQueryClient();
|
|
23
|
+
const router = useRouter();
|
|
21
24
|
const verifyStore = useApiMutation({
|
|
22
25
|
mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { isVerified: true }),
|
|
23
26
|
onSuccess: () => {
|
|
@@ -96,6 +99,10 @@ export function AdminStoresView({ children, ...props }) {
|
|
|
96
99
|
label: ACTIONS.ADMIN["manage-store"].label,
|
|
97
100
|
onClick: () => setEditorRow(row),
|
|
98
101
|
},
|
|
102
|
+
{
|
|
103
|
+
label: "Open full page",
|
|
104
|
+
onClick: () => router.push(String(ROUTES.ADMIN.STORE_DETAIL(row.id))),
|
|
105
|
+
},
|
|
99
106
|
{
|
|
100
107
|
label: ACTIONS.ADMIN["verify-store"].label,
|
|
101
108
|
onClick: () => verifyStore.mutate(row.id),
|
|
@@ -6,7 +6,7 @@ import React from "react";
|
|
|
6
6
|
import { useQuery } from "@tanstack/react-query";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { Button, ConfirmDeleteModal, Div, Form, Input, StackedViewShell, useToast, } from "../../../ui";
|
|
9
|
-
import { FieldInput } from "../../../ui/forms";
|
|
9
|
+
import { FieldInput, FormErrorSummary } from "../../../ui/forms";
|
|
10
10
|
const sublistingCategoryFormSchema = z.object({
|
|
11
11
|
name: z.string().min(1, "Name is required").max(120),
|
|
12
12
|
slug: z.string().regex(/^[a-z0-9-]+$/, "Lowercase letters, digits and hyphens only").optional().or(z.literal("")),
|
|
@@ -81,7 +81,7 @@ export function AdminSublistingCategoryEditorView({ categoryId, onSaved, onDelet
|
|
|
81
81
|
_jsx(Form, { schema: sublistingCategoryFormSchema, onSubmit: (e) => e.preventDefault(), spacing: "md", children: ({ setFieldError, clearErrors }) => (_jsxs(_Fragment, { children: [_jsxs(Div, { layout: "grid", gap: "4", className: "sm:grid-cols-2", children: [_jsx(FieldInput, { name: "name", label: "Category name", value: name, onChange: (v) => setName(v), required: true, placeholder: "e.g. Base Set Charizard 108/120" }), _jsx(Input, { label: "Item code", value: itemCode, onChange: (e) => setItemCode(e.target.value), placeholder: "e.g. PSA 10, 108/120, STH", helperText: "Grade, card number, or series code. Optional." })] }), _jsx(Input, { label: "Description", value: description, onChange: (e) => setDescription(e.target.value), placeholder: "Brief description shown on the public category page" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "sublisting-categories", true, {
|
|
82
82
|
type: "category-image",
|
|
83
83
|
name: name || "sublisting",
|
|
84
|
-
}), onChange: setCoverImage }), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, onClick: () => {
|
|
84
|
+
}), onChange: setCoverImage }), _jsx(FormErrorSummary, {}), _jsxs(Row, { gap: "3", padding: "t-xs", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !name || isSubmitting, onClick: () => {
|
|
85
85
|
clearErrors();
|
|
86
86
|
if (!name.trim()) {
|
|
87
87
|
setFieldError("name", "Category name is required");
|