@mohasinac/appkit 4.61.0 β 4.62.1
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/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +177 -9
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +22 -4
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +60 -12
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -27,10 +27,12 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
27
27
|
import React from "react";
|
|
28
28
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
29
29
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
30
|
-
import { Alert, Badge, Row, Span, Stack, Text, Toggle, useToast } from "../../../ui/index.js";
|
|
30
|
+
import { Alert, Badge, Row, Span, Stack, Text, TextLink, Toggle, useToast } from "../../../ui/index.js";
|
|
31
31
|
import { apiClient } from "../../../http/index.js";
|
|
32
32
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
33
33
|
import { DataListingView } from "./DataListingView.js";
|
|
34
|
+
import { QuickEditMenu } from "./QuickEditMenu.js";
|
|
35
|
+
import { actionIndexOverrideSchema, splitKeywords, joinKeywords, ACTION_INDEX_OVERRIDE_FIELDS, } from "../schemas/action-index-form.js";
|
|
34
36
|
const ENDPOINT = ADMIN_ENDPOINTS.ACTION_INDEX;
|
|
35
37
|
export function AdminActionIndexView({ labels = {} }) {
|
|
36
38
|
const queryClient = useQueryClient();
|
|
@@ -41,7 +43,12 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
41
43
|
staleTime: 60000,
|
|
42
44
|
});
|
|
43
45
|
const payload = query.data?.data;
|
|
44
|
-
|
|
46
|
+
/*
|
|
47
|
+
* Memoised because the `?? {}` fallback is a fresh object every render,
|
|
48
|
+
* which makes the `rows` memo below rebuild on every render β it reads
|
|
49
|
+
* `overrides` for each entry's effective `enabled`.
|
|
50
|
+
*/
|
|
51
|
+
const overrides = React.useMemo(() => payload?.control?.entries ?? {}, [payload?.control?.entries]);
|
|
45
52
|
const toggleMutation = useApiMutation({
|
|
46
53
|
errorMessage: "Could not update that entry.",
|
|
47
54
|
mutationFn: async (vars) => apiClient.patch(ENDPOINT, {
|
|
@@ -53,6 +60,44 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
53
60
|
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
54
61
|
},
|
|
55
62
|
});
|
|
63
|
+
/*
|
|
64
|
+
* Spread the EXISTING override first, exactly as the toggle does. The
|
|
65
|
+
* endpoint's `setOverride` replaces the whole override object, so sending
|
|
66
|
+
* only the edited fields would silently clear this entry's `enabled` β
|
|
67
|
+
* re-showing something an admin had deliberately hidden.
|
|
68
|
+
*/
|
|
69
|
+
const overrideMutation = useApiMutation({
|
|
70
|
+
errorMessage: "Could not save that entry.",
|
|
71
|
+
mutationFn: async (vars) => {
|
|
72
|
+
const parsed = actionIndexOverrideSchema.parse(vars.values);
|
|
73
|
+
return apiClient.patch(ENDPOINT, {
|
|
74
|
+
entryId: vars.entryId,
|
|
75
|
+
override: {
|
|
76
|
+
...overrides[vars.entryId],
|
|
77
|
+
label: parsed.label,
|
|
78
|
+
description: parsed.description,
|
|
79
|
+
keywords: splitKeywords(parsed.keywords),
|
|
80
|
+
weight: parsed.weight,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
onSuccess: () => {
|
|
85
|
+
showToast("Entry saved.", "success");
|
|
86
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
/** Named so the row-action array stays a flat list of declarations. */
|
|
90
|
+
const saveOverride = React.useCallback(async (entryId, values) => {
|
|
91
|
+
await overrideMutation.mutateAsync({ entryId, values });
|
|
92
|
+
}, [overrideMutation]);
|
|
93
|
+
const clearMutation = useApiMutation({
|
|
94
|
+
errorMessage: "Could not reset that entry.",
|
|
95
|
+
mutationFn: async (entryId) => apiClient.patch(ENDPOINT, { entryId, clear: true }),
|
|
96
|
+
onSuccess: () => {
|
|
97
|
+
showToast("Entry reset to its code default.", "success");
|
|
98
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "action-index"] });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
56
101
|
/*
|
|
57
102
|
* Ordered by weight, then label β the same ranking `matchesNavQuery` gives
|
|
58
103
|
* the search, so the admin reading this list sees entries in the order a
|
|
@@ -69,6 +114,10 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
69
114
|
href: `${entry.href}${entry.deepLink ?? ""}`,
|
|
70
115
|
permission: entry.requiredPermission ?? "β",
|
|
71
116
|
enabled: overrides[entry.id]?.enabled !== false,
|
|
117
|
+
// Seeds the edit drawer. `entry.*` is already override-merged by
|
|
118
|
+
// `mergeActionIndex`, so these are the effective values an admin sees.
|
|
119
|
+
keywords: joinKeywords(entry.keywords),
|
|
120
|
+
weight: entry.weight ?? 0,
|
|
72
121
|
})), [payload?.entries, overrides]);
|
|
73
122
|
const sync = payload?.sync;
|
|
74
123
|
return (_jsxs(Stack, { gap: "md", children: [sync && sync.orphanedOverrides.length > 0 && (_jsxs(Alert, { variant: "warning", title: "Overrides with no entry", children: [sync.orphanedOverrides.length, " saved override", sync.orphanedOverrides.length === 1 ? "" : "s", " point at entries the code no longer has (", sync.orphanedOverrides.slice(0, 4).join(", "), sync.orphanedOverrides.length > 4 ? "β¦" : "", "). They are ignored \u2014 clear them when convenient."] })), _jsx(DataListingView, { config: {
|
|
@@ -109,9 +158,19 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
109
158
|
},
|
|
110
159
|
{ key: "portal", header: "Portal" },
|
|
111
160
|
{
|
|
161
|
+
/*
|
|
162
|
+
* A LINK, not mono text.
|
|
163
|
+
*
|
|
164
|
+
* This row IS a destination β "where is it" is half of what
|
|
165
|
+
* this screen exists to answer, and the answer was printed as
|
|
166
|
+
* inert text beside a toggle. The link is also what makes the
|
|
167
|
+
* row reachable at all: every other column is metadata about
|
|
168
|
+
* the entry, so without it there is no way to go look at the
|
|
169
|
+
* thing being enabled or disabled.
|
|
170
|
+
*/
|
|
112
171
|
key: "href",
|
|
113
172
|
header: "Goes to",
|
|
114
|
-
render: (row) => (_jsx(
|
|
173
|
+
render: (row) => (_jsx(TextLink, { href: row.href, size: "xs", onClick: (e) => e.stopPropagation(), children: _jsx(Span, { size: "xs", family: "mono", children: row.href }) })),
|
|
115
174
|
},
|
|
116
175
|
{
|
|
117
176
|
key: "permission",
|
|
@@ -121,8 +180,44 @@ export function AdminActionIndexView({ labels = {} }) {
|
|
|
121
180
|
{
|
|
122
181
|
key: "enabled",
|
|
123
182
|
header: "Shown",
|
|
124
|
-
render: (row) => (_jsx(Row, { align: "center", gap: "sm", children: _jsx(Toggle, { checked: row.enabled, size: "sm",
|
|
183
|
+
render: (row) => (_jsx(Row, { align: "center", gap: "sm", children: _jsx(Toggle, { checked: row.enabled, size: "sm", ariaLabel: `Show ${row.primary} in navigation and search`, onChange: (next) => toggleMutation.mutate({ entryId: row.id, enabled: next }) }) })),
|
|
125
184
|
},
|
|
126
185
|
],
|
|
127
|
-
|
|
186
|
+
/*
|
|
187
|
+
* π This exists because the footer below already PROMISED it:
|
|
188
|
+
* "Labels, descriptions and keywords can be edited here." They
|
|
189
|
+
* could not. `PATCH /api/admin/action-index` has always accepted
|
|
190
|
+
* `label` / `description` / `keywords` / `weight` in an override
|
|
191
|
+
* (see `ActionIndexOverride`), and the only control on this screen
|
|
192
|
+
* was the Shown toggle β so the sentence described a capability the
|
|
193
|
+
* server had and the UI had never grown. Root Cause #51's shape: a
|
|
194
|
+
* label describing something other than what the surface does.
|
|
195
|
+
*
|
|
196
|
+
* `href`, `kind` and every permission field stay out, per the
|
|
197
|
+
* component docstring: repointing a built-in route is routing, and
|
|
198
|
+
* relaxing a permission is access control.
|
|
199
|
+
*/
|
|
200
|
+
renderRowActions: (row) => (_jsx(QuickEditMenu, { actions: [
|
|
201
|
+
{
|
|
202
|
+
label: "Edit entry",
|
|
203
|
+
formTitle: `Edit "${row.primary}"`,
|
|
204
|
+
schema: actionIndexOverrideSchema,
|
|
205
|
+
fields: ACTION_INDEX_OVERRIDE_FIELDS,
|
|
206
|
+
defaultValues: {
|
|
207
|
+
label: row.primary,
|
|
208
|
+
description: row.secondary,
|
|
209
|
+
keywords: row.keywords,
|
|
210
|
+
weight: row.weight,
|
|
211
|
+
},
|
|
212
|
+
onSubmit: (values) => saveOverride(row.id, values),
|
|
213
|
+
submitLabel: "Save entry",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
label: "Reset to code default",
|
|
217
|
+
separator: true,
|
|
218
|
+
destructive: true,
|
|
219
|
+
onClick: () => clearMutation.mutate(row.id),
|
|
220
|
+
},
|
|
221
|
+
] })),
|
|
222
|
+
} }), _jsx(Text, { size: "xs", color: "muted", children: "Hiding an entry removes it from the sidebar, the header search and the command palette at once. Use the row menu to edit an entry\u2019s label, description, keywords and search weight, or to reset it to its code default; a route and its required permission belong to the code." })] }));
|
|
128
223
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { useQueryClient } from "@tanstack/react-query";
|
|
5
|
-
import { Badge, Button, Div, Heading, Row, Span, Stack, Text, useToast } from "../../../ui/index.js";
|
|
5
|
+
import { Badge, Button, Div, Heading, Row, Span, Stack, Text, TextLink, useToast } from "../../../ui/index.js";
|
|
6
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
6
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
7
8
|
import { apiClient } from "../../../http/index.js";
|
|
8
9
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
@@ -31,6 +32,24 @@ export function AdminAddressClustersView(_props) {
|
|
|
31
32
|
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
32
33
|
},
|
|
33
34
|
});
|
|
35
|
+
/*
|
|
36
|
+
* π "Flag Suspicious" was ONE-WAY: there was no unflag, so a wrong call β
|
|
37
|
+
* or a cluster that turned out to be a family sharing an address β left a
|
|
38
|
+
* permanent mark with no way back from this screen.
|
|
39
|
+
*
|
|
40
|
+
* `clear_ban` has existed on the route the whole time and calls
|
|
41
|
+
* `clearBanById`; only the control was missing.
|
|
42
|
+
*/
|
|
43
|
+
const clearFlagMutation = useApiMutation({
|
|
44
|
+
errorMessage: "Failed to clear that flag.",
|
|
45
|
+
mutationFn: async (id) => {
|
|
46
|
+
await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action: "clear_ban" });
|
|
47
|
+
},
|
|
48
|
+
onSuccess: () => {
|
|
49
|
+
showToast("Flag cleared.", "success");
|
|
50
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
|
|
51
|
+
},
|
|
52
|
+
});
|
|
34
53
|
const clusters = data?.clusters ?? [];
|
|
35
|
-
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(
|
|
54
|
+
return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(TextLink, { href: addr.ownerType === "user" ? String(ROUTES.ADMIN.USER_DETAIL(addr.ownerId)) : String(ROUTES.ADMIN.STORE_DETAIL(addr.ownerId)), size: "sm", weight: "medium", truncate: true, children: [addr.ownerType === "user" ? "User" : "Store", ": ", addr.ownerId] }), addr.city && (_jsx(Text, { size: "xs", color: "muted", children: [addr.city, addr.state, addr.postalCode].filter(Boolean).join(", ") }))] }), _jsxs(Row, { gap: "xs", align: "center", children: [addr.banStatus && (_jsx(Badge, { variant: BAN_BADGE[addr.banStatus] ?? "secondary", size: "sm", children: addr.banStatus.replace(/_/g, " ") })), addr.banStatus === "suspicious" ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => clearFlagMutation.mutate(addr.id), disabled: clearFlagMutation.isPending, children: "Clear flag" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(addr.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, addr.id))) })] }, cluster.addressHash)))] }));
|
|
36
55
|
}
|
|
@@ -201,13 +201,13 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
201
201
|
required: true,
|
|
202
202
|
quick: true,
|
|
203
203
|
fields: ["title", "slug", "excerpt", "content"],
|
|
204
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Content" }), _jsx(Input, { label: "Title", value: values.title, onChange: (e) => {
|
|
204
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Content" }), _jsx(Input, { label: "Title", error: errors.title, value: values.title, onChange: (e) => {
|
|
205
205
|
const v = e.target.value;
|
|
206
206
|
onChange({ title: v, ...(!slugManual && { slug: toSlug(v) }) });
|
|
207
|
-
}, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: values.slug, onChange: (e) => {
|
|
207
|
+
}, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", error: errors.slug, value: values.slug, onChange: (e) => {
|
|
208
208
|
setSlugManual(true);
|
|
209
209
|
onChange({ slug: e.target.value });
|
|
210
|
-
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { ariaLabel: "Content", value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
|
|
210
|
+
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsx(Input, { label: "Excerpt", error: errors.excerpt, value: values.excerpt, onChange: (e) => onChange({ excerpt: e.target.value }), placeholder: "Short summary shown in listings and cards" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "muted", children: "Content" }), _jsx(RichTextEditor, { ariaLabel: "Content", value: values.content, onChange: (v) => onChange({ content: v }), placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] })] })),
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
id: "media",
|
|
@@ -216,20 +216,20 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
216
216
|
// its children, which would abort an in-flight upload.
|
|
217
217
|
keepMounted: true,
|
|
218
218
|
fields: ["coverImage", "youtubeId"],
|
|
219
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(ImageUpload, { label: "Cover Image", currentImage: values.coverImage, onUpload: (file) => upload(file, "blog", true, {
|
|
219
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(ImageUpload, { label: "Cover Image", currentImage: values.coverImage, onUpload: (file) => upload(file, "blog", true, {
|
|
220
220
|
type: "blog-cover",
|
|
221
221
|
title: values.title || values.slug,
|
|
222
222
|
category: values.category,
|
|
223
|
-
}), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "YouTube Video ID (optional)", value: values.youtubeId, onChange: (e) => onChange({ youtubeId: e.target.value }), placeholder: "e.g. dQw4w9WgXcQ", helperText: "The 11-character video ID from the YouTube URL." })] })),
|
|
223
|
+
}), onChange: (url) => onChange({ coverImage: url }) }), _jsx(Input, { label: "YouTube Video ID (optional)", error: errors.youtubeId, value: values.youtubeId, onChange: (e) => onChange({ youtubeId: e.target.value }), placeholder: "e.g. dQw4w9WgXcQ", helperText: "The 11-character video ID from the YouTube URL." })] })),
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
id: "seo",
|
|
227
227
|
label: "SEO & Tags",
|
|
228
228
|
quick: true,
|
|
229
229
|
fields: ["category", "tags", "metaTitle", "metaDescription"],
|
|
230
|
-
render: ({ values, onChange }) => {
|
|
230
|
+
render: ({ values, onChange, errors }) => {
|
|
231
231
|
const readTime = Math.max(1, Math.round(values.content.replace(/<[^>]*>/g, "").split(/\s+/).filter(Boolean).length / 200));
|
|
232
|
-
return (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "SEO & Tags" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: values.category, onValueChange: (v) => onChange({ category: v }) }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Estimated Read Time" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["~", readTime, " min (auto-calculated from content)"] })] })] }), _jsx(TagInput, { label: "Tags", value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, grading, tcg" }), _jsx(Input, { label: "Meta Title (optional)", value: values.metaTitle, onChange: (e) => onChange({ metaTitle: e.target.value }), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta Description (optional)", value: values.metaDescription, onChange: (e) => onChange({ metaDescription: e.target.value }), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 })] }));
|
|
232
|
+
return (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "SEO & Tags" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Category", error: errors.category, options: CATEGORY_OPTIONS, value: values.category, onValueChange: (v) => onChange({ category: v }) }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Estimated Read Time" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["~", readTime, " min (auto-calculated from content)"] })] })] }), _jsx(TagInput, { label: "Tags", error: errors.tags, value: values.tags, onChange: (t) => onChange({ tags: t }), placeholder: "e.g. pokemon, grading, tcg" }), _jsx(Input, { label: "Meta Title (optional)", error: errors.metaTitle, value: values.metaTitle, onChange: (e) => onChange({ metaTitle: e.target.value }), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta Description (optional)", error: errors.metaDescription, value: values.metaDescription, onChange: (e) => onChange({ metaDescription: e.target.value }), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 })] }));
|
|
233
233
|
},
|
|
234
234
|
},
|
|
235
235
|
{
|
|
@@ -237,7 +237,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
237
237
|
label: "Publish",
|
|
238
238
|
quick: true,
|
|
239
239
|
fields: ["status", "publishedAt", "authorName", "isFeatured"],
|
|
240
|
-
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Publish Settings" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: values.status, onValueChange: (v) => onChange({ status: v }) }), _jsx(Input, { label: "Publish date (optional)", value: values.publishedAt, onChange: (e) => onChange({ publishedAt: e.target.value }), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Input, { label: "Author Name", value: values.authorName, onChange: (e) => onChange({ authorName: e.target.value }), placeholder: "Author display name" }), _jsx(Toggle, { label: "Featured post", checked: values.isFeatured, onChange: (checked) => onChange({ isFeatured: checked }) }), isEdit && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => setShowDeleteConfirm(true), isLoading: deleteMutation.isPending, children: "Delete post" }) }))] })),
|
|
240
|
+
render: ({ values, onChange, errors }) => (_jsxs(Stack, { gap: "5", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Publish Settings" }), _jsxs(Div, { layout: "grid", gap: "4", className: "grid-cols-2", children: [_jsx(Select, { label: "Status", error: errors.status, options: STATUS_OPTIONS, value: values.status, onValueChange: (v) => onChange({ status: v }) }), _jsx(Input, { label: "Publish date (optional)", error: errors.publishedAt, value: values.publishedAt, onChange: (e) => onChange({ publishedAt: e.target.value }), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Input, { label: "Author Name", error: errors.authorName, value: values.authorName, onChange: (e) => onChange({ authorName: e.target.value }), placeholder: "Author display name" }), _jsx(Toggle, { label: "Featured post", checked: values.isFeatured, onChange: (checked) => onChange({ isFeatured: checked }) }), isEdit && (_jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: _jsx(Button, { type: "button", variant: "danger", size: "sm", onClick: () => setShowDeleteConfirm(true), isLoading: deleteMutation.isPending, children: "Delete post" }) }))] })),
|
|
241
241
|
},
|
|
242
242
|
];
|
|
243
243
|
const deleteModal = (_jsx(ConfirmDeleteModal, { isOpen: showDeleteConfirm, onConfirm: () => {
|
|
@@ -224,7 +224,7 @@ export function AdminCarouselEditorView({ slideId, onSaved, onDeleted, onCancel,
|
|
|
224
224
|
label: "Slide info",
|
|
225
225
|
required: true,
|
|
226
226
|
fields: ["title", "active", "order", "settings"],
|
|
227
|
-
render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => setTitle(v), required: true, placeholder: "e.g. Hot Wheels RLC Exclusives" }), _jsx(Toggle, { label: "Active (visible on homepage)", checked: active, onChange: setActive }), _jsx(Input, { label: "Display order", type: "number", value: order, onChange: (e) => setOrder(e.target.value), min: 0, placeholder: "1" }), _jsx(Select, { label: "Slide height", value: height, onChange: (e) => setHeight(e.target.value), options: HEIGHT_OPTIONS }), _jsx(Input, { label: "Autoplay delay (ms)", type: "number", value: autoplayDelayMs, onChange: (e) => setAutoplayDelayMs(e.target.value), min: 1000, max: 15000, placeholder: "4000", helperText: "How long this slide shows before auto-advancing. Default: 4000 ms." })] })),
|
|
227
|
+
render: () => (_jsxs(Stack, { gap: "md", children: [_jsx(FieldInput, { name: "title", label: "Title", value: title, onChange: (v) => setTitle(v), required: true, placeholder: "e.g. Hot Wheels RLC Exclusives" }), _jsx(Toggle, { label: "Active (visible on homepage)", checked: active, onChange: setActive }), _jsx(Input, { label: "Display order", error: form.shellCtx.errors.order, type: "number", value: order, onChange: (e) => setOrder(e.target.value), min: 0, placeholder: "1" }), _jsx(Select, { label: "Slide height", value: height, onChange: (e) => setHeight(e.target.value), options: HEIGHT_OPTIONS }), _jsx(Input, { label: "Autoplay delay (ms)", type: "number", value: autoplayDelayMs, onChange: (e) => setAutoplayDelayMs(e.target.value), min: 1000, max: 15000, placeholder: "4000", helperText: "How long this slide shows before auto-advancing. Default: 4000 ms." })] })),
|
|
228
228
|
},
|
|
229
229
|
{
|
|
230
230
|
id: "background",
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
4
|
import React, { useState, useCallback } from "react";
|
|
5
|
+
import { useRouter } from "next/navigation.js";
|
|
5
6
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, StickyToolbar } from "../../../ui/index.js";
|
|
7
|
+
import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, TextLink, StickyToolbar, Toggle, useToast } from "../../../ui/index.js";
|
|
8
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
9
|
import { useBottomActions } from "../../layout/index.js";
|
|
8
10
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
11
|
import { ADMIN_ENDPOINTS, HOMEPAGE_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
@@ -14,6 +16,8 @@ import { useAdminListing } from "../hooks/useAdminListing.js";
|
|
|
14
16
|
import { DataTable } from "./DataTable.js";
|
|
15
17
|
import { RowActionMenu } from "../../../ui/components/RowActionMenu.js";
|
|
16
18
|
import { apiClient } from "../../../http/index.js";
|
|
19
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
20
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
17
21
|
const PAGE_SIZE = 50;
|
|
18
22
|
const FILTER_KEYS = ["active"];
|
|
19
23
|
const DEFAULT_SORT = "order";
|
|
@@ -33,6 +37,8 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
33
37
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
34
38
|
const [localRows, setLocalRows] = useState([]);
|
|
35
39
|
const queryClient = useQueryClient();
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const { showToast } = useToast();
|
|
36
42
|
const { view, setView, table, searchInput, setSearchInput, commitSearch, filterOpen, setFilterOpen, openFilters, applyFilters, clearFilters, pendingFilters, setPendingFilters, activeFilterCount, hasActiveState, resetAll, rows: fetchedRows, isLoading, errorMessage, currentPage, totalPages, selection, } = useAdminListing({
|
|
37
43
|
filterKeys: FILTER_KEYS,
|
|
38
44
|
defaultSort: DEFAULT_SORT,
|
|
@@ -111,8 +117,58 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
111
117
|
setDraggingId(null);
|
|
112
118
|
reorderMutation.mutate(next.map((r) => r.id));
|
|
113
119
|
};
|
|
120
|
+
const setActiveMutation = useApiMutation({
|
|
121
|
+
errorMessage: "Could not change that slide's status.",
|
|
122
|
+
mutationFn: ({ id, active }) =>
|
|
123
|
+
// PUT, not PATCH: this route has no PATCH, and every field on
|
|
124
|
+
// `updateSlideSchema` is optional, so a lone `{ active }` is a valid
|
|
125
|
+
// partial update.
|
|
126
|
+
apiClient.put(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id), { active }),
|
|
127
|
+
onSuccess: () => {
|
|
128
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
|
|
129
|
+
// The public hero carousel reads under its own key.
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: ["carousel"] });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
/*
|
|
134
|
+
* π The bulk delete used to be gated entirely on an `onBulkDelete` PROP β
|
|
135
|
+
* and the page renders `<AdminCarouselView />` with no props at all. So
|
|
136
|
+
* `bulkActions` was always `[]`, the BulkActionBar never rendered, and
|
|
137
|
+
* DataTable was handed no selection props either, which meant no checkbox
|
|
138
|
+
* column existed to select with in the first place.
|
|
139
|
+
* `ACTIONS.ADMIN["delete-carousel"]` and its confirmation dialog were
|
|
140
|
+
* unreachable from this page.
|
|
141
|
+
*
|
|
142
|
+
* The prop is kept so an embedder can still override the behaviour, but it
|
|
143
|
+
* no longer decides whether the action EXISTS β this view owns a delete
|
|
144
|
+
* mutation already and can perfectly well call it N times.
|
|
145
|
+
*/
|
|
146
|
+
const handleBulkDelete = useCallback(async () => {
|
|
147
|
+
const ids = selection.selectedIds;
|
|
148
|
+
try {
|
|
149
|
+
if (onBulkDelete) {
|
|
150
|
+
await onBulkDelete(ids);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await Promise.all(ids.map((id) => apiClient.delete(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id))));
|
|
154
|
+
await queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
|
|
155
|
+
}
|
|
156
|
+
selection.clearSelection();
|
|
157
|
+
showToast(`${ids.length} slide${ids.length === 1 ? "" : "s"} deleted.`, "success");
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const normalized = normalizeError(err);
|
|
161
|
+
/*
|
|
162
|
+
* The selection is deliberately NOT cleared on failure. `Promise.all`
|
|
163
|
+
* rejects on the first error, so some of these ids may be gone and
|
|
164
|
+
* some may not β clearing would hide which rows are still there and
|
|
165
|
+
* leave the admin no way to retry the remainder.
|
|
166
|
+
*/
|
|
167
|
+
showToast(toUserMessage(normalized.code, undefined, { fallback: "Could not delete the selected slides." }), "error");
|
|
168
|
+
}
|
|
169
|
+
}, [onBulkDelete, selection, queryClient, showToast]);
|
|
114
170
|
const bulkActions = [
|
|
115
|
-
|
|
171
|
+
buildBulkAction(ACTIONS.ADMIN["delete-carousel"], handleBulkDelete),
|
|
116
172
|
];
|
|
117
173
|
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
118
174
|
if (hasChildren) {
|
|
@@ -138,6 +194,24 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
138
194
|
header: "Details",
|
|
139
195
|
render: (row) => row.secondary,
|
|
140
196
|
},
|
|
197
|
+
{
|
|
198
|
+
/*
|
|
199
|
+
* π Both preventDefault AND stopPropagation.
|
|
200
|
+
*
|
|
201
|
+
* `rowHrefTemplate` makes the whole row an anchor, so stopping
|
|
202
|
+
* propagation alone leaves the anchor's DEFAULT action intact β the
|
|
203
|
+
* toggle would flip and then navigate to the edit page, which reads as
|
|
204
|
+
* "clicking the switch opens the editor". A normal column cell is also
|
|
205
|
+
* outside the two `<Td>`s DataTable stops propagation on; only the
|
|
206
|
+
* selection and row-action cells get that for free.
|
|
207
|
+
*/
|
|
208
|
+
key: "active",
|
|
209
|
+
header: "Active",
|
|
210
|
+
className: "w-24",
|
|
211
|
+
render: (row) => (_jsx(Row, { align: "center", gap: "sm", role: "presentation", onClick: (e) => { e.preventDefault(); e.stopPropagation(); }, onKeyDown: (e) => e.stopPropagation(), children: _jsx(Toggle, { size: "sm", checked: row.status === "Active", ariaLabel: row.status === "Active"
|
|
212
|
+
? `${ACTIONS.ADMIN["deactivate-carousel"].ariaLabel}: ${row.primary}`
|
|
213
|
+
: `${ACTIONS.ADMIN["activate-carousel"].ariaLabel}: ${row.primary}`, onChange: (next) => setActiveMutation.mutate({ id: row.id, active: next }) }) })),
|
|
214
|
+
},
|
|
141
215
|
{
|
|
142
216
|
key: "status",
|
|
143
217
|
header: "Status",
|
|
@@ -152,7 +226,10 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
152
226
|
key: "actions",
|
|
153
227
|
header: "",
|
|
154
228
|
render: (row) => (_jsx(RowActionMenu, { actions: [
|
|
155
|
-
|
|
229
|
+
// router.push, not window.location.href β a full page load to
|
|
230
|
+
// reach a page the row already links to is strictly worse, and
|
|
231
|
+
// this is the same navigation DataTable's row anchor performs.
|
|
232
|
+
{ label: ACTIONS.ADMIN["edit-carousel"].label, onClick: () => router.push(String(ROUTES.ADMIN.CAROUSEL_EDIT(row.id))) },
|
|
156
233
|
{
|
|
157
234
|
label: ACTIONS.ADMIN["delete-carousel"].label,
|
|
158
235
|
destructive: true,
|
|
@@ -163,5 +240,20 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
163
240
|
];
|
|
164
241
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput,
|
|
165
242
|
// Search intentionally absent: this endpoint does not read `q`, so the box accepted typing and changed nothing. Restore it when the collection gains searchTxt β audit-listing-search-capability tracks it.
|
|
166
|
-
onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState
|
|
243
|
+
onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState,
|
|
244
|
+
/*
|
|
245
|
+
* π The empty state named a button that did not exist: "No carousel
|
|
246
|
+
* slides β use 'New slide' to add one", with no New slide control
|
|
247
|
+
* anywhere on the page. `/admin/carousel/new` has always been a real
|
|
248
|
+
* route and nothing linked to it.
|
|
249
|
+
*/
|
|
250
|
+
extra: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }) }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides yet", emptyAction: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }), rowHrefTemplate: "/admin/carousel/{id}/edit",
|
|
251
|
+
/*
|
|
252
|
+
* Selection was destructured and a bulk-action array built from it,
|
|
253
|
+
* and these three props were never passed β so no checkbox column
|
|
254
|
+
* rendered and nothing could be selected to act on.
|
|
255
|
+
*/
|
|
256
|
+
selectedIds: selection.selectedIdSet, onToggleSelect: (id) => selection.toggle(id), onToggleSelectAll: () => selection.toggleAll() })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true,
|
|
257
|
+
/* Rule #7: confirmation copy lives on the ActionDef, not here. */
|
|
258
|
+
title: ACTIONS.ADMIN["delete-carousel"].confirmation?.title, message: ACTIONS.ADMIN["delete-carousel"].confirmation?.body, confirmText: ACTIONS.ADMIN["delete-carousel"].confirmation?.confirmLabel, onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
|
|
167
259
|
}
|
|
@@ -22,11 +22,28 @@ const __P = {
|
|
|
22
22
|
const __O = {
|
|
23
23
|
yAuto: "overflow-y-auto",
|
|
24
24
|
};
|
|
25
|
+
/*
|
|
26
|
+
* π Not derived from `COUPON_TYPE_LABELS` on purpose, and the reason is the
|
|
27
|
+
* opposite of what the duplication framework usually says.
|
|
28
|
+
*
|
|
29
|
+
* `points_earn` is OFFERED here, so an admin can create a points offer β but
|
|
30
|
+
* the editor's discount fields (`discount.value`, `maxDiscount`,
|
|
31
|
+
* `minPurchase`) are meaningless for it, and it carries `pointsPercent`
|
|
32
|
+
* instead. Deriving the list would silently add the option the moment the
|
|
33
|
+
* union grew, which is how `AdminEventsView` ended up offering `contest`,
|
|
34
|
+
* `giveaway` and `flash-sale` β three values `EventType` never had, each
|
|
35
|
+
* matching zero rows forever (Root Cause #72).
|
|
36
|
+
*
|
|
37
|
+
* So the list is explicit and the entry is deliberate.
|
|
38
|
+
* `audit-filter-tab-enums` covers the chip row, which is the surface where
|
|
39
|
+
* coverage actually matters.
|
|
40
|
+
*/
|
|
25
41
|
const TYPE_OPTIONS = [
|
|
26
42
|
{ label: "Percentage Discount", value: "percentage" },
|
|
27
43
|
{ label: "Fixed Amount Discount", value: "fixed" },
|
|
28
44
|
{ label: "Free Shipping", value: "free_shipping" },
|
|
29
45
|
{ label: "Buy X Get Y", value: "buy_x_get_y" },
|
|
46
|
+
{ label: "Points Back (awards points, no discount)", value: "points_earn" },
|
|
30
47
|
];
|
|
31
48
|
// --- Helpers -----------------------------------------------------------------
|
|
32
49
|
function toCouponCode(str) {
|
|
@@ -9,7 +9,8 @@ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
|
9
9
|
import { apiClient } from "../../../http/index.js";
|
|
10
10
|
import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
-
import { useToast } from "../../../ui/index.js";
|
|
12
|
+
import { useToast, TextLink } from "../../../ui/index.js";
|
|
13
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
14
|
async function loadStoreOptions(query, page) {
|
|
14
15
|
const params = new URLSearchParams({ page: String(page), pageSize: "25" });
|
|
15
16
|
if (query)
|
|
@@ -105,5 +106,5 @@ export function AdminFulfillmentView({ initialStoreId, initialStoreName, }) {
|
|
|
105
106
|
setStoreId(null);
|
|
106
107
|
setOrders([]);
|
|
107
108
|
}, children: "Change Store" }))] }), !storeId && (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", color: "muted", children: "Select a store to view its fulfilment queue." }), _jsx(PaginatedSelect, { value: null, onChange: (v) => { if (v)
|
|
108
|
-
setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(
|
|
109
|
+
setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(TextLink, { href: String(ROUTES.ADMIN.ORDER_DETAIL(order.id)), size: "sm", weight: "medium", children: ["#", order.id] }), _jsx(Badge, { variant: statusBadgeVariant(order.status), children: order.status }), order.pickedAt && _jsx(Badge, { variant: "success", children: "Picked" }), order.packedAt && _jsx(Badge, { variant: "success", children: "Packed" })] }), _jsxs(Text, { size: "xs", color: "muted", children: [order.userName, order.items && order.items.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 ", order.items.map((i) => i.productTitle).join(", ")] }))] }), order.assignedWorkerId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Assigned to: ", order.assignedWorkerId] })), _jsxs(Row, { gap: "xs", children: [!order.pickedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPicked(order.id), isLoading: actionLoadingId === order.id + ":picked", children: "Mark Picked" })), order.pickedAt && !order.packedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPacked(order.id), isLoading: actionLoadingId === order.id + ":packed", children: "Mark Packed" })), order.assignedWorkerId && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleUnassign(order.id), isLoading: actionLoadingId === order.id + ":unassign", children: "Unassign" }))] })] }, order.id)))] })), initialStoreName && !storeId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Previously viewing: ", initialStoreName] }))] }));
|
|
109
110
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button, Card, Disclaimer, Div, Heading, Row, Stack, Text } from "../../../ui/index.js";
|
|
5
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
6
|
+
import { Icon } from "../../../ui/components/Icon.js";
|
|
7
|
+
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
8
|
+
/**
|
|
9
|
+
* Admin β Finance β Invoicing: the GSTR-1 draft export (Phase 7d).
|
|
10
|
+
*
|
|
11
|
+
* ## π Why this page exists at all
|
|
12
|
+
*
|
|
13
|
+
* The export is an API route, and an API route with no surface is a dead end β
|
|
14
|
+
* reachable only by someone who already knows the URL, which is Root Cause
|
|
15
|
+
* #56's shape (16 of 70 listing views had no way to open a row). The person
|
|
16
|
+
* who needs this is an accountant at quarter end, and they will not type a
|
|
17
|
+
* query string.
|
|
18
|
+
*
|
|
19
|
+
* ## π It downloads; it does not file
|
|
20
|
+
*
|
|
21
|
+
* Nothing here talks to the GST portal β there is no webhook facility in
|
|
22
|
+
* existence and no GSP contract. The copy says "draft" everywhere it can,
|
|
23
|
+
* because a page that implied a return had been FILED when it had not is the
|
|
24
|
+
* most consequential thing this feature could get wrong.
|
|
25
|
+
*/
|
|
26
|
+
/** Indian FY quarters, which is the unit a GSTR-1 is filed in. */
|
|
27
|
+
const QUARTERS = [
|
|
28
|
+
{ label: "Q1 β Apr to Jun", fromMonth: "04-01", toMonth: "06-30" },
|
|
29
|
+
{ label: "Q2 β Jul to Sep", fromMonth: "07-01", toMonth: "09-30" },
|
|
30
|
+
{ label: "Q3 β Oct to Dec", fromMonth: "10-01", toMonth: "12-31" },
|
|
31
|
+
/*
|
|
32
|
+
* π Q4 SPANS A CALENDAR YEAR BOUNDARY. JanβMar of FY 26-27 is calendar
|
|
33
|
+
* 2027, not 2026 β the same off-by-one the `financialYearLabel` helper
|
|
34
|
+
* exists to contain, surfacing here because a quarter picker has to know it
|
|
35
|
+
* too. `yearOffset` is what carries it; dropping it would export the wrong
|
|
36
|
+
* three months with no visible error.
|
|
37
|
+
*/
|
|
38
|
+
{ label: "Q4 β Jan to Mar", fromMonth: "01-01", toMonth: "03-31", yearOffset: 1 },
|
|
39
|
+
];
|
|
40
|
+
function defaultFyStartYear() {
|
|
41
|
+
const now = new Date();
|
|
42
|
+
// Zero-based month: April is 3. JanβMar belongs to the FY that started last
|
|
43
|
+
// calendar year.
|
|
44
|
+
return now.getMonth() >= 3 ? now.getFullYear() : now.getFullYear() - 1;
|
|
45
|
+
}
|
|
46
|
+
export function AdminInvoicingView() {
|
|
47
|
+
const [fyStart, setFyStart] = useState(String(defaultFyStartYear()));
|
|
48
|
+
const [from, setFrom] = useState("");
|
|
49
|
+
const [to, setTo] = useState("");
|
|
50
|
+
const applyQuarter = (q) => {
|
|
51
|
+
const start = Number(fyStart);
|
|
52
|
+
if (!Number.isFinite(start))
|
|
53
|
+
return;
|
|
54
|
+
const offset = "yearOffset" in q ? q.yearOffset : 0;
|
|
55
|
+
setFrom(`${start + offset}-${q.fromMonth}`);
|
|
56
|
+
setTo(`${start + offset}-${q.toMonth}`);
|
|
57
|
+
};
|
|
58
|
+
const href = from && to
|
|
59
|
+
? `${ADMIN_ENDPOINTS.INVOICING_GSTR1}?from=${from}&to=${to}`
|
|
60
|
+
: undefined;
|
|
61
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsxs(Div, { children: [_jsx(Heading, { level: 1, children: "Invoicing" }), _jsx(Text, { color: "muted", size: "sm", children: "Export the invoice and credit-note register for a period, as a draft for your accountant." })] }), _jsx(Card, { padding: "md", children: _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, children: "GSTR-1 draft" }), _jsx(Disclaimer, { id: "accounting-output" }), _jsx(Row, { gap: "sm", wrap: true, children: _jsx(Div, { className: "w-40", children: _jsx(FieldInput, { name: "fyStart", label: "FY starting", value: fyStart, onChange: setFyStart, hint: "e.g. 2026 for FY 26-27" }) }) }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Quarter" }), _jsx(Row, { gap: "sm", wrap: true, children: QUARTERS.map((q) => (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => applyQuarter(q), children: q.label }, q.label))) })] }), _jsxs(Row, { gap: "sm", wrap: true, children: [_jsx(Div, { className: "w-48", children: _jsx(FieldInput, { name: "from", label: "From", value: from, onChange: setFrom }) }), _jsx(Div, { className: "w-48", children: _jsx(FieldInput, { name: "to", label: "To", value: to, onChange: setTo }) })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsxs(Button, { type: "button", variant: "primary", disabled: !href, onClick: () => {
|
|
62
|
+
if (href)
|
|
63
|
+
window.location.href = href;
|
|
64
|
+
}, children: [_jsx(Icon, { name: "download", size: "sm" }), "Download draft CSV"] }), !href && (_jsx(Text, { color: "muted", size: "xs", children: "Pick a quarter, or enter both dates as YYYY-MM-DD." }))] }), _jsx(Text, { color: "muted", size: "xs", children: "Credit notes appear as negative rows, so the TOTAL row is net of refunds. Any paid order in the window that carries no invoice number is listed at the bottom of the file as a warning \u2014 those are not included in the totals and need looking at." })] }) })] }));
|
|
65
|
+
}
|