@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
|
@@ -10,6 +10,8 @@ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
|
10
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
11
11
|
import { ROUTES } from "../../../constants/index.js";
|
|
12
12
|
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
|
|
13
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
14
|
+
import { apiClient } from "../../../http/index.js";
|
|
13
15
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
14
16
|
const DEFAULT_SORT = "-createdAt";
|
|
15
17
|
const SORT_OPTIONS = [
|
|
@@ -40,6 +42,22 @@ const COLUMNS = [
|
|
|
40
42
|
},
|
|
41
43
|
];
|
|
42
44
|
export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete, onSetDefault, }) {
|
|
45
|
+
/*
|
|
46
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
47
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
48
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
49
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
50
|
+
*
|
|
51
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
52
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
53
|
+
* the override key rather than write back a captured value.
|
|
54
|
+
*/
|
|
55
|
+
const activeFlag = useOptimisticRowFlag({
|
|
56
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID(id), { isActive: next }),
|
|
57
|
+
invalidate: [["seller", "shipping-configs", "listing"]],
|
|
58
|
+
successMessage: (next) => (next ? "Shipping option activated." : "Shipping option deactivated."),
|
|
59
|
+
errorMessage: "Could not change that shipping option's status.",
|
|
60
|
+
});
|
|
43
61
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
44
62
|
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
45
63
|
endpoint: SELLER_ENDPOINTS.SHIPPING_CONFIG_BY_ID,
|
|
@@ -113,12 +131,26 @@ export function SellerShippingConfigsView({ onCreateClick, onEditClick, onDelete
|
|
|
113
131
|
method: String(item.method ?? ""),
|
|
114
132
|
estimatedDays: typeof item.estimatedDays === "number" ? item.estimatedDays : null,
|
|
115
133
|
isDefault: Boolean(item.isDefault),
|
|
116
|
-
|
|
134
|
+
// Resolved through the pending override so the Status badge and the
|
|
135
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
136
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `sc-${index}`), Boolean(item.isActive)),
|
|
117
137
|
})),
|
|
118
138
|
getTotal: (response, mappedRows) => (typeof response.total === "number" ? response.total : mappedRows.length),
|
|
119
139
|
buildFilters: () => undefined,
|
|
120
140
|
primaryAction: { label: "New Config", onClick: handleCreate },
|
|
121
141
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
142
|
+
/*
|
|
143
|
+
* 🛑 `isActive` was a read-only badge and the update schema was
|
|
144
|
+
* built to accept it alone. A menu entry rather than a switch here,
|
|
145
|
+
* to match the other actions in this menu — the menu is already
|
|
146
|
+
* click-stopped in all three renderers.
|
|
147
|
+
*/
|
|
148
|
+
{
|
|
149
|
+
label: row.isActive ? "Deactivate" : "Activate",
|
|
150
|
+
separator: true,
|
|
151
|
+
disabled: activeFlag.pendingId === row.id,
|
|
152
|
+
onClick: () => { void activeFlag.set(row.id, !row.isActive); },
|
|
153
|
+
},
|
|
122
154
|
{ label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
|
|
123
155
|
...(!row.isDefault
|
|
124
156
|
? [{ label: "Set as Default", disabled: settingDefaultId === row.id, onClick: () => handleSetDefault(row.id) }]
|
|
@@ -9,6 +9,7 @@ import { StoreAddressSelectorCreate } from "../../stores/components/StoreAddress
|
|
|
9
9
|
import { SectionForm, useSectionFormNav } from "../../shell/index.js";
|
|
10
10
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
12
13
|
const shippingDraftSchema = z.object({
|
|
13
14
|
customCarrierName: z.string().min(1, "Carrier name is required"),
|
|
14
15
|
customShippingPrice: z.string(),
|
|
@@ -82,7 +83,7 @@ export function SellerShippingView({ apiBase = SELLER_ENDPOINTS.SHIPPING }) {
|
|
|
82
83
|
if (Array.isArray(json?.issues) && json.issues.length > 0) {
|
|
83
84
|
applyZodIssues(json.issues, setFieldError);
|
|
84
85
|
}
|
|
85
|
-
setError(json?.
|
|
86
|
+
setError(toUserMessage(json?.code, undefined, { fallback: "We couldn't save your shipping settings." }));
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
clearErrors();
|
|
@@ -11,6 +11,8 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
|
11
11
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
12
12
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
13
|
import { toRecordArray, toStringValue } from "../hooks/useSellerListingData.js";
|
|
14
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
15
|
+
import { apiClient } from "../../../http/index.js";
|
|
14
16
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
15
17
|
const DEFAULT_SORT = "displayOrder";
|
|
16
18
|
const SORT_OPTIONS = [
|
|
@@ -37,6 +39,22 @@ const COLUMNS = [
|
|
|
37
39
|
},
|
|
38
40
|
];
|
|
39
41
|
export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete, onBulkDelete, onBulkActivate, onBulkDeactivate, }) {
|
|
42
|
+
/*
|
|
43
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
44
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
45
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
46
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
47
|
+
*
|
|
48
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
49
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
50
|
+
* the override key rather than write back a captured value.
|
|
51
|
+
*/
|
|
52
|
+
const activeFlag = useOptimisticRowFlag({
|
|
53
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.STORE_CATEGORY_BY_ID(id), { isActive: next }),
|
|
54
|
+
invalidate: [["seller", "store-categories", "listing"]],
|
|
55
|
+
successMessage: (next) => (next ? "Category activated." : "Category deactivated."),
|
|
56
|
+
errorMessage: "Could not change that category's status.",
|
|
57
|
+
});
|
|
40
58
|
const { showToast } = useToast();
|
|
41
59
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
42
60
|
const { deletingId, handleDelete: performDelete } = useEntityDelete({
|
|
@@ -79,7 +97,9 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
|
|
|
79
97
|
label: String(item.label ?? ""),
|
|
80
98
|
slug: String(item.slug ?? ""),
|
|
81
99
|
productCount: Number(item.productIds?.length ?? 0),
|
|
82
|
-
|
|
100
|
+
// Resolved through the pending override so the Status badge and the
|
|
101
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
102
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `cat-${index}`), Boolean(item.isActive)),
|
|
83
103
|
})),
|
|
84
104
|
getTotal: (response, rows) => (typeof response.total === "number" ? response.total : rows.length),
|
|
85
105
|
buildFilters: () => undefined,
|
|
@@ -93,6 +113,18 @@ export function SellerStoreCategoriesView({ onCreateClick, onEditClick, onDelete
|
|
|
93
113
|
return actions;
|
|
94
114
|
},
|
|
95
115
|
renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
|
|
116
|
+
/*
|
|
117
|
+
* 🛑 `isActive` was a read-only badge and the update schema was
|
|
118
|
+
* built to accept it alone. A menu entry rather than a switch here,
|
|
119
|
+
* to match the other actions in this menu — the menu is already
|
|
120
|
+
* click-stopped in all three renderers.
|
|
121
|
+
*/
|
|
122
|
+
{
|
|
123
|
+
label: row.isActive ? "Deactivate" : "Activate",
|
|
124
|
+
separator: true,
|
|
125
|
+
disabled: activeFlag.pendingId === row.id,
|
|
126
|
+
onClick: () => { void activeFlag.set(row.id, !row.isActive); },
|
|
127
|
+
},
|
|
96
128
|
{
|
|
97
129
|
label: ACTIONS.STORE["edit-listing"].label,
|
|
98
130
|
onClick: () => onEditClick
|
|
@@ -24,7 +24,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
* close a complaint made against them.
|
|
25
25
|
*/
|
|
26
26
|
import React from "react";
|
|
27
|
-
import {
|
|
27
|
+
import { useQuery } from "@tanstack/react-query";
|
|
28
|
+
import { Badge, RecordDetailModal, Span, Stack, Text } from "../../../ui/index.js";
|
|
29
|
+
import { apiClient } from "../../../http/index.js";
|
|
28
30
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
29
31
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
30
32
|
import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names.js";
|
|
@@ -38,7 +40,58 @@ const STATUS_VARIANT = {
|
|
|
38
40
|
resolved: "success",
|
|
39
41
|
closed: "default",
|
|
40
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* One ticket, read-only, in the shared detail modal.
|
|
45
|
+
*
|
|
46
|
+
* Its own component so the query only runs once a row has actually been
|
|
47
|
+
* opened — the list is the common case and nobody should pay a per-ticket
|
|
48
|
+
* fetch to scroll it.
|
|
49
|
+
*/
|
|
50
|
+
function SellerSupportTicketModal({ ticketId, onClose, }) {
|
|
51
|
+
const { data, isLoading } = useQuery({
|
|
52
|
+
queryKey: ["store", "support-ticket", ticketId],
|
|
53
|
+
queryFn: () => apiClient.get(SELLER_ENDPOINTS.SUPPORT_TICKET_BY_ID(ticketId)),
|
|
54
|
+
staleTime: 30000,
|
|
55
|
+
});
|
|
56
|
+
const ticket = data?.ticket;
|
|
57
|
+
const messages = Array.isArray(ticket?.messages)
|
|
58
|
+
? ticket.messages
|
|
59
|
+
: [];
|
|
60
|
+
return (_jsx(RecordDetailModal, { isOpen: true, onClose: onClose, title: toStringValue(ticket?.subject, isLoading ? "Loading…" : "Ticket"), badges: ticket
|
|
61
|
+
? [
|
|
62
|
+
{
|
|
63
|
+
label: toStringValue(ticket.status).replace(/_/g, " "),
|
|
64
|
+
variant: STATUS_VARIANT[toStringValue(ticket.status)] ?? "default",
|
|
65
|
+
},
|
|
66
|
+
{ label: toStringValue(ticket.category).replace(/_/g, " "), variant: "default" },
|
|
67
|
+
]
|
|
68
|
+
: undefined, description: toStringValue(ticket?.description), fields: ticket
|
|
69
|
+
? [
|
|
70
|
+
{ label: "Ticket", value: toStringValue(ticket.id) },
|
|
71
|
+
{ label: "Raised", value: toRelativeDate(ticket.createdAt) },
|
|
72
|
+
{ label: "Last update", value: toRelativeDate(ticket.updatedAt ?? ticket.createdAt) },
|
|
73
|
+
{ label: "Priority", value: toStringValue(ticket.priority, "—") },
|
|
74
|
+
]
|
|
75
|
+
: undefined, items: messages.length > 0
|
|
76
|
+
? {
|
|
77
|
+
heading: `Thread (${messages.length})`,
|
|
78
|
+
entries: messages.map((m) => ({
|
|
79
|
+
/*
|
|
80
|
+
* The author's ROLE, not their name. A seller reading a
|
|
81
|
+
* complaint made against them does not need the buyer's
|
|
82
|
+
* identity to follow the thread, and those fields are
|
|
83
|
+
* PII-encrypted at rest precisely so they are not handed out
|
|
84
|
+
* by default.
|
|
85
|
+
*/
|
|
86
|
+
title: m.authorRole === "admin" ? "Support team" : "Buyer",
|
|
87
|
+
subtitle: toStringValue(m.body),
|
|
88
|
+
trailing: toRelativeDate((m.createdAt ?? null)),
|
|
89
|
+
})),
|
|
90
|
+
}
|
|
91
|
+
: undefined }));
|
|
92
|
+
}
|
|
41
93
|
export function SellerSupportView({ labels = {} }) {
|
|
94
|
+
const [detailId, setDetailId] = React.useState(null);
|
|
42
95
|
const mapRows = React.useCallback((response) => {
|
|
43
96
|
const items = Array.isArray(response?.items) ? response.items : [];
|
|
44
97
|
return items.map((raw) => {
|
|
@@ -96,5 +149,19 @@ export function SellerSupportView({ labels = {} }) {
|
|
|
96
149
|
},
|
|
97
150
|
{ key: "updated", header: "Updated" },
|
|
98
151
|
],
|
|
99
|
-
|
|
152
|
+
/*
|
|
153
|
+
* 🛑 This list was a DEAD END, and unlike most of the ones fixed
|
|
154
|
+
* alongside it that was not a pure UI gap: a seller could read a
|
|
155
|
+
* subject and a 140-character slice of the description and never
|
|
156
|
+
* open the thread, under the footer below promising the thread is
|
|
157
|
+
* where replies happen. `/api/store/support` was GET-list-only with
|
|
158
|
+
* no `[id]` sibling at all, so the route had to be built first.
|
|
159
|
+
*
|
|
160
|
+
* A modal rather than a page: the seller cannot act on these — a
|
|
161
|
+
* ticket raised ABOUT a store is not one its owner can reply to or
|
|
162
|
+
* close — so there is nothing here a route would add, and this is
|
|
163
|
+
* the same `RecordDetailModal` the other dead ends were fixed with.
|
|
164
|
+
*/
|
|
165
|
+
onRowClick: (row) => setDetailId(row.id),
|
|
166
|
+
} }), detailId && (_jsx(SellerSupportTicketModal, { ticketId: detailId, onClose: () => setDetailId(null) })), _jsx(Text, { size: "xs", color: "muted", children: "Replies and status changes happen in the ticket thread with our support team \u2014 a ticket raised about your store is not one you can close." })] }));
|
|
100
167
|
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
3
|
+
// The defining module, not a barrel (Root Cause #18). `hasEncPrefix` is a pure
|
|
4
|
+
// string test with no crypto in its import chain, so it is client-safe.
|
|
5
|
+
import { hasEncPrefix } from "../../../security/pii-mask.js";
|
|
3
6
|
export function toRecordArray(value) {
|
|
4
7
|
return Array.isArray(value) ? value.filter(Boolean) : [];
|
|
5
8
|
}
|
|
6
9
|
export function toStringValue(value, fallback = "-") {
|
|
7
10
|
if (typeof value === "string") {
|
|
11
|
+
/*
|
|
12
|
+
* 🛑 Ciphertext is not a display value, and this is the last line of
|
|
13
|
+
* defence rather than the fix.
|
|
14
|
+
*
|
|
15
|
+
* `OrderRepository.mapDoc` deliberately does not decrypt, so any route
|
|
16
|
+
* that forgets `revealOrders`/`maskedOrders` hands this function a stored
|
|
17
|
+
* `enc:v1:…` blob — and it is a `string`, so it rendered verbatim.
|
|
18
|
+
* Measured live in the seller's order list, where the row subtitle read
|
|
19
|
+
* `enc:v1:8plr3ZzKBqejPPti:MCZsrfOidLkSL6EogbxlCzU=:…` where the buyer's
|
|
20
|
+
* name belongs.
|
|
21
|
+
*
|
|
22
|
+
* This is a RECURRING class, not one slip: eight separate files in appkit
|
|
23
|
+
* carry a comment recording the same leak, and one of them had already
|
|
24
|
+
* baked `enc:v1:…` into 383 notification bodies. The route fix is the real
|
|
25
|
+
* repair; this makes the NEXT one degrade to "-" instead of garbage.
|
|
26
|
+
*
|
|
27
|
+
* `hasEncPrefix` rather than `isPiiEncrypted`: its own docstring says this
|
|
28
|
+
* is the check the display masks want, because a settings-encrypted or
|
|
29
|
+
* malformed envelope is exactly as unshowable as a PII one.
|
|
30
|
+
*/
|
|
31
|
+
if (hasEncPrefix(value))
|
|
32
|
+
return fallback;
|
|
8
33
|
return value.trim() || fallback;
|
|
9
34
|
}
|
|
10
35
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -325,6 +325,25 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
325
325
|
}
|
|
326
326
|
void onSubmit();
|
|
327
327
|
}, [shellCtx, onSubmit, erroringSectionIds, fieldErrors, openIds, setOpenIds, expandMode]);
|
|
328
|
+
/**
|
|
329
|
+
* Open a section if it is closed, then scroll to it.
|
|
330
|
+
*
|
|
331
|
+
* Named rather than inlined in the rail's `onClick`: inline it sat five
|
|
332
|
+
* braces deep inside the JSX, and it is the same open-then-scroll the
|
|
333
|
+
* submit handler above performs for the first erroring section — so the
|
|
334
|
+
* two now read as the same operation rather than two similar ones.
|
|
335
|
+
*
|
|
336
|
+
* 🛑 Opening is CONDITIONAL on purpose. In `single` mode an unconditional
|
|
337
|
+
* `setOpenIds([id])` would COLLAPSE the section you are already in when you
|
|
338
|
+
* click its own rail entry, so the rail would read as having closed the
|
|
339
|
+
* form.
|
|
340
|
+
*/
|
|
341
|
+
const openAndScrollTo = useCallback((id) => {
|
|
342
|
+
if (!openIds.includes(id)) {
|
|
343
|
+
setOpenIds(expandMode === "single" ? [id] : [...openIds, id]);
|
|
344
|
+
}
|
|
345
|
+
scrollToSection(id);
|
|
346
|
+
}, [openIds, setOpenIds, expandMode]);
|
|
328
347
|
useFormBottomActions({
|
|
329
348
|
onSubmit: handleSubmit,
|
|
330
349
|
onCancel,
|
|
@@ -391,21 +410,6 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
391
410
|
* sections").
|
|
392
411
|
*/
|
|
393
412
|
const showRail = ordered.length >= 3;
|
|
394
|
-
/**
|
|
395
|
-
* One rail click: open the section if it is not already, then scroll to it.
|
|
396
|
-
* Named rather than inline in the button's `onClick`, where the `if` body
|
|
397
|
-
* sat a brace too deep inside the mapped JSX.
|
|
398
|
-
*
|
|
399
|
-
* Opening is conditional on purpose — in `single` mode an unconditional
|
|
400
|
-
* `setOpenIds([id])` would COLLAPSE the section you are already in when you
|
|
401
|
-
* click its own rail entry, so the rail would look like it closed the form.
|
|
402
|
-
*/
|
|
403
|
-
const openAndScrollTo = (id) => {
|
|
404
|
-
if (!openIds.includes(id)) {
|
|
405
|
-
setOpenIds(expandMode === "single" ? [id] : [...openIds, id]);
|
|
406
|
-
}
|
|
407
|
-
scrollToSection(id);
|
|
408
|
-
};
|
|
409
413
|
/*
|
|
410
414
|
* `sticky` under the measured header, and it publishes NO height: per the
|
|
411
415
|
* three-tier rule a sticky element CONSUMES the tier and never joins it.
|
|
@@ -444,7 +448,7 @@ export function SectionForm({ sections, values, onChange, onSubmit, submitLabel
|
|
|
444
448
|
forceOpenIds.includes(section.id);
|
|
445
449
|
return (_jsxs(Button, { type: "button",
|
|
446
450
|
/* `ghost`, not `bare` — UI_BUTTON has no `bare` variant. */
|
|
447
|
-
variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => openAndScrollTo(section.id), children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
|
|
451
|
+
variant: "ghost", size: "sm", justify: "between", gap: "sm", className: `w-full text-left ${isOpen ? "appkit-section-rail__item--active" : ""}`, "aria-current": isOpen ? "true" : undefined, onClick: () => openAndScrollTo(section.id), children: [_jsx(Span, { size: "sm", color: isOpen ? "primary" : "muted", children: section.label }), submitAttempted && errorCount > 0 && (_jsx(Span, { size: "xs", color: "error", weight: "semibold", children: errorCount }))] }, section.id));
|
|
448
452
|
}) }) }) })) : null;
|
|
449
453
|
const panels = (_jsxs(Stack, { gap: "md", children: [ordered.map((section) => {
|
|
450
454
|
const isRequired = section.required === true;
|
|
@@ -288,7 +288,20 @@ function renderField(field, ctx, opts) {
|
|
|
288
288
|
* case that most needs searching.
|
|
289
289
|
*/
|
|
290
290
|
if (options.length > PAGINATED_SELECT_THRESHOLD) {
|
|
291
|
-
|
|
291
|
+
/*
|
|
292
|
+
* 🛑 The error block here used to be hand-rolled, and was the only
|
|
293
|
+
* field in this generator whose error was not wired like the rest:
|
|
294
|
+
* a `<Text>` with no `id`, nothing pointing `aria-describedby` at it,
|
|
295
|
+
* no `aria-invalid` on the control, and — unlike every `Field*`
|
|
296
|
+
* branch below — the help text was still rendered underneath the
|
|
297
|
+
* error rather than replaced by it.
|
|
298
|
+
*
|
|
299
|
+
* `PaginatedSelect` now takes `name`/`error` and owns all of that,
|
|
300
|
+
* so hand it the error and delete the copy. This is the one branch
|
|
301
|
+
* that every schema-generated form shares, so the fix lands on all
|
|
302
|
+
* of them at once rather than per call site.
|
|
303
|
+
*/
|
|
304
|
+
return (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "muted", children: [label, required ? " *" : ""] }), _jsx(PaginatedSelect, { name: name, error: ctx.errors[name], value: typeof value === "string" ? value : null, onChange: (v) => set(v ?? ""), options: options, ariaLabel: label, placeholder: `Select ${label.toLowerCase()}…` }), meta.help && !ctx.errors[name] && (_jsx(Text, { size: "xs", color: "muted", children: meta.help }))] }, name));
|
|
292
305
|
}
|
|
293
306
|
return (_jsx(FieldSelect, { name: name, label: label, hint: meta.help, required: required, value: typeof value === "string" ? value : "", options: options, onChange: set }, name));
|
|
294
307
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
6
|
import { Alert, Badge, Button, Div, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui/index.js";
|
|
6
7
|
import { apiClient } from "../../../http/index.js";
|
|
@@ -11,6 +12,7 @@ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
|
|
|
11
12
|
import { ValidationError } from "../../../errors/validation-error.js";
|
|
12
13
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
13
14
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
15
|
+
import { QuickEditMenu } from "../../admin/components/QuickEditMenu.js";
|
|
14
16
|
import { useShipmentItems } from "../hooks/useShipments.js";
|
|
15
17
|
import { ShipmentItemLinkModal } from "./ShipmentItemLinkModal.js";
|
|
16
18
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
@@ -32,6 +34,30 @@ function parseBulkText(text) {
|
|
|
32
34
|
})
|
|
33
35
|
.filter((row) => row.title.length > 0);
|
|
34
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The three fields a lot item can be corrected on after import.
|
|
39
|
+
*
|
|
40
|
+
* Deliberately NOT the whole of `shipmentItemBaseSchema`: `isForSelfUse`
|
|
41
|
+
* decides whether `price` is required at all and flipping it from a
|
|
42
|
+
* three-field drawer would silently change what the row means to the
|
|
43
|
+
* projections, and `categorySlugs`/`brandSlug` belong to the link flow that
|
|
44
|
+
* already owns them.
|
|
45
|
+
*/
|
|
46
|
+
const shipmentItemQuickEditSchema = z.object({
|
|
47
|
+
title: z.string().trim().min(1, "Item title is required"),
|
|
48
|
+
quantity: z.coerce.number().int().min(1, "Quantity must be at least 1."),
|
|
49
|
+
price: z.coerce.number().min(0, "Price cannot be negative."),
|
|
50
|
+
});
|
|
51
|
+
const SHIPMENT_ITEM_QUICK_FIELDS = [
|
|
52
|
+
{ name: "title", label: "Title", type: "text", required: true },
|
|
53
|
+
{ name: "quantity", label: "Quantity", type: "number", required: true },
|
|
54
|
+
{
|
|
55
|
+
name: "price",
|
|
56
|
+
label: "Projected sale price",
|
|
57
|
+
type: "number",
|
|
58
|
+
helperText: "Feeds this lot's projected revenue, and every profit figure on the projections screen.",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
35
61
|
export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
36
62
|
const [page, setPage] = React.useState(1);
|
|
37
63
|
const [pageSize, setPageSize] = React.useState(50);
|
|
@@ -113,6 +139,19 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
113
139
|
mutationFn: (itemId) => apiClient.delete(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId)),
|
|
114
140
|
onSuccess: () => refetch(),
|
|
115
141
|
});
|
|
142
|
+
const editItemMutation = useApiMutation({
|
|
143
|
+
successMessage: "Item updated",
|
|
144
|
+
errorMessage: "Could not update that item.",
|
|
145
|
+
mutationFn: ({ itemId, values }) => {
|
|
146
|
+
const parsed = shipmentItemQuickEditSchema.parse(values);
|
|
147
|
+
return apiClient.patch(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId), parsed);
|
|
148
|
+
},
|
|
149
|
+
onSuccess: () => refetch(),
|
|
150
|
+
});
|
|
151
|
+
/** Named so the row-action array stays a flat list of declarations. */
|
|
152
|
+
const saveItemEdit = React.useCallback(async (itemId, values) => {
|
|
153
|
+
await editItemMutation.mutateAsync({ itemId, values });
|
|
154
|
+
}, [editItemMutation]);
|
|
116
155
|
const unlinkItemMutation = useApiMutation({
|
|
117
156
|
successMessage: "Item unlinked",
|
|
118
157
|
mutationFn: (itemId) => apiClient.patch(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEM_BY_ID(shipmentId, lotId, itemId), {
|
|
@@ -134,7 +173,21 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
134
173
|
return;
|
|
135
174
|
}
|
|
136
175
|
addItemMutation.mutate();
|
|
137
|
-
}, onValidationChange: () => itemForm.validate(itemDraft), schema: shipmentItemFormSchema, openIds: itemNav.openIds, onOpenChange: itemNav.setOpenIds, submitLabel: "Save Item", cancelLabel: "Cancel", onCancel: () => setShowAddItem(false), isLoading: addItemMutation.isPending })] })), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : items.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No items tracked yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Title" }), _jsx(Th, { children: "Qty" }), _jsx(Th, { children: "Price" }), _jsx(Th, { children: "Status" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: items.map((item) => (_jsxs(Tr, { children: [_jsx(Td, { children: item.title }), _jsx(Td, { children: item.quantity }), _jsx(Td, { children: item.isForSelfUse ? "—" : formatCurrency(item.price ?? 0) }), _jsx(Td, { children: item.isForSelfUse ? (_jsx(Badge, { variant: "secondary", children: "Self use" })) : item.linkedProductId ? (_jsx(Badge, { variant: "success", children: "Linked" })) : (_jsx(Badge, { variant: "default", children: "Unlinked" })) }), _jsxs(Td, { className: "flex gap-[var(--appkit-space-2)]", children: [
|
|
176
|
+
}, onValidationChange: () => itemForm.validate(itemDraft), schema: shipmentItemFormSchema, openIds: itemNav.openIds, onOpenChange: itemNav.setOpenIds, submitLabel: "Save Item", cancelLabel: "Cancel", onCancel: () => setShowAddItem(false), isLoading: addItemMutation.isPending })] })), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : items.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No items tracked yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Title" }), _jsx(Th, { children: "Qty" }), _jsx(Th, { children: "Price" }), _jsx(Th, { children: "Status" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: items.map((item) => (_jsxs(Tr, { children: [_jsx(Td, { children: item.title }), _jsx(Td, { children: item.quantity }), _jsx(Td, { children: item.isForSelfUse ? "—" : formatCurrency(item.price ?? 0) }), _jsx(Td, { children: item.isForSelfUse ? (_jsx(Badge, { variant: "secondary", children: "Self use" })) : item.linkedProductId ? (_jsx(Badge, { variant: "success", children: "Linked" })) : (_jsx(Badge, { variant: "default", children: "Unlinked" })) }), _jsxs(Td, { className: "flex gap-[var(--appkit-space-2)]", children: [_jsx(QuickEditMenu, { actions: [
|
|
177
|
+
{
|
|
178
|
+
label: "Edit item",
|
|
179
|
+
formTitle: `Edit "${item.title}"`,
|
|
180
|
+
schema: shipmentItemQuickEditSchema,
|
|
181
|
+
fields: SHIPMENT_ITEM_QUICK_FIELDS,
|
|
182
|
+
defaultValues: {
|
|
183
|
+
title: item.title,
|
|
184
|
+
quantity: item.quantity,
|
|
185
|
+
price: item.price ?? 0,
|
|
186
|
+
},
|
|
187
|
+
onSubmit: (values) => saveItemEdit(item.id, values),
|
|
188
|
+
submitLabel: "Save item",
|
|
189
|
+
},
|
|
190
|
+
] }), !item.isForSelfUse &&
|
|
138
191
|
(item.linkedProductId ? (_jsx(Button, { size: "sm", variant: "ghost", isLoading: unlinkItemMutation.isPending, onClick: () => unlinkItemMutation.mutate(item.id), children: "Unlink" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setLinkTarget({ itemId: item.id, title: item.title }), children: "Create pre-order link" }))), _jsx(Button, { size: "sm", variant: "ghost", action: ACTIONS.SHIPMENT["delete"], isLoading: deleteItemMutation.isPending, onClick: () => deleteItemMutation.mutate(item.id), children: "Delete" })] })] }, item.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
|
|
139
192
|
setPageSize(size);
|
|
140
193
|
setPage(1);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text } from "../../../ui/index.js";
|
|
4
|
+
import { Button, Div, Heading, Modal, Pagination, Select, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, TextLink } from "../../../ui/index.js";
|
|
5
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
5
6
|
import { useShipmentProjections, useShipmentItems } from "../hooks/useShipments.js";
|
|
6
7
|
import { ShipmentItemLinkModal } from "./ShipmentItemLinkModal.js";
|
|
7
8
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
@@ -25,7 +26,7 @@ export function AdminShipmentProjectionsView() {
|
|
|
25
26
|
const [sorts, setSorts] = React.useState(SORT_OPTIONS[0].value);
|
|
26
27
|
const { lots, meta, isLoading, refetch } = useShipmentProjections({ page, pageSize, sorts });
|
|
27
28
|
const [pickerLot, setPickerLot] = React.useState(null);
|
|
28
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: lot.lotName }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
|
|
29
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { layout: "flex", align: "center", justify: "between", children: [_jsx(Heading, { level: 2, children: "Projections" }), _jsx(Select, { "aria-label": "Sort by", value: sorts, onValueChange: setSorts, options: SORT_OPTIONS })] }), isLoading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" }), _jsx(Skeleton, { variant: "rectangular", height: "32px" })] })) : lots.length === 0 ? (_jsx(Text, { variant: "secondary", children: "No lots to project yet." })) : (_jsxs(Table, { children: [_jsx(Thead, { children: _jsxs(Tr, { children: [_jsx(Th, { children: "Lot" }), _jsx(Th, { children: "Items" }), _jsx(Th, { children: "Landed Cost" }), _jsx(Th, { children: "Projected Revenue" }), _jsx(Th, { children: "Projected Profit" }), _jsx(Th, {})] }) }), _jsx(Tbody, { children: lots.map((lot) => (_jsxs(Tr, { children: [_jsx(Td, { children: _jsx(TextLink, { href: String(ROUTES.ADMIN.SHIPMENT_LOT_ITEMS(lot.shipmentId, lot.id)), children: lot.lotName }) }), _jsx(Td, { children: lot.itemCount }), _jsx(Td, { children: formatCurrency(lot.totalLandedCost) }), _jsx(Td, { children: formatCurrency(lot.projectedRevenue) }), _jsx(Td, { className: lot.projectedProfit < 0 ? "text-error" : "text-success", children: formatCurrency(lot.projectedProfit) }), _jsx(Td, { children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPickerLot(lot), children: "Create pre-order link" }) })] }, lot.id))) })] })), meta && meta.totalPages > 1 && (_jsx(Pagination, { currentPage: page, totalPages: meta.totalPages, onPageChange: setPage, pageSize: pageSize, onPageSizeChange: (size) => {
|
|
29
30
|
setPageSize(size);
|
|
30
31
|
setPage(1);
|
|
31
32
|
} })), pickerLot && (_jsx(LotItemPickerModal, { lot: pickerLot, onClose: () => setPickerLot(null), onLinked: refetch }))] }));
|
|
@@ -15,7 +15,7 @@ export declare const createShipmentSchema: z.ZodObject<{
|
|
|
15
15
|
laborRatePerHour: z.ZodOptional<z.ZodNumber>;
|
|
16
16
|
notes: z.ZodOptional<z.ZodString>;
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
|
-
status: "received" | "completed" | "processing" | "cancelled" | "
|
|
18
|
+
status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
|
|
19
19
|
shipmentNumber: string;
|
|
20
20
|
supplierName: string;
|
|
21
21
|
customsTotal: number;
|
|
@@ -29,7 +29,7 @@ export declare const createShipmentSchema: z.ZodObject<{
|
|
|
29
29
|
laborHoursSpent?: number | undefined;
|
|
30
30
|
laborRatePerHour?: number | undefined;
|
|
31
31
|
}, {
|
|
32
|
-
status: "received" | "completed" | "processing" | "cancelled" | "
|
|
32
|
+
status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
|
|
33
33
|
shipmentNumber: string;
|
|
34
34
|
supplierName: string;
|
|
35
35
|
customsTotal: number;
|
|
@@ -58,7 +58,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
|
|
|
58
58
|
laborRatePerHour: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
59
59
|
notes: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
60
60
|
}, "strip", z.ZodTypeAny, {
|
|
61
|
-
status?: "received" | "completed" | "processing" | "cancelled" | "
|
|
61
|
+
status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
|
|
62
62
|
trackingNumber?: string | undefined;
|
|
63
63
|
notes?: string | undefined;
|
|
64
64
|
carrier?: string | undefined;
|
|
@@ -72,7 +72,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
|
|
|
72
72
|
laborHoursSpent?: number | undefined;
|
|
73
73
|
laborRatePerHour?: number | undefined;
|
|
74
74
|
}, {
|
|
75
|
-
status?: "received" | "completed" | "processing" | "cancelled" | "
|
|
75
|
+
status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
|
|
76
76
|
trackingNumber?: string | undefined;
|
|
77
77
|
notes?: string | undefined;
|
|
78
78
|
carrier?: string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
2
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
3
|
+
import { type ShippingBoxDocument } from "../schemas/firestore";
|
|
4
|
+
/**
|
|
5
|
+
* The shared carton library. See the schema header for why it is global.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ShippingBoxesRepository extends BaseRepository<ShippingBoxDocument> {
|
|
8
|
+
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
9
|
+
constructor();
|
|
10
|
+
/**
|
|
11
|
+
* The picker's list. Active only, by name.
|
|
12
|
+
*
|
|
13
|
+
* 🛑 Retired presets are excluded here and NOT deleted, so a draft shipment
|
|
14
|
+
* holding one does not lose it mid-dispatch.
|
|
15
|
+
*/
|
|
16
|
+
listActive(model: SieveModel): Promise<FirebaseSieveResult<ShippingBoxDocument>>;
|
|
17
|
+
/**
|
|
18
|
+
* Create a preset, keyed on a slug of its name.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 `box-<slug>` as the document id, so the SAME carton name cannot be added
|
|
21
|
+
* twice by two sellers. That is the whole value of a shared library — two
|
|
22
|
+
* rows called "Standard carton" with different numbers is worse than one
|
|
23
|
+
* row, because the picker gives no way to tell them apart.
|
|
24
|
+
*
|
|
25
|
+
* A colliding create is reported to the caller rather than silently
|
|
26
|
+
* overwriting: the existing row has an author and may be in use, and the
|
|
27
|
+
* second seller's intent ("I want a preset for this") is already satisfied
|
|
28
|
+
* by the row that exists.
|
|
29
|
+
*/
|
|
30
|
+
createPreset(input: {
|
|
31
|
+
name: string;
|
|
32
|
+
length: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
tareWeight?: number;
|
|
36
|
+
createdByUid: string;
|
|
37
|
+
createdByName: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
box: ShippingBoxDocument;
|
|
40
|
+
created: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export declare const shippingBoxesRepository: ShippingBoxesRepository;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase/index.js";
|
|
2
|
+
import { slugify } from "../../../utils/string.formatter.js";
|
|
3
|
+
import { SHIPPING_BOX_COLLECTION, SHIPPING_BOX_FIELDS, SHIPPING_BOX_LIMITS, } from "../schemas/firestore.js";
|
|
4
|
+
/**
|
|
5
|
+
* The shared carton library. See the schema header for why it is global.
|
|
6
|
+
*/
|
|
7
|
+
export class ShippingBoxesRepository extends BaseRepository {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(SHIPPING_BOX_COLLECTION);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The picker's list. Active only, by name.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 Retired presets are excluded here and NOT deleted, so a draft shipment
|
|
15
|
+
* holding one does not lose it mid-dispatch.
|
|
16
|
+
*/
|
|
17
|
+
async listActive(model) {
|
|
18
|
+
const baseQuery = this.getCollection().where(SHIPPING_BOX_FIELDS.IS_ACTIVE, "==", true);
|
|
19
|
+
return this.sieveQuery(model, ShippingBoxesRepository.SIEVE_FIELDS, {
|
|
20
|
+
baseQuery,
|
|
21
|
+
defaultPageSize: 20,
|
|
22
|
+
maxPageSize: 50,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create a preset, keyed on a slug of its name.
|
|
27
|
+
*
|
|
28
|
+
* 🛑 `box-<slug>` as the document id, so the SAME carton name cannot be added
|
|
29
|
+
* twice by two sellers. That is the whole value of a shared library — two
|
|
30
|
+
* rows called "Standard carton" with different numbers is worse than one
|
|
31
|
+
* row, because the picker gives no way to tell them apart.
|
|
32
|
+
*
|
|
33
|
+
* A colliding create is reported to the caller rather than silently
|
|
34
|
+
* overwriting: the existing row has an author and may be in use, and the
|
|
35
|
+
* second seller's intent ("I want a preset for this") is already satisfied
|
|
36
|
+
* by the row that exists.
|
|
37
|
+
*/
|
|
38
|
+
async createPreset(input) {
|
|
39
|
+
const name = input.name.trim();
|
|
40
|
+
const id = `box-${slugify(name)}`;
|
|
41
|
+
const existing = await this.findById(id);
|
|
42
|
+
if (existing)
|
|
43
|
+
return { box: existing, created: false };
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const box = {
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
length: clampCm(input.length),
|
|
49
|
+
width: clampCm(input.width),
|
|
50
|
+
height: clampCm(input.height),
|
|
51
|
+
...(input.tareWeight === undefined ? {} : { tareWeight: clampKg(input.tareWeight) }),
|
|
52
|
+
createdByUid: input.createdByUid,
|
|
53
|
+
createdByName: input.createdByName,
|
|
54
|
+
isActive: true,
|
|
55
|
+
createdAt: now,
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
};
|
|
58
|
+
await this.createWithId(id, box);
|
|
59
|
+
return { box, created: true };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
ShippingBoxesRepository.SIEVE_FIELDS = {
|
|
63
|
+
/*
|
|
64
|
+
* `name` is sortable AND filterable because this list is picked from by
|
|
65
|
+
* typing — `PaginatedSelect` searches it. A field the picker searches that
|
|
66
|
+
* is not allowlisted here would have its clause silently DROPPED and the
|
|
67
|
+
* search would return the whole library while looking like it filtered
|
|
68
|
+
* (Root Cause #62).
|
|
69
|
+
*/
|
|
70
|
+
name: { canFilter: true, canSort: true },
|
|
71
|
+
isActive: { canFilter: true, canSort: false },
|
|
72
|
+
createdByUid: { canFilter: true, canSort: false },
|
|
73
|
+
};
|
|
74
|
+
/*
|
|
75
|
+
* Clamped rather than rejected. These bounds exist so a typo cannot reach a
|
|
76
|
+
* courier API as a 4-metre parcel; they are not a tariff, and refusing a
|
|
77
|
+
* legitimate large parcel client-side is worse than letting it through to be
|
|
78
|
+
* priced by `checkServiceability` against the real figures.
|
|
79
|
+
*/
|
|
80
|
+
function clampCm(v) {
|
|
81
|
+
if (!Number.isFinite(v))
|
|
82
|
+
return SHIPPING_BOX_LIMITS.minCm;
|
|
83
|
+
return Math.min(SHIPPING_BOX_LIMITS.maxCm, Math.max(SHIPPING_BOX_LIMITS.minCm, v));
|
|
84
|
+
}
|
|
85
|
+
function clampKg(v) {
|
|
86
|
+
if (!Number.isFinite(v))
|
|
87
|
+
return SHIPPING_BOX_LIMITS.minKg;
|
|
88
|
+
return Math.min(SHIPPING_BOX_LIMITS.maxKg, Math.max(SHIPPING_BOX_LIMITS.minKg, v));
|
|
89
|
+
}
|
|
90
|
+
export const shippingBoxesRepository = new ShippingBoxesRepository();
|