@mohasinac/appkit 4.43.0 → 4.44.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/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +62 -11
- package/dist/features/categories/repository/categories.repository.js +178 -35
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +5 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -15,6 +15,7 @@ import { SITE_SETTINGS_TABS, isSiteSettingsTabId, } from "../constants/site-sett
|
|
|
15
15
|
import { ThemeManagerView, } from "../../site-settings/components/ThemeManagerView";
|
|
16
16
|
import { DEFAULT_LIGHT_THEME, DEFAULT_DARK_THEME } from "../../../tokens/themes";
|
|
17
17
|
import { DEFAULT_AUCTION_BID_INCREMENT_TIERS, } from "../../../_internal/shared/features/auctions/config";
|
|
18
|
+
import { validateGstEnablement } from "../../../_internal/shared/features/site-settings/gst-registration";
|
|
18
19
|
import { siteSettingsFormSchema } from "../schemas/site-settings-form";
|
|
19
20
|
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary";
|
|
20
21
|
import { SectionForm } from "../../shell";
|
|
@@ -187,6 +188,24 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
187
188
|
const [gstin, setGstin] = React.useState("");
|
|
188
189
|
const [gstLegalName, setGstLegalName] = React.useState("");
|
|
189
190
|
const [gstAddress, setGstAddress] = React.useState("");
|
|
191
|
+
/*
|
|
192
|
+
* Shown inline, live, and ONLY while the toggle is on — the same rule the
|
|
193
|
+
* server enforces. `/api/admin/site` rejects the save, so without this the
|
|
194
|
+
* admin would get a generic "save failed" with no indication of which of the
|
|
195
|
+
* four fields it meant.
|
|
196
|
+
*
|
|
197
|
+
* Live rather than on-submit because the toggle is the action: an admin who
|
|
198
|
+
* flips it on sees immediately what else is needed, instead of discovering
|
|
199
|
+
* it after a round trip. Empty when the toggle is off, so an admin editing
|
|
200
|
+
* their address with GST disabled is never shown errors about it
|
|
201
|
+
* (Root Cause #74 — a summary that accuses before anything was attempted).
|
|
202
|
+
*/
|
|
203
|
+
const gstProblems = React.useMemo(() => validateGstEnablement({
|
|
204
|
+
enabled: gstEnabled,
|
|
205
|
+
gstin,
|
|
206
|
+
legalName: gstLegalName,
|
|
207
|
+
address: gstAddress,
|
|
208
|
+
}), [gstEnabled, gstin, gstLegalName, gstAddress]);
|
|
190
209
|
const [platformFeePercent, setPlatformFeePercent] = React.useState(5);
|
|
191
210
|
const [gstPercent, setGstPercent] = React.useState(18);
|
|
192
211
|
const [minimumTransactionFee, setMinimumTransactionFee] = React.useState(0);
|
|
@@ -783,7 +802,7 @@ export function AdminSiteSettingsView({ labels = {}, ...rest }) {
|
|
|
783
802
|
{ label: "Top right", value: "top-right" },
|
|
784
803
|
{ label: "Bottom left", value: "bottom-left" },
|
|
785
804
|
{ label: "Bottom right", value: "bottom-right" },
|
|
786
|
-
], value: watermarkPosition, onValueChange: (v) => setWatermarkPosition(v), disabled: watermarkUseCustomOffset }), _jsx(Toggle, { checked: watermarkUseCustomOffset, onChange: setWatermarkUseCustomOffset, label: "Use a custom X/Y offset instead" }), watermarkUseCustomOffset && (_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "X offset (%) \u2014 + right / \u2212 left of center", value: String(watermarkOffsetX), onChange: (e) => setWatermarkOffsetX(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 }), _jsx(Input, { label: "Y offset (%) \u2014 + down / \u2212 up from center", value: String(watermarkOffsetY), onChange: (e) => setWatermarkOffsetY(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Preview (text watermark only)" }), _jsx(Row, { surface: "default", justify: "end", align: "end", className: `relative h-32 ${__O.hidden}`, rounded: "default", children: _jsx(Span, { ref: watermarkPreviewRef, weight: "medium", color: "faint", padding: "1", className: "select-none", children: watermarkText }) })] })] }) }), _jsx(TabsContent, { value: "fees", children: _jsxs(SettingsTabForm, { id: "fees", label: "Fees", children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Platform fee \u2014 our cut (%)", helperText: "% charged on order value. Buyer pays this.", value: String(platformFeePercent), onChange: (e) => setPlatformFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "GST on platform fee (%)", helperText: "Applied to our fee only (not full order). Usually 18%.", value: String(gstPercent), onChange: (e) => setGstPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "PhonePe gateway cost (%)", helperText: "Gateway's own fee \u2014 absorbed by platform, not passed through.", value: String(gatewayFeePercent), onChange: (e) => setGatewayFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 10, step: 0.01 }), _jsx(Input, { label: "Minimum transaction fee (\u20B9)", helperText: "Per-transaction floor. Total charge will never be below base + this.", value: String(minimumTransactionFee), onChange: (e) => setMinimumTransactionFee(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Maximum platform fee (\u20B9)", helperText: "Ceiling on the buyer's platform commission, charged once per checkout on every payment method. GST is calculated on the capped amount.", value: String(platformFeeMax), onChange: (e) => setPlatformFeeMax(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Seller payout hold (days)", value: String(payoutHoldDays), onChange: (e) => setPayoutHoldDays(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Minimum payout amount (\u20B9)", value: String(minPayoutAmount), onChange: (e) => setMinPayoutAmount(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Auction listing fee (\u20B9)", value: String(auctionListingFee), onChange: (e) => setAuctionListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Pre-order listing fee (\u20B9)", value: String(preOrderListingFee), onChange: (e) => setPreOrderListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Featured slot fee (\u20B9)", value: String(featuredSlotFee), onChange: (e) => setFeaturedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Promoted slot fee (\u20B9)", value: String(promotedSlotFee), onChange: (e) => setPromotedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD deposit (%)", helperText: "% of order collected upfront on COD orders; remainder paid on delivery.", value: String(codDepositPercent), onChange: (e) => setCodDepositPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 })] }), _jsx(Toggle, { checked: whatsappNotifyFeeEnabled, onChange: setWhatsappNotifyFeeEnabled, label: "Offer the WhatsApp order-updates addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "WhatsApp addon fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in. Requires WhatsApp credentials + at least one approved template \u2014 see the WhatsApp tab.", value: String(whatsappNotifyFee), onChange: (e) => setWhatsappNotifyFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !whatsappNotifyFeeEnabled }) }), _jsx(Toggle, { checked: giftWrapFeeEnabled, onChange: setGiftWrapFeeEnabled, label: "Offer the gift-wrap addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "Gift wrap fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in.", value: String(giftWrapFee), onChange: (e) => setGiftWrapFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !giftWrapFeeEnabled }) }), _jsx(Toggle, { checked: shipmentProtectionFeeEnabled, onChange: setShipmentProtectionFeeEnabled, label: "Offer the shipment-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Shipment protection (%)", helperText: "% of order subtotal, charged when the buyer opts in.", value: String(shipmentProtectionFeePercent), onChange: (e) => setShipmentProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !shipmentProtectionFeeEnabled }), _jsx(Input, { label: "Shipment protection minimum (\u20B9)", helperText: "Floor for the shipment-protection fee.", value: String(shipmentProtectionFeeMin), onChange: (e) => setShipmentProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !shipmentProtectionFeeEnabled })] })] }) }), _jsx(TabsContent, { value: "procurement", children: _jsxs(SettingsTabForm, { id: "procurement", label: "Procurement", children: [_jsx(Text, { size: "xs", color: "muted", children: "Used to compute each procurement shipment's labor cost (hours spent \u00D7 hourly rate) and its estimated processing time (hours spent \u00F7 max hours/day)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Hourly rate (\u20B9)", helperText: "Your effective hourly wage for sorting/categorizing/listing a shipment.", value: String(laborHourlyRate), onChange: (e) => setLaborHourlyRate(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Max hours per day", helperText: "Used only to project how many days a shipment's tracked hours will take.", value: String(laborMaxHoursPerDay), onChange: (e) => setLaborMaxHoursPerDay(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 24 })] })] }) }), _jsx(TabsContent, { value: "emi", children: _jsxs(SettingsTabForm, { id: "emi", label: "Emi", children: [_jsx(Text, { size: "xs", color: "muted", children: "A seller must also opt in via their own Payout Settings for EMI to appear at checkout on their items. See the \"How EMI Works\" public page for buyer-facing copy." }), _jsx(Toggle, { checked: emiEnabled, onChange: setEmiEnabled, label: "Enable EMI platform-wide" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Minimum order value (\u20B9)", helperText: "A seller's cart subtotal must exceed this for EMI to appear as an option.", value: String(emiMinOrderValue), onChange: (e) => setEmiMinOrderValue(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Tenure options (months, comma-separated)", helperText: "e.g. 2,3,4,5,6", value: emiTenureOptionsText, onChange: (e) => setEmiTenureOptionsText(e.target.value) }), _jsx(Input, { label: "Token / down-payment (%)", helperText: "Collected upfront at checkout.", value: String(emiTokenPercent), onChange: (e) => setEmiTokenPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 }), _jsx(Input, { label: "Billing day (1\u201310)", helperText: "Day of month each installment is due.", value: String(emiBillingDay), onChange: (e) => setEmiBillingDay(parseFloat(e.target.value) || 1), type: "number", min: 1, max: 10 }), _jsx(Input, { label: "Surcharge (% of principal per month)", helperText: "The 'excess EMI fee' the buyer pays for spreading payment.", value: String(emiSurchargePercentPerMonth), onChange: (e) => setEmiSurchargePercentPerMonth(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Seller's share of surcharge (%)", helperText: "The rest of the surcharge goes to the platform.", value: String(emiSurchargeSellerSharePercent), onChange: (e) => setEmiSurchargeSellerSharePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 })] })] }) }), _jsx(TabsContent, { value: "gst", children: _jsxs(SettingsTabForm, { id: "gst", label: "Gst", children: [_jsx(Text, { size: "xs", color: "muted", children: "Required before enabling GST-inclusive checkout (P-8). Also set a GST rate + HSN code on each taxable product for the tax breakdown to appear." }), _jsx(Toggle, { checked: gstEnabled, onChange: setGstEnabled, label: "Enable GST on checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "GSTIN", helperText: "15-character GST Identification Number.", value: gstin, onChange: (e) => setGstin(e.target.value), placeholder: "29AAAAA0000A1Z5", maxLength: 15 }), _jsx(Input, { label: "Legal name", helperText: "Registered business name for invoices.", value: gstLegalName, onChange: (e) => setGstLegalName(e.target.value), placeholder: "LetItRip Collectibles Pvt Ltd" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Registered address" }), _jsx(Textarea, { value: gstAddress, onChange: (e) => setGstAddress(e.target.value), placeholder: "Registered business address for GST invoices\u2026", rows: 3 })] })] }) }), _jsx(TabsContent, { value: "listings", children: _jsxs(SettingsTabForm, { id: "listings", label: "Listings", children: [_jsx(Text, { size: "xs", color: "muted", children: "Which listing and category types the marketplace offers. A disabled type is hidden from every browse surface and rejected on create and add-to-cart \u2014 existing data is untouched, it just becomes unreachable until re-enabled." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Listing types" }), _jsx(Text, { size: "xs", color: "muted", children: "Disabled types are hidden from listings and reject create/add-to-cart." }), _jsx(Grid, { cols: 2, gap: "sm", children: LISTING_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 disabled by default, manual payment is the default", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "High-value checkout OTP threshold (\u20B9)", helperText: "Carts at or above this subtotal require an extra OTP verification step before payment (0 = disabled).", value: String(otpCheckoutThreshold), onChange: (e) => setOtpCheckoutThreshold(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Text, { size: "sm", weight: "medium", color: "muted", className: "pt-[var(--appkit-space-2)]", children: "Seller shipping charge defaults" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Seller shipping fixed (\u20B9)", helperText: "Flat shipping charge collected on the seller's behalf.", value: String(sellerShippingFixed), onChange: (e) => setSellerShippingFixed(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Platform shipping (%)", helperText: "Platform's own shipping markup, as a % of order value.", value: String(platformShippingPercent), onChange: (e) => setPlatformShippingPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "Platform shipping minimum (\u20B9)", helperText: "Floor for the platform shipping markup.", value: String(platformShippingFixedMin), onChange: (e) => setPlatformShippingFixedMin(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "auction", children: _jsxs(SettingsTabForm, { id: "auction", label: "Auction", children: [_jsxs(Stack, { gap: "sm", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "xs", weight: "medium", color: "muted", children: "Bid increment tiers" }), _jsx(Text, { size: "xs", color: "muted", children: "The minimum jump a new bid must clear, based on the current bid amount. A seller's per-listing override can require more than the matching tier, but can never undercut it." }), bidIncrementTiers.map((tier, i) => {
|
|
805
|
+
], value: watermarkPosition, onValueChange: (v) => setWatermarkPosition(v), disabled: watermarkUseCustomOffset }), _jsx(Toggle, { checked: watermarkUseCustomOffset, onChange: setWatermarkUseCustomOffset, label: "Use a custom X/Y offset instead" }), watermarkUseCustomOffset && (_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "X offset (%) \u2014 + right / \u2212 left of center", value: String(watermarkOffsetX), onChange: (e) => setWatermarkOffsetX(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 }), _jsx(Input, { label: "Y offset (%) \u2014 + down / \u2212 up from center", value: String(watermarkOffsetY), onChange: (e) => setWatermarkOffsetY(Math.max(-45, Math.min(45, parseInt(e.target.value) || 0))), type: "number", min: -45, max: 45 })] })), _jsxs(Stack, { gap: "xs", surface: "muted", rounded: "lg", border: "default", padding: "md", children: [_jsx(Text, { size: "xs", color: "muted", children: "Preview (text watermark only)" }), _jsx(Row, { surface: "default", justify: "end", align: "end", className: `relative h-32 ${__O.hidden}`, rounded: "default", children: _jsx(Span, { ref: watermarkPreviewRef, weight: "medium", color: "faint", padding: "1", className: "select-none", children: watermarkText }) })] })] }) }), _jsx(TabsContent, { value: "fees", children: _jsxs(SettingsTabForm, { id: "fees", label: "Fees", children: [_jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Platform fee \u2014 our cut (%)", helperText: "% charged on order value. Buyer pays this.", value: String(platformFeePercent), onChange: (e) => setPlatformFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "GST on platform fee (%)", helperText: "Applied to our fee only (not full order). Usually 18%.", value: String(gstPercent), onChange: (e) => setGstPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "PhonePe gateway cost (%)", helperText: "Gateway's own fee \u2014 absorbed by platform, not passed through.", value: String(gatewayFeePercent), onChange: (e) => setGatewayFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 10, step: 0.01 }), _jsx(Input, { label: "Minimum transaction fee (\u20B9)", helperText: "Per-transaction floor. Total charge will never be below base + this.", value: String(minimumTransactionFee), onChange: (e) => setMinimumTransactionFee(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Maximum platform fee (\u20B9)", helperText: "Ceiling on the buyer's platform commission, charged once per checkout on every payment method. GST is calculated on the capped amount.", value: String(platformFeeMax), onChange: (e) => setPlatformFeeMax(parseFloat(e.target.value) || 0), type: "number", min: 0, step: 0.01 }), _jsx(Input, { label: "Seller payout hold (days)", value: String(payoutHoldDays), onChange: (e) => setPayoutHoldDays(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Minimum payout amount (\u20B9)", value: String(minPayoutAmount), onChange: (e) => setMinPayoutAmount(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Auction listing fee (\u20B9)", value: String(auctionListingFee), onChange: (e) => setAuctionListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Pre-order listing fee (\u20B9)", value: String(preOrderListingFee), onChange: (e) => setPreOrderListingFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Featured slot fee (\u20B9)", value: String(featuredSlotFee), onChange: (e) => setFeaturedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Promoted slot fee (\u20B9)", value: String(promotedSlotFee), onChange: (e) => setPromotedSlotFee(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "COD deposit (%)", helperText: "% of order collected upfront on COD orders; remainder paid on delivery.", value: String(codDepositPercent), onChange: (e) => setCodDepositPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 })] }), _jsx(Toggle, { checked: whatsappNotifyFeeEnabled, onChange: setWhatsappNotifyFeeEnabled, label: "Offer the WhatsApp order-updates addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "WhatsApp addon fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in. Requires WhatsApp credentials + at least one approved template \u2014 see the WhatsApp tab.", value: String(whatsappNotifyFee), onChange: (e) => setWhatsappNotifyFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !whatsappNotifyFeeEnabled }) }), _jsx(Toggle, { checked: giftWrapFeeEnabled, onChange: setGiftWrapFeeEnabled, label: "Offer the gift-wrap addon at checkout" }), _jsx(Grid, { cols: 2, gap: "md", children: _jsx(Input, { label: "Gift wrap fee (\u20B9)", helperText: "Flat fee charged to the buyer when they opt in.", value: String(giftWrapFee), onChange: (e) => setGiftWrapFee(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !giftWrapFeeEnabled }) }), _jsx(Toggle, { checked: shipmentProtectionFeeEnabled, onChange: setShipmentProtectionFeeEnabled, label: "Offer the shipment-protection addon at checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Shipment protection (%)", helperText: "% of order subtotal, charged when the buyer opts in.", value: String(shipmentProtectionFeePercent), onChange: (e) => setShipmentProtectionFeePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1, disabled: !shipmentProtectionFeeEnabled }), _jsx(Input, { label: "Shipment protection minimum (\u20B9)", helperText: "Floor for the shipment-protection fee.", value: String(shipmentProtectionFeeMin), onChange: (e) => setShipmentProtectionFeeMin(parseFloat(e.target.value) || 0), type: "number", min: 0, disabled: !shipmentProtectionFeeEnabled })] })] }) }), _jsx(TabsContent, { value: "procurement", children: _jsxs(SettingsTabForm, { id: "procurement", label: "Procurement", children: [_jsx(Text, { size: "xs", color: "muted", children: "Used to compute each procurement shipment's labor cost (hours spent \u00D7 hourly rate) and its estimated processing time (hours spent \u00F7 max hours/day)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Hourly rate (\u20B9)", helperText: "Your effective hourly wage for sorting/categorizing/listing a shipment.", value: String(laborHourlyRate), onChange: (e) => setLaborHourlyRate(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Max hours per day", helperText: "Used only to project how many days a shipment's tracked hours will take.", value: String(laborMaxHoursPerDay), onChange: (e) => setLaborMaxHoursPerDay(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 24 })] })] }) }), _jsx(TabsContent, { value: "emi", children: _jsxs(SettingsTabForm, { id: "emi", label: "Emi", children: [_jsx(Text, { size: "xs", color: "muted", children: "A seller must also opt in via their own Payout Settings for EMI to appear at checkout on their items. See the \"How EMI Works\" public page for buyer-facing copy." }), _jsx(Toggle, { checked: emiEnabled, onChange: setEmiEnabled, label: "Enable EMI platform-wide" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Minimum order value (\u20B9)", helperText: "A seller's cart subtotal must exceed this for EMI to appear as an option.", value: String(emiMinOrderValue), onChange: (e) => setEmiMinOrderValue(Math.round((parseFloat(e.target.value) || 0) * 100) / 100), type: "number", min: 0 }), _jsx(Input, { label: "Tenure options (months, comma-separated)", helperText: "e.g. 2,3,4,5,6", value: emiTenureOptionsText, onChange: (e) => setEmiTenureOptionsText(e.target.value) }), _jsx(Input, { label: "Token / down-payment (%)", helperText: "Collected upfront at checkout.", value: String(emiTokenPercent), onChange: (e) => setEmiTokenPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 }), _jsx(Input, { label: "Billing day (1\u201310)", helperText: "Day of month each installment is due.", value: String(emiBillingDay), onChange: (e) => setEmiBillingDay(parseFloat(e.target.value) || 1), type: "number", min: 1, max: 10 }), _jsx(Input, { label: "Surcharge (% of principal per month)", helperText: "The 'excess EMI fee' the buyer pays for spreading payment.", value: String(emiSurchargePercentPerMonth), onChange: (e) => setEmiSurchargePercentPerMonth(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Seller's share of surcharge (%)", helperText: "The rest of the surcharge goes to the platform.", value: String(emiSurchargeSellerSharePercent), onChange: (e) => setEmiSurchargeSellerSharePercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100 })] })] }) }), _jsx(TabsContent, { value: "gst", children: _jsxs(SettingsTabForm, { id: "gst", label: "Gst", children: [_jsx(Text, { size: "xs", color: "muted", children: "Required before enabling GST-inclusive checkout (P-8). Also set a GST rate + HSN code on each taxable product for the tax breakdown to appear." }), _jsx(Toggle, { checked: gstEnabled, onChange: setGstEnabled, label: "Enable GST on checkout" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "GSTIN", helperText: "15-character GST Identification Number.", error: gstProblems.gstin, value: gstin, onChange: (e) => setGstin(e.target.value), placeholder: "29AAAAA0000A1Z5", maxLength: 15 }), _jsx(Input, { label: "Legal name", helperText: "Registered business name for invoices.", error: gstProblems.legalName, value: gstLegalName, onChange: (e) => setGstLegalName(e.target.value), placeholder: "LetItRip Collectibles Pvt Ltd" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Registered address" }), _jsx(Textarea, { value: gstAddress, onChange: (e) => setGstAddress(e.target.value), placeholder: "Registered business address for GST invoices\u2026", rows: 3 }), gstProblems.address ? (_jsx(Text, { size: "sm", variant: "error", role: "alert", children: gstProblems.address })) : null] })] }) }), _jsx(TabsContent, { value: "listings", children: _jsxs(SettingsTabForm, { id: "listings", label: "Listings", children: [_jsx(Text, { size: "xs", color: "muted", children: "Which listing and category types the marketplace offers. A disabled type is hidden from every browse surface and rejected on create and add-to-cart \u2014 existing data is untouched, it just becomes unreachable until re-enabled." }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Listing types" }), _jsx(Text, { size: "xs", color: "muted", children: "Disabled types are hidden from listings and reject create/add-to-cart." }), _jsx(Grid, { cols: 2, gap: "sm", children: LISTING_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: listingTypeFlags[key] ?? true, onChange: (v) => setListingTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Category types" }), _jsx(Grid, { cols: 2, gap: "sm", children: CATEGORY_TYPE_KEYS.map((key) => (_jsx(Toggle, { label: key, checked: categoryTypeFlags[key] ?? true, onChange: (v) => setCategoryTypeFlags((prev) => ({ ...prev, [key]: v })) }, key))) })] })] }) }), _jsx(TabsContent, { value: "integrations", children: _jsxs(SettingsTabForm, { id: "integrations", label: "Integrations", children: [_jsx(Text, { size: "xs", color: "muted", children: "Keys are masked in transit and stored encrypted. Click Reveal to view." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "PhonePe" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "PhonePe Client ID", value: phonepeClientId, onChange: setPhonepeClientId, placeholder: "MERCHANTID_\u2026" }), _jsx(MaskedInput, { label: "PhonePe Client Secret", value: phonepeClientSecret, onChange: setPhonepeClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "PhonePe Client Version", value: phonepeClientVersion, onChange: (e) => setPhonepeClientVersion(e.target.value), placeholder: "1" }), _jsx(Select, { label: "PhonePe Environment", options: PHONEPE_ENVIRONMENT_OPTIONS, value: phonepeEnvironment, onValueChange: setPhonepeEnvironment }), _jsx(MaskedInput, { label: "PhonePe Webhook Username", value: phonepeWebhookUsername, onChange: setPhonepeWebhookUsername, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "PhonePe Webhook Password", value: phonepeWebhookPassword, onChange: setPhonepeWebhookPassword, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "SMTP / Email" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "SMTP host", value: smtpHost, onChange: (e) => setSmtpHost(e.target.value), placeholder: "smtp.sendgrid.net" }), _jsx(Input, { label: "SMTP port", value: smtpPort, onChange: (e) => setSmtpPort(e.target.value), type: "number", placeholder: "587" }), _jsx(Input, { label: "SMTP user", value: smtpUser, onChange: (e) => setSmtpUser(e.target.value), placeholder: "apikey" })] }), _jsx(Input, { label: "From address", value: smtpFrom, onChange: (e) => setSmtpFrom(e.target.value), placeholder: "noreply@letitrip.in", type: "email" }), _jsx(Text, { size: "xs", color: "muted", children: "Transactional email is delivered through Resend \u2014 set the Resend API key below. These SMTP fields are stored but not currently used for delivery." })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Google Places (store reviews)" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Google Maps API key", value: googleMapsApiKey, onChange: setGoogleMapsApiKey, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(Input, { label: "Default Google Place ID", value: googlePlaceId, onChange: (e) => setGooglePlaceId(e.target.value), placeholder: "ChIJ\u2026" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Analytics & Tracking" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Google Analytics ID", value: gaMeasurementId, onChange: (e) => setGaMeasurementId(e.target.value), placeholder: "G-XXXXXXXXXX" }), _jsx(Input, { label: "Facebook Pixel ID", value: fbPixelId, onChange: (e) => setFbPixelId(e.target.value), placeholder: "XXXXXXXXXXXXXXXX" }), _jsx(Input, { label: "GTM Container ID", value: gtmContainerId, onChange: (e) => setGtmContainerId(e.target.value), placeholder: "GTM-XXXXXXX" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Meta (Instagram & Facebook Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Used by the Social Feed section to fetch Instagram and Facebook posts via Meta Graph API v19." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Page Access Token", value: metaPageAccessToken, onChange: setMetaPageAccessToken, placeholder: "EAAxxxxxxx\u2026" }), _jsx(Input, { label: "Page ID (or handle)", value: metaPageId, onChange: (e) => setMetaPageId(e.target.value), placeholder: "letitrip" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "TikTok for Developers (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials + long-lived access token from TikTok for Developers. Used to list your account's public videos." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client Key", value: tiktokClientKey, onChange: setTiktokClientKey, placeholder: "aw\u2026" }), _jsx(MaskedInput, { label: "Client Secret", value: tiktokClientSecret, onChange: setTiktokClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" }), _jsx(MaskedInput, { label: "Access Token (long-lived)", value: tiktokAccessToken, onChange: setTiktokAccessToken, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "DeviantArt OAuth2 (Social Feed)" }), _jsx(Text, { size: "xs", color: "muted", children: "Client credentials for DeviantArt gallery fetching (client-credentials OAuth2 flow \u2014 no user login required)." }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(MaskedInput, { label: "Client ID", value: deviantartClientId, onChange: setDeviantartClientId, placeholder: "1234" }), _jsx(MaskedInput, { label: "Client Secret", value: deviantartClientSecret, onChange: setDeviantartClientSecret, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" })] })] })] }) }), _jsx(TabsContent, { value: "shipping", children: _jsxs(SettingsTabForm, { id: "shipping", label: "Shipping", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Payment methods" }), _jsx(Div, { id: "setting-phonepe-enabled", children: _jsx(Toggle, { label: "PhonePe (online card/UPI) enabled \u2014 disabled by default, manual payment is the default", checked: phonepeEnabled, onChange: setPhonepeEnabled }) }), _jsx(Div, { id: "setting-manual-payment-enabled", children: _jsx(Toggle, { label: "Manual UPI/bank transfer enabled", checked: upiManualEnabled, onChange: setUpiManualEnabled }) }), _jsx(Div, { id: "setting-cod-enabled", children: _jsx(Toggle, { label: "Cash on delivery (COD) enabled", checked: codEnabled, onChange: setCodEnabled }) }), _jsx(Input, { label: "Free shipping threshold (\u20B9)", value: String(freeShippingThreshold), onChange: (e) => setFreeShippingThreshold(parseInt(e.target.value) || 0), type: "number", min: 0, helperText: "Orders above this amount get free shipping." }), _jsx(Select, { label: "Default carrier", options: CARRIER_OPTIONS, value: defaultCarrier, onValueChange: setDefaultCarrier }), _jsx(Input, { label: "Max delivery radius (km, 0 = no limit)", value: String(maxDeliveryRadius), onChange: (e) => setMaxDeliveryRadius(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "High-value checkout OTP threshold (\u20B9)", helperText: "Carts at or above this subtotal require an extra OTP verification step before payment (0 = disabled).", value: String(otpCheckoutThreshold), onChange: (e) => setOtpCheckoutThreshold(parseInt(e.target.value) || 0), type: "number", min: 0 }), _jsx(Text, { size: "sm", weight: "medium", color: "muted", className: "pt-[var(--appkit-space-2)]", children: "Seller shipping charge defaults" }), _jsxs(Grid, { cols: 2, gap: "md", children: [_jsx(Input, { label: "Seller shipping fixed (\u20B9)", helperText: "Flat shipping charge collected on the seller's behalf.", value: String(sellerShippingFixed), onChange: (e) => setSellerShippingFixed(parseFloat(e.target.value) || 0), type: "number", min: 0 }), _jsx(Input, { label: "Platform shipping (%)", helperText: "Platform's own shipping markup, as a % of order value.", value: String(platformShippingPercent), onChange: (e) => setPlatformShippingPercent(parseFloat(e.target.value) || 0), type: "number", min: 0, max: 100, step: 0.1 }), _jsx(Input, { label: "Platform shipping minimum (\u20B9)", helperText: "Floor for the platform shipping markup.", value: String(platformShippingFixedMin), onChange: (e) => setPlatformShippingFixedMin(parseFloat(e.target.value) || 0), type: "number", min: 0 })] })] }) }), _jsx(TabsContent, { value: "auction", children: _jsxs(SettingsTabForm, { id: "auction", label: "Auction", children: [_jsxs(Stack, { gap: "sm", rounded: "lg", border: "default", padding: "sm", children: [_jsx(Text, { size: "xs", weight: "medium", color: "muted", children: "Bid increment tiers" }), _jsx(Text, { size: "xs", color: "muted", children: "The minimum jump a new bid must clear, based on the current bid amount. A seller's per-listing override can require more than the matching tier, but can never undercut it." }), bidIncrementTiers.map((tier, i) => {
|
|
787
806
|
const isLast = i === bidIncrementTiers.length - 1;
|
|
788
807
|
return (_jsxs(Grid, { align: "end", gap: "xs", className: "grid-cols-12", children: [_jsx(Div, { className: "col-span-5", children: isLast ? (_jsxs(_Fragment, { children: [_jsx(Text, { size: "xs", weight: "medium", className: "mb-1", children: "Up to (\u20B9)" }), _jsx(Text, { size: "sm", color: "muted", children: "and above" })] })) : (_jsx(Input, { label: "Up to (\u20B9)", type: "number", min: 1, value: String(tier.upTo ?? ""), onChange: (e) => setBidIncrementTiers(bidIncrementTiers.map((t, j) => j === i ? { ...t, upTo: parseInt(e.target.value) || 0 } : t)) })) }), _jsx(Div, { className: "col-span-5", children: _jsx(Input, { label: "Increment (\u20B9)", type: "number", min: 1, value: String(tier.increment), onChange: (e) => setBidIncrementTiers(bidIncrementTiers.map((t, j) => j === i ? { ...t, increment: parseInt(e.target.value) || 0 } : t)) }) }), _jsx(Row, { centered: true, className: "col-span-2", padding: "b-xs", children: _jsx(Button, { variant: "ghost", type: "button", disabled: bidIncrementTiers.length <= 1, onClick: () => setBidIncrementTiers(bidIncrementTiers.filter((_, j) => j !== i)), "aria-label": "Remove tier", children: "\u00D7" }) })] }, i));
|
|
789
808
|
}), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setBidIncrementTiers([
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { StackedViewShellProps } from "../../../ui";
|
|
3
|
+
export interface AdminTaxCodeEditorViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
4
|
+
taxCodeId?: string;
|
|
5
|
+
/** When true, omit the StackedViewShell wrapper (SideDrawer body). */
|
|
6
|
+
embedded?: boolean;
|
|
7
|
+
onSaved?: (id: string) => void;
|
|
8
|
+
onDeleted?: () => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }: AdminTaxCodeEditorViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { useQuery } from "@tanstack/react-query";
|
|
6
|
+
import { ConfirmDeleteModal, Div, StackedViewShell, useToast } from "../../../ui";
|
|
7
|
+
import { apiClient } from "../../../http";
|
|
8
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { FormErrorSummary, applyZodIssues } from "../../../ui/forms";
|
|
10
|
+
import { FormShellContext, useFormShellState } from "../../../ui/forms/FormShell";
|
|
11
|
+
import { buildSectionsFromSchema, visibleValues } from "../../shell/build-sections";
|
|
12
|
+
import { SectionForm, useSectionFormNav } from "../../shell/SectionForm";
|
|
13
|
+
import { taxCodeFormSchema, GST_RATE_OPTIONS, HSN_CHAPTER_OPTIONS, } from "../../tax-codes/schemas/tax-code-form";
|
|
14
|
+
const __P = { p4: "p-[var(--appkit-space-4)]" };
|
|
15
|
+
const __O = { yAuto: "overflow-y-auto" };
|
|
16
|
+
/**
|
|
17
|
+
* 🛑 The delete copy says what the server will actually do.
|
|
18
|
+
*
|
|
19
|
+
* `DELETE /api/admin/tax-codes/[id]` REFUSES with a 409 while any category
|
|
20
|
+
* still points here, and names the referrers. Promising an unconditional
|
|
21
|
+
* delete and then showing a 409 is how a confirmation dialog loses its
|
|
22
|
+
* meaning — and the honest alternative (deactivate) is the one an admin
|
|
23
|
+
* usually wants anyway, since an inactive code is already skipped by
|
|
24
|
+
* `findResolvable` and gone from every picker.
|
|
25
|
+
*/
|
|
26
|
+
const DELETE_CONFIRM_TEXT = "Delete this tax code? This is refused while any category still points at it — " +
|
|
27
|
+
"set it inactive instead to retire it without breaking those categories.";
|
|
28
|
+
const TOAST = {
|
|
29
|
+
CREATED: "Tax code created.",
|
|
30
|
+
UPDATED: "Tax code updated.",
|
|
31
|
+
DELETED: "Tax code deleted.",
|
|
32
|
+
};
|
|
33
|
+
export function AdminTaxCodeEditorView({ taxCodeId, embedded, onSaved, onDeleted, ...rest }) {
|
|
34
|
+
const isEdit = Boolean(taxCodeId);
|
|
35
|
+
const { showToast } = useToast();
|
|
36
|
+
const [form, setForm] = React.useState({
|
|
37
|
+
label: "",
|
|
38
|
+
hsnCode: "",
|
|
39
|
+
// The default rate is the one 70 of this catalogue's products need.
|
|
40
|
+
gstRate: "5",
|
|
41
|
+
chapter: "9503",
|
|
42
|
+
description: "",
|
|
43
|
+
notes: "",
|
|
44
|
+
isActive: true,
|
|
45
|
+
});
|
|
46
|
+
const [deleteConfirmOpen, setDeleteConfirmOpen] = React.useState(false);
|
|
47
|
+
const patch = (partial) => setForm((prev) => Object.assign({}, prev, partial));
|
|
48
|
+
const codeQuery = useQuery({
|
|
49
|
+
queryKey: ["admin", "tax-code", taxCodeId],
|
|
50
|
+
queryFn: async () => {
|
|
51
|
+
const res = await apiClient.get(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId));
|
|
52
|
+
return res?.data ?? res;
|
|
53
|
+
},
|
|
54
|
+
enabled: isEdit,
|
|
55
|
+
});
|
|
56
|
+
React.useEffect(() => {
|
|
57
|
+
const t = codeQuery.data;
|
|
58
|
+
if (!t)
|
|
59
|
+
return;
|
|
60
|
+
patch({
|
|
61
|
+
label: t.label ?? "",
|
|
62
|
+
hsnCode: t.hsnCode ?? "",
|
|
63
|
+
/*
|
|
64
|
+
* `?? 5` would be wrong here and the distinction is the whole point of
|
|
65
|
+
* this field: rate 0 is a DELIBERATE exemption, and `t.gstRate ?? 5`
|
|
66
|
+
* would silently re-rate an exempt row to 5% the moment an admin opened
|
|
67
|
+
* it and saved anything. Test for null, not falsiness.
|
|
68
|
+
*/
|
|
69
|
+
gstRate: t.gstRate == null ? "5" : String(t.gstRate),
|
|
70
|
+
chapter: t.chapter ?? "",
|
|
71
|
+
description: t.description ?? "",
|
|
72
|
+
notes: t.notes ?? "",
|
|
73
|
+
isActive: t.isActive ?? true,
|
|
74
|
+
});
|
|
75
|
+
}, [codeQuery.data]);
|
|
76
|
+
const saveMutation = useApiMutation({
|
|
77
|
+
errorMessage: "Failed to save tax code.",
|
|
78
|
+
mutationFn: async () => {
|
|
79
|
+
const payload = {
|
|
80
|
+
label: form.label,
|
|
81
|
+
hsnCode: form.hsnCode.trim() || undefined,
|
|
82
|
+
gstRate: Number(form.gstRate),
|
|
83
|
+
chapter: form.chapter || undefined,
|
|
84
|
+
description: form.description || undefined,
|
|
85
|
+
notes: form.notes || undefined,
|
|
86
|
+
isActive: form.isActive,
|
|
87
|
+
};
|
|
88
|
+
/*
|
|
89
|
+
* PATCH, not PUT — the route is `.strict()` and partial by design, so a
|
|
90
|
+
* PUT would 404. (`AdminFeatureEditorView` uses PUT because ITS route
|
|
91
|
+
* declares PUT; copying the verb rather than reading the route is how
|
|
92
|
+
* that kind of mismatch ships.)
|
|
93
|
+
*/
|
|
94
|
+
if (isEdit) {
|
|
95
|
+
return apiClient.patch(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId), payload);
|
|
96
|
+
}
|
|
97
|
+
return apiClient.post(ADMIN_ENDPOINTS.TAX_CODES, payload);
|
|
98
|
+
},
|
|
99
|
+
onSuccess: (res) => {
|
|
100
|
+
const id = res?.data?.id ??
|
|
101
|
+
res?.id ??
|
|
102
|
+
taxCodeId;
|
|
103
|
+
showToast(isEdit ? TOAST.UPDATED : TOAST.CREATED, "success");
|
|
104
|
+
if (onSaved && id)
|
|
105
|
+
onSaved(String(id));
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
const deleteMutation = useApiMutation({
|
|
109
|
+
errorMessage: "Failed to delete tax code.",
|
|
110
|
+
mutationFn: () => apiClient.delete(ADMIN_ENDPOINTS.TAX_CODE_BY_ID(taxCodeId)),
|
|
111
|
+
onSuccess: () => {
|
|
112
|
+
showToast(TOAST.DELETED, "success");
|
|
113
|
+
if (onDeleted)
|
|
114
|
+
onDeleted();
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const isSubmitting = saveMutation.isPending || codeQuery.isLoading;
|
|
118
|
+
const sections = React.useMemo(() => buildSectionsFromSchema(taxCodeFormSchema, {
|
|
119
|
+
// `FieldUiMeta` has no `options` key — a select's VALUES are schema and
|
|
120
|
+
// its option LIST is supplied here. Same split as the bundle editor.
|
|
121
|
+
options: {
|
|
122
|
+
gstRate: [...GST_RATE_OPTIONS],
|
|
123
|
+
chapter: [{ value: "", label: "No chapter" }, ...HSN_CHAPTER_OPTIONS],
|
|
124
|
+
},
|
|
125
|
+
}), []);
|
|
126
|
+
const nav = useSectionFormNav(sections, form, { scope: "admin:tax-code-editor" });
|
|
127
|
+
const { shellCtx, setFieldError, clearErrors } = useFormShellState(taxCodeFormSchema, {
|
|
128
|
+
sections: nav.sectionMeta,
|
|
129
|
+
onGoToSection: nav.goToSection,
|
|
130
|
+
fieldToSectionIndex: nav.fieldToSectionIndex,
|
|
131
|
+
});
|
|
132
|
+
const onSubmit = () => {
|
|
133
|
+
clearErrors();
|
|
134
|
+
const draft = visibleValues(taxCodeFormSchema, form);
|
|
135
|
+
const parsed = taxCodeFormSchema.safeParse({
|
|
136
|
+
...draft,
|
|
137
|
+
hsnCode: form.hsnCode.trim() || undefined,
|
|
138
|
+
gstRate: Number(form.gstRate),
|
|
139
|
+
chapter: form.chapter || undefined,
|
|
140
|
+
description: form.description || undefined,
|
|
141
|
+
notes: form.notes || undefined,
|
|
142
|
+
});
|
|
143
|
+
if (!parsed.success) {
|
|
144
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
saveMutation.mutate();
|
|
148
|
+
};
|
|
149
|
+
const formSection = (_jsxs(FormShellContext.Provider, { value: shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: form, onChange: patch, onSubmit: onSubmit, schema: taxCodeFormSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, isLoading: isSubmitting, submitLabel: isEdit ? "Save changes" : "Create tax code", destructiveAction: isEdit
|
|
150
|
+
? { label: "Delete", onClick: () => setDeleteConfirmOpen(true) }
|
|
151
|
+
: undefined })] }));
|
|
152
|
+
const deleteModal = deleteConfirmOpen && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Tax Code", message: DELETE_CONFIRM_TEXT, onConfirm: () => {
|
|
153
|
+
deleteMutation.mutate();
|
|
154
|
+
setDeleteConfirmOpen(false);
|
|
155
|
+
}, onClose: () => setDeleteConfirmOpen(false), isDeleting: deleteMutation.isPending }));
|
|
156
|
+
if (embedded) {
|
|
157
|
+
return (_jsxs(Div, { className: `${__O.yAuto} ${__P.p4}`, children: [formSection, deleteModal] }));
|
|
158
|
+
}
|
|
159
|
+
return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Tax Code" : "New Tax Code", sections: [formSection] }), deleteModal] }));
|
|
160
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ListingLayoutProps } from "../../../ui";
|
|
3
|
+
export type AdminTaxCodesViewProps = ListingLayoutProps;
|
|
4
|
+
/**
|
|
5
|
+
* `/admin/tax-codes` — the HSN + GST rate catalogue.
|
|
6
|
+
*
|
|
7
|
+
* 🛑 No bulk actions, deliberately. The two that would be offered by habit are
|
|
8
|
+
* both wrong here: a bulk DELETE would hit the referential guard row by row
|
|
9
|
+
* and report a partial failure an admin cannot act on, and a bulk
|
|
10
|
+
* activate/deactivate across a mixed selection is a rate change applied to
|
|
11
|
+
* things nobody looked at. There are ~10 rows; each one is a per-row decision.
|
|
12
|
+
*/
|
|
13
|
+
export declare function AdminTaxCodesView({ children, ...props }: AdminTaxCodesViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { sortBy } from "@mohasinac/appkit/client";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { ListingLayout } from "../../../ui";
|
|
6
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
+
import { toRecordArray, toRelativeDate, toStringValue, } from "../hooks/useAdminListingData";
|
|
8
|
+
import { DataListingView } from "./DataListingView";
|
|
9
|
+
import { AdminTaxCodeEditorView } from "./AdminTaxCodeEditorView";
|
|
10
|
+
const PAGE_SIZE = 50;
|
|
11
|
+
function mapTaxCodeRow(item, index) {
|
|
12
|
+
const isActive = typeof item.isActive === "boolean" ? item.isActive : true;
|
|
13
|
+
const rate = typeof item.gstRate === "number" ? item.gstRate : null;
|
|
14
|
+
const hsn = toStringValue(item.hsnCode, "");
|
|
15
|
+
return {
|
|
16
|
+
id: toStringValue(item.id, `tax-${index}`),
|
|
17
|
+
primary: toStringValue(item.label, "Untitled tax code"),
|
|
18
|
+
/*
|
|
19
|
+
* The RATE leads, because it is the number an admin is checking. `0%` is
|
|
20
|
+
* rendered explicitly rather than falling back to an em-dash: a blank
|
|
21
|
+
* would read as "not set", and 0 is a deliberate exemption — the same
|
|
22
|
+
* `undefined`-vs-`0` distinction the field itself depends on. A rate that
|
|
23
|
+
* genuinely has no value says so.
|
|
24
|
+
*/
|
|
25
|
+
secondary: [
|
|
26
|
+
rate == null ? "rate not set" : `${rate}% GST`,
|
|
27
|
+
hsn ? `HSN ${hsn}` : "no HSN (exempt)",
|
|
28
|
+
toStringValue(item.chapter, "") ? `chapter ${item.chapter}` : "",
|
|
29
|
+
]
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.join(" — "),
|
|
32
|
+
status: isActive ? "Active" : "Inactive",
|
|
33
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `/admin/tax-codes` — the HSN + GST rate catalogue.
|
|
38
|
+
*
|
|
39
|
+
* 🛑 No bulk actions, deliberately. The two that would be offered by habit are
|
|
40
|
+
* both wrong here: a bulk DELETE would hit the referential guard row by row
|
|
41
|
+
* and report a partial failure an admin cannot act on, and a bulk
|
|
42
|
+
* activate/deactivate across a mixed selection is a rate change applied to
|
|
43
|
+
* things nobody looked at. There are ~10 rows; each one is a per-row decision.
|
|
44
|
+
*/
|
|
45
|
+
export function AdminTaxCodesView({ children, ...props }) {
|
|
46
|
+
if (React.Children.count(children) > 0) {
|
|
47
|
+
return (_jsx(ListingLayout, { portal: "admin", ...props, children: children }));
|
|
48
|
+
}
|
|
49
|
+
const config = {
|
|
50
|
+
portal: "admin",
|
|
51
|
+
title: "Tax Codes",
|
|
52
|
+
// No search box: this endpoint does not read `q`, and a box that accepts
|
|
53
|
+
// typing and changes nothing is worse than no box
|
|
54
|
+
// (audit-listing-search-capability's rule). ~10 rows do not need one.
|
|
55
|
+
emptyLabel: "No tax codes yet",
|
|
56
|
+
filterKeys: [],
|
|
57
|
+
defaultSort: sortBy("label", "ASC"),
|
|
58
|
+
pageSize: PAGE_SIZE,
|
|
59
|
+
queryKey: ["admin", "tax-codes", "listing"],
|
|
60
|
+
/*
|
|
61
|
+
* `?all=true` — the admin list MUST show inactive rows, because
|
|
62
|
+
* deactivating is how a code is retired and reactivating has to be
|
|
63
|
+
* possible. The route's default is active-only because its other caller is
|
|
64
|
+
* the category editor's picker, where offering an inactive code would let
|
|
65
|
+
* an admin point a category at something `findResolvable` then refuses.
|
|
66
|
+
*/
|
|
67
|
+
endpoint: `${ADMIN_ENDPOINTS.TAX_CODES}?all=true`,
|
|
68
|
+
sortOptions: [
|
|
69
|
+
{ value: "label", label: "Label A–Z" },
|
|
70
|
+
{ value: sortBy("label", "DESC"), label: "Label Z–A" },
|
|
71
|
+
{ value: "gstRate", label: "Rate, low to high" },
|
|
72
|
+
{ value: sortBy("gstRate", "DESC"), label: "Rate, high to low" },
|
|
73
|
+
{ value: "hsnCode", label: "HSN code" },
|
|
74
|
+
],
|
|
75
|
+
mapRows: (response) => toRecordArray(response.items).map(mapTaxCodeRow),
|
|
76
|
+
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
77
|
+
buildFilters: () => undefined,
|
|
78
|
+
primaryAction: {
|
|
79
|
+
label: "Add Tax Code",
|
|
80
|
+
onClick: ({ openCreatePanel }) => openCreatePanel(),
|
|
81
|
+
},
|
|
82
|
+
renderEditor: ({ editId, closePanel }) => (_jsx(AdminTaxCodeEditorView, { taxCodeId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })),
|
|
83
|
+
resolveEditorTitle: ({ isCreate }) => isCreate ? "Add Tax Code" : "Edit Tax Code",
|
|
84
|
+
};
|
|
85
|
+
return _jsx(DataListingView, { config: config });
|
|
86
|
+
}
|
|
@@ -6,7 +6,7 @@ import React, { useMemo } from "react";
|
|
|
6
6
|
import { useQueryClient } from "@tanstack/react-query";
|
|
7
7
|
import { FilterChipGroup, Heading, ListingLayout, RowActionMenu, Span, Text } from "../../../ui";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
9
|
-
import { useApiMutation } from "../../../client";
|
|
9
|
+
import { useApiMutation } from "../../../client/api/useApiMutation";
|
|
10
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
12
12
|
import { ADMIN_BULK_ACTIONS, ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Div, Heading, Row, Text } from "../../../../ui";
|
|
3
3
|
export function AdminTopProductsTable({ products, labels = {}, formatRevenue = (n) => String(n), renderProductLink, className = "", }) {
|
|
4
|
-
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }),
|
|
4
|
+
return (_jsxs(Div, { className: `border border-[var(--appkit-color-border-subtle)] sm:p-[1.5rem] ${className}`, rounded: "xl", padding: "md", surface: "default", children: [labels.title && (_jsx(Heading, { level: 3, className: "mb-4", size: "base", weight: "semibold", children: labels.title })), _jsx(Div, { children: products.map((product) => (_jsxs(Row, { className: "border-b last:border-b-0 border-[var(--appkit-color-border-subtle)]", padding: "y-sm", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: product.title }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: [product.orders, " ", labels.orders ?? "orders"] })] }), _jsxs(Div, { className: "text-right ml-4", children: [_jsx(Text, { weight: "semibold", children: formatRevenue(product.revenue) }), renderProductLink?.(product)] })] }, product.productId))) })] }));
|
|
5
5
|
}
|
|
@@ -16,9 +16,9 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
16
16
|
title: string;
|
|
17
17
|
slug: string;
|
|
18
18
|
excerpt: string;
|
|
19
|
-
/**
|
|
19
|
+
/** Test-data flag — set on the shared admin-seeded test post; swept by testDataCleanup. */
|
|
20
20
|
isTestData?: boolean;
|
|
21
|
-
/** When isTestData, the cutoff after which
|
|
21
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
|
|
22
22
|
testDataExpiresAt?: Date;
|
|
23
23
|
content: string;
|
|
24
24
|
coverImage?: MediaFieldInput;
|
|
@@ -38,8 +38,12 @@ import { useMediaUpload } from "../../media";
|
|
|
38
38
|
import { MediaUploadList } from "../../media/upload/MediaUploadList";
|
|
39
39
|
import { createCatalogueItemSchema } from "../schemas/validation";
|
|
40
40
|
/** PUBLIC endpoints — this is a buyer surface; the admin ones would 403. */
|
|
41
|
-
async function loadOptionsFrom(endpoint, query, page
|
|
42
|
-
|
|
41
|
+
async function loadOptionsFrom(endpoint, query, page,
|
|
42
|
+
/** Extra query params. Passed as an object rather than baked into
|
|
43
|
+
* `endpoint`, because this builds its own `?` and a pre-existing query
|
|
44
|
+
* string would produce `...?listingOnly=true?page=1`. */
|
|
45
|
+
extra) {
|
|
46
|
+
const params = new URLSearchParams({ page: String(page), pageSize: "25", ...extra });
|
|
43
47
|
if (query)
|
|
44
48
|
params.set("q", query);
|
|
45
49
|
const res = await apiClient.get(`${endpoint}?${params.toString()}`);
|
|
@@ -49,7 +53,14 @@ async function loadOptionsFrom(endpoint, query, page) {
|
|
|
49
53
|
hasMore: data.hasMore ?? false,
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
/*
|
|
57
|
+
* 🛑 `listingOnly=true` is load-bearing. Without it this picker offered brand
|
|
58
|
+
* and bundle rows as categories — measured against production: "Takara-Tomy",
|
|
59
|
+
* "Original Collector's Set", "Metal Fusion Duo". They share the `categories`
|
|
60
|
+
* collection and a plain listing category omits `categoryType`, so the test
|
|
61
|
+
* cannot be a query clause; the route applies it in memory.
|
|
62
|
+
*/
|
|
63
|
+
const loadCategoryOptions = (q, p) => loadOptionsFrom(CATEGORY_ENDPOINTS.LIST, q, p, { listingOnly: "true" });
|
|
53
64
|
const loadBrandOptions = (q, p) => loadOptionsFrom(BRAND_ENDPOINTS.LIST, q, p);
|
|
54
65
|
export function CatalogueItemEditorView({ item, onSaved }) {
|
|
55
66
|
const isCreate = !item;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-categories — GET /api/categories/[id]/description-template
|
|
3
|
+
*
|
|
4
|
+
* The ONE read the seller form makes when a category or condition changes.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // app/api/categories/[id]/description-template/route.ts
|
|
8
|
+
* export { categoryDescriptionTemplateGET as GET } from "@mohasinac/appkit";
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* ## Why an endpoint rather than letting the client walk the tree
|
|
12
|
+
*
|
|
13
|
+
* A description template lives on the category and INHERITS: a named-model
|
|
14
|
+
* leaf like `category-dragoon-g` carries no body of its own, and the right one
|
|
15
|
+
* is on the `category-original-engine-gear` line above it. Resolving that on
|
|
16
|
+
* the client would be one request per ancestor, on every category change, from
|
|
17
|
+
* a form — up to five round trips to answer one question.
|
|
18
|
+
*
|
|
19
|
+
* Here it is **two reads worst case and zero on a cache hit**: the leaf, then
|
|
20
|
+
* one bounded `getAll` over at most four ancestors, behind a one-hour edge
|
|
21
|
+
* cache. The response depends only on `(id, variant)` and category content
|
|
22
|
+
* changes when an admin edits it, so an hour of staleness costs a seller
|
|
23
|
+
* nothing and saves a function invocation per keystroke-free form open.
|
|
24
|
+
*
|
|
25
|
+
* ## 🛑 NO MONEY IN THIS PAYLOAD
|
|
26
|
+
*
|
|
27
|
+
* `Cache-Control: public` means a CDN holds this and anyone can fetch it, so
|
|
28
|
+
* `priceGuidance` is deliberately stripped even though it sits in the same
|
|
29
|
+
* `productDefaults` object. The band is competitor-derived market data, not a
|
|
30
|
+
* listing price — but prices are withheld from signed-out visitors precisely
|
|
31
|
+
* because hiding one number is pointless if a neighbouring surface publishes
|
|
32
|
+
* the range, and a publicly-cached endpoint is exactly such a surface. The
|
|
33
|
+
* seller's price hint is C1's job, from a surface that knows who is asking.
|
|
34
|
+
*
|
|
35
|
+
* `taxCodeId` is stripped for a different reason: nothing on the client reads
|
|
36
|
+
* it. The rate is resolved server-side by `deriveTaxonomy`, which is the whole
|
|
37
|
+
* point of the reference being server-resolved (C2a) — a client that knew the
|
|
38
|
+
* code could not do anything correct with it.
|
|
39
|
+
*/
|
|
40
|
+
import { NextResponse } from "next/server.js";
|
|
41
|
+
export declare function GET(request: Request, context: {
|
|
42
|
+
params: Promise<{
|
|
43
|
+
id: string;
|
|
44
|
+
}>;
|
|
45
|
+
}): Promise<NextResponse>;
|