@mohasinac/appkit 4.60.1 → 4.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.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/auctions/actions.d.ts +1 -1
- package/dist/_internal/server/features/auctions/actions.js +12 -1
- package/dist/_internal/server/features/blog/actions.d.ts +1 -1
- package/dist/_internal/server/features/blog/actions.js +12 -1
- package/dist/_internal/server/features/brands/actions.d.ts +1 -1
- package/dist/_internal/server/features/brands/actions.js +12 -1
- package/dist/_internal/server/features/cart/actions.d.ts +1 -1
- package/dist/_internal/server/features/cart/actions.js +12 -1
- package/dist/_internal/server/features/checkout/actions.js +0 -1
- package/dist/_internal/server/features/events/actions.d.ts +1 -1
- package/dist/_internal/server/features/events/actions.js +12 -1
- package/dist/_internal/server/features/gateway-events/redact.js +14 -2
- package/dist/_internal/server/features/history/actions.d.ts +1 -1
- package/dist/_internal/server/features/history/actions.js +12 -1
- package/dist/_internal/server/features/lottery/actions.d.ts +1 -1
- package/dist/_internal/server/features/lottery/actions.js +12 -1
- package/dist/_internal/server/features/orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/orders/actions.js +12 -1
- package/dist/_internal/server/features/orders/cancellation-refund.js +0 -1
- package/dist/_internal/server/features/orders/service.js +31 -1
- package/dist/_internal/server/features/payouts/actions.d.ts +1 -1
- package/dist/_internal/server/features/payouts/actions.js +12 -1
- package/dist/_internal/server/features/points/award.js +9 -1
- package/dist/_internal/server/features/pre-orders/actions.d.ts +1 -1
- package/dist/_internal/server/features/pre-orders/actions.js +12 -1
- package/dist/_internal/server/features/prize-draws/actions.d.ts +1 -1
- package/dist/_internal/server/features/prize-draws/actions.js +12 -1
- package/dist/_internal/server/features/products/actions.d.ts +1 -1
- package/dist/_internal/server/features/products/actions.js +12 -1
- package/dist/_internal/server/features/promotions/actions.d.ts +1 -1
- package/dist/_internal/server/features/promotions/actions.js +12 -1
- package/dist/_internal/server/features/protection-claims/actions.d.ts +2 -2
- package/dist/_internal/server/features/protection-claims/actions.js +21 -12
- package/dist/_internal/server/features/raffle/actions.d.ts +1 -1
- package/dist/_internal/server/features/raffle/actions.js +12 -1
- package/dist/_internal/server/features/refunds/actions.d.ts +1 -3
- package/dist/_internal/server/features/refunds/actions.js +14 -2
- package/dist/_internal/server/features/reviews/actions.d.ts +1 -1
- package/dist/_internal/server/features/reviews/actions.js +12 -1
- package/dist/_internal/server/features/wishlist/actions.d.ts +1 -1
- package/dist/_internal/server/features/wishlist/actions.js +12 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +0 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +93 -76
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +22 -1
- package/dist/_internal/server/jobs/handlers/onShipmentAllocationSync.js +16 -4
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/features/promotions/schema.d.ts +66 -0
- package/dist/_internal/shared/features/promotions/schema.js +7 -0
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/obligations/deadlines.js +11 -1
- package/dist/_internal/shared/tester/window.d.ts +25 -0
- package/dist/_internal/shared/tester/window.js +66 -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 +1 -1
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +3 -0
- package/dist/constants/api-endpoints.js +1 -0
- package/dist/constants/field-names.d.ts +17 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/core/server-action.js +19 -3
- package/dist/core/unit-of-work.js +15 -1
- package/dist/errors/error-mapping.js +26 -1
- package/dist/errors/firestore-codes.d.ts +37 -0
- package/dist/errors/firestore-codes.js +52 -0
- package/dist/errors/index.d.ts +0 -1
- package/dist/errors/index.js +12 -2
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- 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 +8 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -2
- 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/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/DataListingView.js +2 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +4 -1
- 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/repository/site-settings.repository.d.ts +2 -1
- package/dist/features/admin/repository/site-settings.repository.js +7 -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/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.d.ts +1 -1
- package/dist/features/catalogue/actions/approve-catalogue-listing.js +12 -1
- package/dist/features/catalogue/actions/list-from-catalogue.d.ts +1 -1
- package/dist/features/catalogue/actions/list-from-catalogue.js +12 -1
- package/dist/features/catalogue/actions/submit-for-approval.d.ts +1 -1
- package/dist/features/catalogue/actions/submit-for-approval.js +12 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/categories/repository/price-observation.repository.d.ts +19 -0
- package/dist/features/categories/repository/price-observation.repository.js +21 -2
- package/dist/features/events/actions/event-actions.js +4 -1
- package/dist/features/events/components/AdminEventEditorView.js +79 -50
- package/dist/features/events/components/EventsListPageView.js +2 -1
- package/dist/features/events/schemas/firestore.d.ts +17 -0
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/events/utils/sort-pinned.d.ts +33 -0
- package/dist/features/events/utils/sort-pinned.js +46 -0
- package/dist/features/gateway-events/repository/gateway-events.repository.js +3 -40
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/inventory/components/AdminStorageBoxesView.js +55 -39
- package/dist/features/inventory/components/ReceiveStationView.js +61 -17
- package/dist/features/inventory/repository/storage-boxes.repository.d.ts +3 -1
- package/dist/features/inventory/repository/storage-boxes.repository.js +36 -3
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/obligations/repository/obligations.repository.js +10 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +19 -0
- package/dist/features/orders/schemas/firestore.js +19 -0
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/payment-cases/repository/payment-cases.repository.d.ts +1 -0
- package/dist/features/payment-cases/repository/payment-cases.repository.js +3 -14
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- package/dist/features/products/components/ProductForm.js +2 -2
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/promotions/repository/claimed-coupons.repository.js +31 -1
- package/dist/features/promotions/schemas/firestore.d.ts +22 -0
- package/dist/features/promotions/schemas/firestore.js +22 -0
- package/dist/features/promotions/schemas/index.d.ts +479 -6
- package/dist/features/promotions/schemas/index.js +153 -0
- package/dist/features/protection-claims/repository/protection-claims.repository.js +22 -15
- 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/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/schemas/coupon-form.d.ts +5 -0
- package/dist/features/seller/schemas/coupon-form.js +18 -0
- package/dist/features/shell/SectionForm.js +35 -9
- 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/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/sticker-trace/server.d.ts +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/http/api-handler.js +4 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +6 -4
- package/dist/next/routing/route-map.d.ts +4 -2
- package/dist/next/routing/route-map.js +19 -1
- package/dist/schemas/registry.d.ts +15 -10
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -2
- package/dist/server.js +6 -4
- package/dist/styles.css +51 -0
- package/dist/ui/components/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +0 -0
- 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/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/package.json +1 -1
- package/scripts/seed-cli.mjs +1822 -1812
- package/dist/features/tester/utils/phases.d.ts +0 -48
- package/dist/features/tester/utils/phases.js +0 -139
|
@@ -18,9 +18,32 @@ export const discountConfigSchema = z.object({
|
|
|
18
18
|
maxDiscount: rupeesSchema.optional(),
|
|
19
19
|
minPurchase: rupeesSchema.optional(),
|
|
20
20
|
});
|
|
21
|
+
/*
|
|
22
|
+
* 🛑 THIS IS THE ONLY bxgy VALIDATOR. The four admin write paths — the POST
|
|
23
|
+
* and PATCH routes, both server actions, and `createCouponSchema` below — all
|
|
24
|
+
* import it rather than restating the shape.
|
|
25
|
+
*
|
|
26
|
+
* That is not tidiness. Until 2026-10-22 none of those four declared `bxgy` at
|
|
27
|
+
* all, and every one of them is a `z.object()`, which STRIPS unknown keys. So
|
|
28
|
+
* `AdminCouponEditorView` built a correct `bxgy` payload, the route answered
|
|
29
|
+
* 200, and the field was never written — an admin could create a BOGO coupon
|
|
30
|
+
* that discounted nothing, which is the exact silent-strip trap the `tiers`
|
|
31
|
+
* comment in the POST route already describes, recurring one field over.
|
|
32
|
+
*
|
|
33
|
+
* `maxItemPrice` is deliberately `.optional()` with no default: absent means
|
|
34
|
+
* "no ceiling", which is what every BOGO coupon written before it existed
|
|
35
|
+
* means. A default would retroactively shrink them.
|
|
36
|
+
*/
|
|
21
37
|
export const bxgyConfigSchema = z.object({
|
|
22
38
|
buyQuantity: z.number().int().positive(),
|
|
23
39
|
getQuantity: z.number().int().positive(),
|
|
40
|
+
// `.refine` rather than `.positive()` — `rupeesSchema` is a ZodEffects (it
|
|
41
|
+
// carries the 2-decimal check), and ZodNumber's numeric helpers are not on
|
|
42
|
+
// it. A zero ceiling would disqualify every unit, so it is rejected here as
|
|
43
|
+
// well as being treated as absent by `calculateBxgyDiscount`.
|
|
44
|
+
maxItemPrice: rupeesSchema
|
|
45
|
+
.refine((n) => n > 0, { message: "Price ceiling must be greater than zero" })
|
|
46
|
+
.optional(),
|
|
24
47
|
applicableProducts: z.array(z.string()).optional(),
|
|
25
48
|
applicableCategories: z.array(z.string()).optional(),
|
|
26
49
|
});
|
|
@@ -70,6 +93,136 @@ export const couponFirestoreSchema = z.object({
|
|
|
70
93
|
stats: couponStatsSchema,
|
|
71
94
|
...auditTimestampsShape,
|
|
72
95
|
});
|
|
96
|
+
/**
|
|
97
|
+
* The admin create-coupon REQUEST BODY.
|
|
98
|
+
*
|
|
99
|
+
* 🛑 It lives here, in appkit, because `bxgyConfigSchema` does — and a zod
|
|
100
|
+
* schema CANNOT cross the version boundary. The consumer root is on zod 4 and
|
|
101
|
+
* appkit depends on zod 3, so `/api/admin/coupons` composing
|
|
102
|
+
* `bxgy: bxgyConfigSchema.optional()` into a consumer `z.object({...})` did
|
|
103
|
+
* not merely infer `unknown`: zod 4 **throws** on it —
|
|
104
|
+
*
|
|
105
|
+
* Invalid element at key "bx": expected a Zod schema
|
|
106
|
+
*
|
|
107
|
+
* — so every POST to that route failed. The type error was the only signal,
|
|
108
|
+
* and it reads as a cosmetic inference problem.
|
|
109
|
+
*
|
|
110
|
+
* Composed from the pieces above rather than restated, which is the concern
|
|
111
|
+
* the route's own comment raised about `bxgy`: "four write paths validate this
|
|
112
|
+
* shape and a fifth copy is how they drift". Re-declaring the shape in
|
|
113
|
+
* consumer zod would have been that fifth copy; moving the whole body schema
|
|
114
|
+
* to where the pieces live is the version of that fix which also works.
|
|
115
|
+
*/
|
|
116
|
+
export const couponCreateBodySchema = z
|
|
117
|
+
.object({
|
|
118
|
+
code: z.string().min(2).max(50),
|
|
119
|
+
name: z.string().min(1),
|
|
120
|
+
description: z.string().default(""),
|
|
121
|
+
type: couponTypeEnumSchema,
|
|
122
|
+
tiers: z.array(tieredDiscountSchema).max(6).optional(),
|
|
123
|
+
bxgy: bxgyConfigSchema.optional(),
|
|
124
|
+
discount: z.object({
|
|
125
|
+
value: z.number().min(0),
|
|
126
|
+
maxDiscount: z.number().optional(),
|
|
127
|
+
minPurchase: z.number().optional(),
|
|
128
|
+
}),
|
|
129
|
+
usage: z.object({
|
|
130
|
+
totalLimit: z.number().optional(),
|
|
131
|
+
perUserLimit: z.number().optional(),
|
|
132
|
+
currentUsage: z.number().default(0),
|
|
133
|
+
}),
|
|
134
|
+
validity: z.object({
|
|
135
|
+
startDate: z.string().transform((v) => new Date(v)),
|
|
136
|
+
endDate: z
|
|
137
|
+
.string()
|
|
138
|
+
.optional()
|
|
139
|
+
.transform((v) => (v ? new Date(v) : undefined)),
|
|
140
|
+
isActive: z.boolean().default(false),
|
|
141
|
+
}),
|
|
142
|
+
restrictions: z
|
|
143
|
+
.object({
|
|
144
|
+
applicableProducts: z.array(z.string()).optional(),
|
|
145
|
+
applicableCategories: z.array(z.string()).optional(),
|
|
146
|
+
applicableSellers: z.array(z.string()).optional(),
|
|
147
|
+
excludeProducts: z.array(z.string()).optional(),
|
|
148
|
+
excludeCategories: z.array(z.string()).optional(),
|
|
149
|
+
firstTimeUserOnly: z.boolean().default(false),
|
|
150
|
+
})
|
|
151
|
+
.default({ firstTimeUserOnly: false }),
|
|
152
|
+
createdBy: z.string().optional().default("admin"),
|
|
153
|
+
})
|
|
154
|
+
.superRefine((data, ctx) => {
|
|
155
|
+
if (data.type === "percentage" && data.discount.value > 100) {
|
|
156
|
+
ctx.addIssue({
|
|
157
|
+
code: z.ZodIssueCode.custom,
|
|
158
|
+
message: "Percentage discount cannot exceed 100%",
|
|
159
|
+
path: ["discount", "value"],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
/**
|
|
164
|
+
* The admin update-coupon REQUEST BODY (`PATCH /api/admin/coupons/[id]`).
|
|
165
|
+
*
|
|
166
|
+
* 🛑 HERE FOR THE SAME REASON AS `couponCreateBodySchema` — read its note
|
|
167
|
+
* first. It embeds `bxgyConfigSchema`, and a zod-3 schema composed into a
|
|
168
|
+
* consumer zod-4 `z.object({...})` THROWS at parse time rather than merely
|
|
169
|
+
* mistyping. The POST route was migrated when that was discovered; this is
|
|
170
|
+
* the sibling PATCH, which has the same embed and would fail the same way.
|
|
171
|
+
*
|
|
172
|
+
* 🛑 NOT `couponCreateBodySchema.partial()`. That schema carries a
|
|
173
|
+
* `.superRefine`, so it is a ZodEffects and has no `.partial()` — and the
|
|
174
|
+
* update body is genuinely a different shape anyway: no `code` (a coupon's
|
|
175
|
+
* code is immutable after creation), plus the `action` discriminator the
|
|
176
|
+
* route uses for activate/deactivate.
|
|
177
|
+
*
|
|
178
|
+
* Every nested object stays OPTIONAL as a whole, which is what lets the route
|
|
179
|
+
* merge `validity` and `restrictions` against the stored document instead of
|
|
180
|
+
* replacing them — a caller sending only `{isActive: false}` must not wipe
|
|
181
|
+
* the coupon's real dates.
|
|
182
|
+
*/
|
|
183
|
+
export const couponUpdateBodySchema = z.object({
|
|
184
|
+
name: z.string().min(1).optional(),
|
|
185
|
+
description: z.string().optional(),
|
|
186
|
+
tiers: z.array(tieredDiscountSchema).max(6).optional(),
|
|
187
|
+
bxgy: bxgyConfigSchema.optional(),
|
|
188
|
+
discount: z
|
|
189
|
+
.object({
|
|
190
|
+
value: z.number().min(0),
|
|
191
|
+
maxDiscount: z.number().optional(),
|
|
192
|
+
minPurchase: z.number().optional(),
|
|
193
|
+
})
|
|
194
|
+
.optional(),
|
|
195
|
+
usage: z
|
|
196
|
+
.object({
|
|
197
|
+
totalLimit: z.number().optional(),
|
|
198
|
+
perUserLimit: z.number().optional(),
|
|
199
|
+
})
|
|
200
|
+
.optional(),
|
|
201
|
+
validity: z
|
|
202
|
+
.object({
|
|
203
|
+
startDate: z
|
|
204
|
+
.string()
|
|
205
|
+
.transform((v) => new Date(v))
|
|
206
|
+
.optional(),
|
|
207
|
+
endDate: z
|
|
208
|
+
.string()
|
|
209
|
+
.optional()
|
|
210
|
+
.transform((v) => (v ? new Date(v) : undefined)),
|
|
211
|
+
isActive: z.boolean().optional(),
|
|
212
|
+
})
|
|
213
|
+
.optional(),
|
|
214
|
+
restrictions: z
|
|
215
|
+
.object({
|
|
216
|
+
applicableProducts: z.array(z.string()).optional(),
|
|
217
|
+
applicableCategories: z.array(z.string()).optional(),
|
|
218
|
+
applicableSellers: z.array(z.string()).optional(),
|
|
219
|
+
excludeProducts: z.array(z.string()).optional(),
|
|
220
|
+
excludeCategories: z.array(z.string()).optional(),
|
|
221
|
+
firstTimeUserOnly: z.boolean().optional(),
|
|
222
|
+
})
|
|
223
|
+
.optional(),
|
|
224
|
+
action: z.enum(["activate", "deactivate"]).optional(),
|
|
225
|
+
});
|
|
73
226
|
export const couponUsageFirestoreSchema = z.object({
|
|
74
227
|
id: z.string(),
|
|
75
228
|
userId: z.string(),
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
+
// One shared predicate — this file had its own copy; see firestore-codes.ts.
|
|
2
|
+
import { isAlreadyExistsError } from "../../../errors/firestore-codes.js";
|
|
1
3
|
import { BaseRepository, prepareForFirestore } from "../../../providers/db-firebase/index.js";
|
|
2
4
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
3
5
|
import { buildHistoryEntry } from "../../../_internal/shared/history/history.js";
|
|
4
6
|
import { ConflictError } from "../../../errors/index.js";
|
|
5
7
|
import { PROTECTION_CLAIMS_COLLECTION, PROTECTION_CLAIM_ACTIVE_STATUSES, PROTECTION_CLAIM_FIELDS, createProtectionClaimId, } from "../schemas/firestore.js";
|
|
6
|
-
|
|
7
|
-
function isAlreadyExists(err) {
|
|
8
|
-
if (typeof err !== "object" || err === null)
|
|
9
|
-
return false;
|
|
10
|
-
const code = err.code;
|
|
11
|
-
if (code === GRPC_ALREADY_EXISTS)
|
|
12
|
-
return true;
|
|
13
|
-
if (code === "already-exists" || code === "ALREADY_EXISTS")
|
|
14
|
-
return true;
|
|
15
|
-
const message = err.message;
|
|
16
|
-
return typeof message === "string" && message.includes("ALREADY_EXISTS");
|
|
17
|
-
}
|
|
8
|
+
import { serverLogger } from "../../../monitoring/index.js";
|
|
18
9
|
export class ProtectionClaimsRepository extends BaseRepository {
|
|
19
10
|
constructor() {
|
|
20
11
|
super(PROTECTION_CLAIMS_COLLECTION);
|
|
@@ -64,7 +55,7 @@ export class ProtectionClaimsRepository extends BaseRepository {
|
|
|
64
55
|
return doc;
|
|
65
56
|
}
|
|
66
57
|
catch (err) {
|
|
67
|
-
if (
|
|
58
|
+
if (isAlreadyExistsError(err)) {
|
|
68
59
|
void normalizeError(err);
|
|
69
60
|
throw new ConflictError(`A ${input.kind} protection claim is already open on this order. Add to that claim rather than filing a second one.`);
|
|
70
61
|
}
|
|
@@ -118,7 +109,15 @@ export class ProtectionClaimsRepository extends BaseRepository {
|
|
|
118
109
|
await this.getCollection().doc(id).update({ refundId, updatedAt: now });
|
|
119
110
|
}
|
|
120
111
|
catch (err) {
|
|
121
|
-
|
|
112
|
+
/*
|
|
113
|
+
* Best effort, but RECORDED. `void normalizeError(err)` alone types the
|
|
114
|
+
* error and reports it nowhere — as silent as `catch {}` while looking
|
|
115
|
+
* handled, which is what audit-silent-degrade calls normalize theatre.
|
|
116
|
+
* Swallowing is still correct here (the claim row is already durable; this is a best-effort annotation); losing the reason is not.
|
|
117
|
+
*/
|
|
118
|
+
serverLogger.warn("protectionClaims.write failed", {
|
|
119
|
+
error: normalizeError(err).message,
|
|
120
|
+
});
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
async setStatus(id, status, reason, now = new Date()) {
|
|
@@ -142,7 +141,15 @@ export class ProtectionClaimsRepository extends BaseRepository {
|
|
|
142
141
|
});
|
|
143
142
|
}
|
|
144
143
|
catch (err) {
|
|
145
|
-
|
|
144
|
+
/*
|
|
145
|
+
* Best effort, but RECORDED. `void normalizeError(err)` alone types the
|
|
146
|
+
* error and reports it nowhere — as silent as `catch {}` while looking
|
|
147
|
+
* handled, which is what audit-silent-degrade calls normalize theatre.
|
|
148
|
+
* Swallowing is still correct here (the claim row is already durable; this is a best-effort annotation); losing the reason is not.
|
|
149
|
+
*/
|
|
150
|
+
serverLogger.warn("protectionClaims.write failed", {
|
|
151
|
+
error: normalizeError(err).message,
|
|
152
|
+
});
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
async findByOrderAndKind(orderId, kind) {
|
|
@@ -9,7 +9,8 @@ import { apiClient } from "../../../http/index.js";
|
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
10
10
|
import { BarcodeField } from "./BarcodeField.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
|
function statusBadgeVariant(status) {
|
|
14
15
|
if (status === "confirmed")
|
|
15
16
|
return "warning";
|
|
@@ -118,5 +119,5 @@ export function FulfillmentView({ currentUserId, }) {
|
|
|
118
119
|
setScanError(null);
|
|
119
120
|
}, children: "Cancel" })] })] })), !scanResult && !scanError && (_jsx(Text, { color: "muted", size: "sm", children: orders.length === 0 && !isLoadingOrders
|
|
120
121
|
? "No orders waiting — great work!"
|
|
121
|
-
: `${orders.length} order${orders.length === 1 ? "" : "s"} in queue.` }))] })), mode === "list" && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { variant: filterMine ? "primary" : "secondary", size: "sm", onClick: () => setFilterMine((v) => !v), children: filterMine ? "Showing My Orders" : "My Orders" }), _jsx(Text, { size: "xs", color: "muted", children: isLoadingOrders ? "Loading…" : `${visibleOrders.length} order${visibleOrders.length === 1 ? "" : "s"}` })] }), visibleOrders.length === 0 && !isLoadingOrders && (_jsx(Text, { color: "muted", children: "No orders waiting \u2014 great work!" })), visibleOrders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(
|
|
122
|
+
: `${orders.length} order${orders.length === 1 ? "" : "s"} in queue.` }))] })), mode === "list" && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Button, { variant: filterMine ? "primary" : "secondary", size: "sm", onClick: () => setFilterMine((v) => !v), children: filterMine ? "Showing My Orders" : "My Orders" }), _jsx(Text, { size: "xs", color: "muted", children: isLoadingOrders ? "Loading…" : `${visibleOrders.length} order${visibleOrders.length === 1 ? "" : "s"}` })] }), visibleOrders.length === 0 && !isLoadingOrders && (_jsx(Text, { color: "muted", children: "No orders waiting \u2014 great work!" })), visibleOrders.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.STORE.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: ", order.assignedWorkerId === currentUserId ? "Me" : 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.id)))] }))] }));
|
|
122
123
|
}
|
|
@@ -11,6 +11,7 @@ import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
|
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
12
|
import { TextLink } from "../../../ui/index.js";
|
|
13
13
|
import { ROUTES } from "../../../constants/index.js";
|
|
14
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
14
15
|
const __O = {
|
|
15
16
|
xAuto: "overflow-x-auto",
|
|
16
17
|
};
|
|
@@ -47,9 +48,9 @@ function fromDoc(doc) {
|
|
|
47
48
|
isDefault: doc.isDefault,
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
function AddressCard({ address, onEdit, onDelete, }) {
|
|
51
|
+
function AddressCard({ address, onEdit, onDelete, onSetDefault, }) {
|
|
51
52
|
const isBanned = address.banStatus === "banned";
|
|
52
|
-
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "sm", children: [_jsxs(Row, { align: "start", justify: "between", gap: "xs", children: [_jsxs(Row, { gap: "xs", wrap: true, className: "min-w-0", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: address.label }), address.isDefault && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_DEFAULT_PILL, children: [_jsx(Star, { className: "h-3 w-3" }), "Default"] })), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }), _jsx(Row, { gap: "px", className: "shrink-0", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(_Fragment, { children: [_jsx(IconButton, { type: "button", onClick: onEdit, title: "Edit address", "aria-label": "Edit address", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(IconButton, { type: "button", onClick: onDelete, title: "Delete address", "aria-label": "Delete address", variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) })] })) })] }), _jsxs(Text, { size: "sm", color: "muted", children: [address.fullName, " \u00B7 ", address.phone] }), _jsxs(Text, { className: "leading-relaxed", color: "muted", size: "xs", children: [address.addressLine1, address.addressLine2 ? `, ${address.addressLine2}` : "", address.landmark ? ` (near ${address.landmark})` : "", _jsx("br", {}), address.city, ", ", address.state, " ", address.postalCode, ", ", address.country] })] }));
|
|
53
|
+
return (_jsxs(Stack, { surface: "card", padding: "sm", gap: "sm", children: [_jsxs(Row, { align: "start", justify: "between", gap: "xs", children: [_jsxs(Row, { gap: "xs", wrap: true, className: "min-w-0", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-[var(--appkit-color-primary)]" }), _jsx(Span, { size: "sm", weight: "semibold", className: "truncate", color: "primary", children: address.label }), address.isDefault && (_jsxs(Span, { size: "xs", weight: "medium", className: CLS_DEFAULT_PILL, children: [_jsx(Star, { className: "h-3 w-3" }), "Default"] })), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }), _jsx(Row, { gap: "px", className: "shrink-0", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(_Fragment, { children: [!address.isDefault && (_jsx(IconButton, { type: "button", onClick: onSetDefault, title: "Set as default pickup address", "aria-label": `Set ${address.label} as the default pickup address`, variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Star, { className: "h-4 w-4" }) })), _jsx(IconButton, { type: "button", onClick: onEdit, title: "Edit address", "aria-label": "Edit address", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] hover:text-[var(--appkit-color-text-muted)]", children: _jsx(Pencil, { className: "h-4 w-4" }) }), _jsx(IconButton, { type: "button", onClick: onDelete, title: "Delete address", "aria-label": "Delete address", variant: "ghost", rounded: "lg", className: CLS_DELETE_BTN, children: _jsx(Trash2, { className: "h-4 w-4" }) })] })) })] }), _jsxs(Text, { size: "sm", color: "muted", children: [address.fullName, " \u00B7 ", address.phone] }), _jsxs(Text, { className: "leading-relaxed", color: "muted", size: "xs", children: [address.addressLine1, address.addressLine2 ? `, ${address.addressLine2}` : "", address.landmark ? ` (near ${address.landmark})` : "", _jsx("br", {}), address.city, ", ", address.state, " ", address.postalCode, ", ", address.country] })] }));
|
|
53
54
|
}
|
|
54
55
|
// ---------------------------------------------------------------------------
|
|
55
56
|
// Main Component
|
|
@@ -129,7 +130,7 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
129
130
|
}
|
|
130
131
|
catch (err) {
|
|
131
132
|
void normalizeError(err);
|
|
132
|
-
setSaveError(err
|
|
133
|
+
setSaveError(toUserMessage(err?.code, undefined, { fallback: "Failed to save address" }));
|
|
133
134
|
}
|
|
134
135
|
finally {
|
|
135
136
|
setSaving(false);
|
|
@@ -141,9 +142,36 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
141
142
|
onSuccess: () => { load(); },
|
|
142
143
|
});
|
|
143
144
|
const handleDelete = (addr) => setDeleteTargetAddr(addr);
|
|
145
|
+
/**
|
|
146
|
+
* Promote one pickup address to the default.
|
|
147
|
+
*
|
|
148
|
+
* A partial PUT rather than a dedicated endpoint: unlike buyer addresses,
|
|
149
|
+
* which have `/api/user/addresses/[id]/set-default`, the store side has no
|
|
150
|
+
* such route — but `addressUpdateSchema` is the form schema `.partial()`,
|
|
151
|
+
* so `{ isDefault: true }` on its own is a valid body.
|
|
152
|
+
*/
|
|
153
|
+
const handleSetDefault = async (addr) => {
|
|
154
|
+
try {
|
|
155
|
+
// Same request shape as `handleSave` below, rather than a second
|
|
156
|
+
// hand-rolled fetch beside it.
|
|
157
|
+
const method = "PUT";
|
|
158
|
+
const res = await fetch(`${apiBase}/${addr.id}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({ isDefault: true }),
|
|
162
|
+
});
|
|
163
|
+
if (!res.ok)
|
|
164
|
+
throw new Error("Failed to set default address");
|
|
165
|
+
await load();
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const normalized = normalizeError(err);
|
|
169
|
+
setSaveError(toUserMessage(normalized.code, undefined, { fallback: "Failed to set default address" }));
|
|
170
|
+
}
|
|
171
|
+
};
|
|
144
172
|
const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
|
|
145
173
|
const setField = (key) => (value) => set(key, value);
|
|
146
|
-
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
174
|
+
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(StickyToolbar, { offset: "header", tone: "default", border: true, padding: "md", children: _jsxs(Row, { justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", color: "primary", children: "Pickup Addresses" }), _jsx(Text, { size: "xs", color: "muted", className: "mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(TextLink, { href: String(ROUTES.STORE.ADDRESSES_NEW), children: "Open full page \u2192" }), _jsxs(Button, { gap: "sm", size: "sm", onClick: openAdd, children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx(Span, { children: "Add Address" })] })] })] }) }), _jsxs(Div, { paddingX: "x-md-lg", className: "max-w-2xl", padding: "y-lg", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), isLoading ? (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })) : addresses.length === 0 ? (_jsxs(Stack, { border: "default", className: "border-2 border-dashed", padding: "y-4xl", align: "center", gap: "3", rounded: "xl", children: [_jsx(MapPin, { className: "h-8 w-8 text-[var(--appkit-color-text-muted)]" }), _jsx(Text, { size: "sm", color: "muted", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { justify: "end", className: "mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx(Grid, { gap: "sm", children: addresses.map((addr) => (_jsx(Div, { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr), onSetDefault: () => void handleSetDefault(addr) }) }, addr.id))) })) : (_jsx(Div, { className: `${__O.xAuto}`, rounded: "lg", border: "default", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { surface: "muted", children: _jsxs(Tr, { children: [_jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Label" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Name" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "City" }), _jsx(Th, { className: "text-left", padding: "sm", weight: "semibold", children: "Phone" }), _jsx(Th, { className: "text-right", padding: "sm", weight: "semibold", children: "Actions" })] }) }), _jsx(Tbody, { children: addresses.map((addr) => {
|
|
147
175
|
const isBanned = addr.banStatus === "banned";
|
|
148
176
|
return (_jsxs(Tr, { className: `border-t border-[var(--appkit-color-border)] ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx(Td, { padding: "sm", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { children: addr.label }), isBanned && _jsx(Badge, { variant: "danger", size: "sm", children: "Banned" })] }) }), _jsx(Td, { padding: "sm", children: addr.fullName }), _jsxs(Td, { padding: "sm", children: [addr.city, ", ", addr.state] }), _jsx(Td, { className: "tabular-nums", padding: "sm", children: addr.phone }), _jsx(Td, { className: "text-right", padding: "sm", children: isBanned ? (_jsx(Span, { size: "xs", color: "muted", title: "Contact support to resolve address ban", children: "Contact support" })) : (_jsxs(Row, { justify: "end", gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label })] })) })] }, addr.id));
|
|
149
177
|
}) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer:
|
|
@@ -2,6 +2,7 @@
|
|
|
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 { z } from "zod";
|
|
5
6
|
import { alertMetricsForScope } from "../../store-extensions/constants/alert-metrics.js";
|
|
6
7
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
7
8
|
import { Alert, Badge, Button, Div, Heading, Row, Section, Stack, Text, Toggle, useToast } from "../../../ui/index.js";
|
|
@@ -14,6 +15,7 @@ import { applyZodIssues } from "../../../ui/forms/apply-zod-issues.js";
|
|
|
14
15
|
import { buildSectionsFromSchema, visibleValues } from "../../shell/build-sections.js";
|
|
15
16
|
import { SectionForm, useSectionFormNav } from "../../shell/SectionForm.js";
|
|
16
17
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
18
|
+
import { QuickEditMenu } from "../../admin/components/QuickEditMenu.js";
|
|
17
19
|
const __P = {
|
|
18
20
|
p5: "p-[var(--appkit-space-5)]",
|
|
19
21
|
};
|
|
@@ -80,11 +82,55 @@ const OPERATOR_BADGE = {
|
|
|
80
82
|
// ---------------------------------------------------------------------------
|
|
81
83
|
// Sub-components
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
85
|
+
/**
|
|
86
|
+
* The three values a seller actually tunes once an alert is live.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately not `metric` or `operator`: changing either turns the alert
|
|
89
|
+
* into a different alert while keeping its firing history, so
|
|
90
|
+
* `lastTriggeredAt`/`lastObservedValue` on the card would then describe a rule
|
|
91
|
+
* that no longer exists. Those stay a delete-and-recreate.
|
|
92
|
+
*/
|
|
93
|
+
const alertQuickEditSchema = z.object({
|
|
94
|
+
label: z.string().trim().min(1, "A name is required."),
|
|
95
|
+
threshold: z.coerce.number().min(0, "Threshold cannot be negative."),
|
|
96
|
+
windowHours: z.coerce.number().int().min(1),
|
|
97
|
+
});
|
|
98
|
+
const ALERT_QUICK_FIELDS = [
|
|
99
|
+
{ name: "label", label: "Name", type: "text", required: true },
|
|
100
|
+
{
|
|
101
|
+
name: "threshold",
|
|
102
|
+
label: "Threshold",
|
|
103
|
+
type: "number",
|
|
104
|
+
required: true,
|
|
105
|
+
helperText: "The value the metric is compared against.",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "windowHours",
|
|
109
|
+
label: "Window",
|
|
110
|
+
type: "select",
|
|
111
|
+
required: true,
|
|
112
|
+
options: WINDOW_OPTIONS,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
function AlertCard({ alert, onToggle, onDelete, onEdit, }) {
|
|
84
116
|
const metricLabel = METRIC_OPTIONS.find((m) => m.value === alert.metric)?.label ?? alert.metric;
|
|
85
117
|
const opLabel = OPERATOR_OPTIONS.find((o) => o.value === alert.operator)?.label ?? alert.operator;
|
|
86
118
|
const windowLabel = WINDOW_OPTIONS.find((w) => w.value === String(alert.windowHours))?.label ?? `${alert.windowHours}h`;
|
|
87
|
-
return (_jsxs(Row, { surface: "card", padding: "sm", align: "start", justify: "between", gap: "md", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-1", gap: "sm", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: alert.label }), _jsx(Badge, { variant: alert.isActive ? "success" : "default", children: alert.isActive ? "Active" : "Paused" })] }), _jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: [metricLabel, " ", opLabel, " ", alert.threshold, " \u00B7 ", windowLabel] }), METRIC_DESCRIPTIONS[alert.metric] ? (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: METRIC_DESCRIPTIONS[alert.metric] })) : null, alert.lastTriggeredAt ? (_jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: ["Last fired ", new Date(alert.lastTriggeredAt).toLocaleString(), alert.lastObservedValue != null ? ` at ${alert.lastObservedValue}` : ""] })) : null, _jsx(Row, { gap: "xs", wrap: true, children: alert.notifyChannels.map((ch) => (_jsx(Badge, { variant: OPERATOR_BADGE[alert.operator] ?? "default", children: ch }, ch))) }), alert.lastTriggeredAt && (_jsxs(Text, { className: "mt-1", color: "faint", size: "xs", children: ["Last triggered: ", new Date(alert.lastTriggeredAt).toLocaleString("en-IN")] }))] }), _jsxs(Row, { className: "shrink-0", align: "center", gap: "sm", children: [_jsx(Toggle, { checked: alert.isActive, onChange: (v) => onToggle(alert.id, v), size: "sm"
|
|
119
|
+
return (_jsxs(Row, { surface: "card", padding: "sm", align: "start", justify: "between", gap: "md", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-1", gap: "sm", wrap: true, children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: alert.label }), _jsx(Badge, { variant: alert.isActive ? "success" : "default", children: alert.isActive ? "Active" : "Paused" })] }), _jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: [metricLabel, " ", opLabel, " ", alert.threshold, " \u00B7 ", windowLabel] }), METRIC_DESCRIPTIONS[alert.metric] ? (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: METRIC_DESCRIPTIONS[alert.metric] })) : null, alert.lastTriggeredAt ? (_jsxs(Text, { className: "mb-1", color: "muted", size: "xs", children: ["Last fired ", new Date(alert.lastTriggeredAt).toLocaleString(), alert.lastObservedValue != null ? ` at ${alert.lastObservedValue}` : ""] })) : null, _jsx(Row, { gap: "xs", wrap: true, children: alert.notifyChannels.map((ch) => (_jsx(Badge, { variant: OPERATOR_BADGE[alert.operator] ?? "default", children: ch }, ch))) }), alert.lastTriggeredAt && (_jsxs(Text, { className: "mt-1", color: "faint", size: "xs", children: ["Last triggered: ", new Date(alert.lastTriggeredAt).toLocaleString("en-IN")] }))] }), _jsxs(Row, { className: "shrink-0", align: "center", gap: "sm", children: [_jsx(Toggle, { checked: alert.isActive, onChange: (v) => onToggle(alert.id, v), size: "sm", ariaLabel: `${alert.isActive ? "Pause" : "Resume"} ${alert.label}` }), _jsx(QuickEditMenu, { actions: [
|
|
120
|
+
{
|
|
121
|
+
label: "Edit alert",
|
|
122
|
+
formTitle: `Edit "${alert.label}"`,
|
|
123
|
+
schema: alertQuickEditSchema,
|
|
124
|
+
fields: ALERT_QUICK_FIELDS,
|
|
125
|
+
defaultValues: {
|
|
126
|
+
label: alert.label,
|
|
127
|
+
threshold: alert.threshold,
|
|
128
|
+
windowHours: String(alert.windowHours),
|
|
129
|
+
},
|
|
130
|
+
onSubmit: (values) => onEdit(alert.id, values),
|
|
131
|
+
submitLabel: "Save alert",
|
|
132
|
+
},
|
|
133
|
+
] }), _jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-analytics-alert"], onClick: () => onDelete(alert.id), className: "text-error hover:text-error" })] })] }));
|
|
88
134
|
}
|
|
89
135
|
const EMPTY_DRAFT = {
|
|
90
136
|
label: "",
|
|
@@ -142,6 +188,17 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
142
188
|
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
143
189
|
},
|
|
144
190
|
});
|
|
191
|
+
const editMutation = useApiMutation({
|
|
192
|
+
errorMessage: "Failed to update alert",
|
|
193
|
+
mutationFn: async ({ id, values }) => {
|
|
194
|
+
const parsed = alertQuickEditSchema.parse(values);
|
|
195
|
+
return apiClient.patch(SELLER_ENDPOINTS.ANALYTICS_ALERT_BY_ID(id), parsed);
|
|
196
|
+
},
|
|
197
|
+
onSuccess: () => {
|
|
198
|
+
showToast("Alert updated", "success");
|
|
199
|
+
void queryClient.invalidateQueries({ queryKey: ["seller", "analytics-alerts"] });
|
|
200
|
+
},
|
|
201
|
+
});
|
|
145
202
|
const toggleChannel = useCallback((ch) => {
|
|
146
203
|
setDraft((d) => ({
|
|
147
204
|
...d,
|
|
@@ -192,5 +249,7 @@ export function SellerAnalyticsAlertsView({ labels = {}, }) {
|
|
|
192
249
|
* page's own form — a viewport-pinned Save would claim the
|
|
193
250
|
* bottom tier for a control the reader may have scrolled past.
|
|
194
251
|
*/
|
|
195
|
-
bottomBar: false })] })] })), isLoading && (_jsx(Stack, { gap: "sm", children: [1, 2, 3].map((i) => (_jsx(Div, { className: "h-16 animate-pulse", surface: "subtle", rounded: "lg" }, i))) })), !isLoading && alerts.length === 0 && !showForm && (_jsx(Alert, { variant: "info", children: "No alerts configured. Create an alert to get notified when a key metric crosses a threshold." })), !isLoading && alerts.length > 0 && (_jsx(Stack, { gap: "3", children: alerts.map((alert) => (_jsx(AlertCard, { alert: alert, onToggle: (id, isActive) => toggleMutation.mutate({ id, isActive }), onDelete: (id) => deleteMutation.mutate(id)
|
|
252
|
+
bottomBar: false })] })] })), isLoading && (_jsx(Stack, { gap: "sm", children: [1, 2, 3].map((i) => (_jsx(Div, { className: "h-16 animate-pulse", surface: "subtle", rounded: "lg" }, i))) })), !isLoading && alerts.length === 0 && !showForm && (_jsx(Alert, { variant: "info", children: "No alerts configured. Create an alert to get notified when a key metric crosses a threshold." })), !isLoading && alerts.length > 0 && (_jsx(Stack, { gap: "3", children: alerts.map((alert) => (_jsx(AlertCard, { alert: alert, onToggle: (id, isActive) => toggleMutation.mutate({ id, isActive }), onDelete: (id) => deleteMutation.mutate(id), onEdit: async (id, values) => {
|
|
253
|
+
await editMutation.mutateAsync({ id, values });
|
|
254
|
+
} }, alert.id))) }))] }));
|
|
196
255
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { SIEVE_OP, sieveFilter, sortBy } from "@mohasinac/appkit/client";
|
|
4
|
-
import { Badge, Button, FilterChipGroup, Stack, Text, TextLink } from "../../../ui/index.js";
|
|
4
|
+
import { Badge, Button, FilterChipGroup, Stack, Text, TextLink, Toggle } from "../../../ui/index.js";
|
|
5
5
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
6
6
|
import { ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs.js";
|
|
7
7
|
import { SELLER_BULK_ACTIONS } from "../../products/constants/action-defs.js";
|
|
8
8
|
import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy.js";
|
|
9
9
|
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
|
|
10
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
11
|
+
import { apiClient } from "../../../http/index.js";
|
|
10
12
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
11
13
|
function formatPrice(value) {
|
|
12
14
|
if (typeof value !== "number" || value <= 0)
|
|
@@ -38,6 +40,22 @@ const COLUMNS = [
|
|
|
38
40
|
{ key: "updatedAt", header: "Updated", className: "w-28" },
|
|
39
41
|
];
|
|
40
42
|
export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
43
|
+
/*
|
|
44
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
45
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
46
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
47
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
48
|
+
*
|
|
49
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
50
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
51
|
+
* the override key rather than write back a captured value.
|
|
52
|
+
*/
|
|
53
|
+
const activeFlag = useOptimisticRowFlag({
|
|
54
|
+
mutate: (id, next) => apiClient.put(SELLER_ENDPOINTS.BUNDLE_BY_ID(id), { isActive: next }),
|
|
55
|
+
invalidate: [["seller", "bundles", "listing"], ["bundles"]],
|
|
56
|
+
successMessage: (next) => (next ? "Bundle activated." : "Bundle deactivated."),
|
|
57
|
+
errorMessage: "Could not change that bundle's status.",
|
|
58
|
+
});
|
|
41
59
|
const config = {
|
|
42
60
|
portal: "seller",
|
|
43
61
|
title: "Bundles",
|
|
@@ -60,19 +78,29 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
|
60
78
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
61
79
|
],
|
|
62
80
|
columns: COLUMNS,
|
|
63
|
-
mapRows: (response) => toRecordArray(response.items).map((item, index) =>
|
|
64
|
-
id
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
mapRows: (response) => toRecordArray(response.items).map((item, index) => {
|
|
82
|
+
const id = toStringValue(item.id, `bundle-${index}`);
|
|
83
|
+
/*
|
|
84
|
+
* Resolved ONCE, through the pending override, and then used for both
|
|
85
|
+
* `isActive` and the `status` LABEL below. Deriving the label from the
|
|
86
|
+
* raw value while the flag read the override is how the badge ends up
|
|
87
|
+
* saying "Inactive" beside a switch that is on.
|
|
88
|
+
*/
|
|
89
|
+
const isActive = activeFlag.resolve(id, item.isActive === true);
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
primary: toStringValue(item.name, "Untitled bundle"),
|
|
93
|
+
secondary: toStringValue(item.slug, "no-slug"),
|
|
94
|
+
price: formatPrice(item.bundlePrice),
|
|
95
|
+
members: String(Array.isArray(item.bundleProductIds) ? item.bundleProductIds.length : 0),
|
|
96
|
+
stockStatus: toStringValue(item.bundleStockStatus, "in_stock"),
|
|
97
|
+
isActive,
|
|
98
|
+
status: isActive
|
|
99
|
+
? BUNDLE_COPY.adminList.activeBadge
|
|
100
|
+
: BUNDLE_COPY.adminList.inactiveBadge,
|
|
101
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
76
104
|
getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
|
|
77
105
|
buildFilters: (filterState) => {
|
|
78
106
|
const parts = [];
|
|
@@ -83,6 +111,17 @@ export function SellerBundlesView({ editHrefTemplate, newHref }) {
|
|
|
83
111
|
return parts.join(",") || undefined;
|
|
84
112
|
},
|
|
85
113
|
rowHrefTemplate: editHrefTemplate,
|
|
114
|
+
/*
|
|
115
|
+
* 🛑 This view had NO renderRowActions at all — the only per-row
|
|
116
|
+
* affordance was the row link to the editor, so a bundle could only be
|
|
117
|
+
* activated or deactivated by opening it.
|
|
118
|
+
*
|
|
119
|
+
* The toggle lives here rather than in a column because `rowHrefTemplate`
|
|
120
|
+
* makes the row an anchor: a normal column cell does not stop
|
|
121
|
+
* propagation, so a switch there would flip AND navigate. This slot is
|
|
122
|
+
* already click-stopped in all three renderers.
|
|
123
|
+
*/
|
|
124
|
+
renderRowActions: (row) => (_jsx(Toggle, { size: "sm", checked: row.isActive, disabled: activeFlag.pendingId === row.id, ariaLabel: `${row.isActive ? "Deactivate" : "Activate"} ${row.primary}`, onChange: (next) => { void activeFlag.set(row.id, next); } })),
|
|
86
125
|
toolbarExtra: (_jsx(Button, { asChild: true, size: "sm", variant: "primary", children: _jsx(TextLink, { href: newHref, layout: "flex", align: "center", gap: "xs", children: "+ New Bundle" }) })),
|
|
87
126
|
// Rule #7: bulk-action array sourced from the SELLER_BULK_ACTIONS preset.
|
|
88
127
|
buildBulkActions: (selection) => {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Row, sortBy } from "@mohasinac/appkit/client";
|
|
4
|
-
import { Badge, Button, Span } from "../../../ui/index.js";
|
|
4
|
+
import { Badge, Button, Span, Toggle } from "../../../ui/index.js";
|
|
5
5
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
6
6
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
7
7
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
8
8
|
import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData.js";
|
|
9
|
+
import { useOptimisticRowFlag } from "../../admin/hooks/useOptimisticRowFlag.js";
|
|
10
|
+
import { apiClient } from "../../../http/index.js";
|
|
9
11
|
import { DataListingView } from "../../admin/components/DataListingView.js";
|
|
10
12
|
const THEME_LABELS = {
|
|
11
13
|
related: "Related",
|
|
@@ -51,6 +53,22 @@ const COLUMNS = [
|
|
|
51
53
|
},
|
|
52
54
|
];
|
|
53
55
|
export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDeleteClick, }) {
|
|
56
|
+
/*
|
|
57
|
+
* 🛑 `isActive` was a READ-ONLY badge here, and the update schema was built
|
|
58
|
+
* to take it on its own — shipping-config's even says so in a comment: "a
|
|
59
|
+
* PATCH may legitimately carry only `isActive`". Nothing in the client had
|
|
60
|
+
* ever sent one, so flipping one boolean meant a full-editor round trip.
|
|
61
|
+
*
|
|
62
|
+
* The optimistic/rollback logic lives in the hook because eight listings
|
|
63
|
+
* grew this control at once; see its header for why rollback must DELETE
|
|
64
|
+
* the override key rather than write back a captured value.
|
|
65
|
+
*/
|
|
66
|
+
const activeFlag = useOptimisticRowFlag({
|
|
67
|
+
mutate: (id, next) => apiClient.patch(SELLER_ENDPOINTS.GROUPED_LISTING_BY_ID(id), { isActive: next }),
|
|
68
|
+
invalidate: [["seller", "grouped-listings"], ["grouped-listings"]],
|
|
69
|
+
successMessage: (next) => (next ? "Group activated." : "Group deactivated."),
|
|
70
|
+
errorMessage: "Could not change that group's status.",
|
|
71
|
+
});
|
|
54
72
|
const config = {
|
|
55
73
|
portal: "seller",
|
|
56
74
|
title: "Grouped Listings",
|
|
@@ -72,7 +90,9 @@ export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDelete
|
|
|
72
90
|
title: toStringValue(item.title, "Untitled group"),
|
|
73
91
|
groupTheme: toStringValue(item.groupTheme, "generic"),
|
|
74
92
|
productCount: Array.isArray(item.productIds) ? item.productIds.length : 0,
|
|
75
|
-
|
|
93
|
+
// Resolved through the pending override so the Status badge and the
|
|
94
|
+
// row toggle cannot disagree between the click and the refetch.
|
|
95
|
+
isActive: activeFlag.resolve(toStringValue(item.id, `group-${index}`), item.isActive === true),
|
|
76
96
|
visibilityStatus: toStringValue(item.visibilityStatus, "hidden"),
|
|
77
97
|
createdAt: toRelativeDate(item.createdAt),
|
|
78
98
|
})),
|
|
@@ -92,7 +112,7 @@ export function SellerGroupedListingsView({ onCreateClick, onEditClick, onDelete
|
|
|
92
112
|
: undefined,
|
|
93
113
|
// Mirrors the "Edit" row action below so table rows and cards both navigate on click.
|
|
94
114
|
onRowClick: onEditClick ? (row) => onEditClick(row.id) : undefined,
|
|
95
|
-
renderRowActions: (row) => (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => onEditClick?.(row.id), children: ACTIONS.STORE["edit-listing"].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onDeleteClick?.(row.id), children: ACTIONS.STORE["delete-listing"].label })] })),
|
|
115
|
+
renderRowActions: (row) => (_jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Toggle, { size: "sm", checked: row.isActive, disabled: activeFlag.pendingId === row.id, ariaLabel: `${row.isActive ? "Deactivate" : "Activate"} ${row.title}`, onChange: (next) => { void activeFlag.set(row.id, next); } }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onEditClick?.(row.id), children: ACTIONS.STORE["edit-listing"].label }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => onDeleteClick?.(row.id), children: ACTIONS.STORE["delete-listing"].label })] })),
|
|
96
116
|
};
|
|
97
117
|
return _jsx(DataListingView, { config: config });
|
|
98
118
|
}
|