@mohasinac/appkit 4.61.0 → 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.
Files changed (96) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +19 -3
  3. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
  5. package/dist/_internal/server/jobs/core/paymentReconcile.js +87 -97
  6. package/dist/_internal/shared/actions/action-registry.js +149 -9
  7. package/dist/_internal/shared/media/limits.d.ts +10 -0
  8. package/dist/_internal/shared/media/limits.js +10 -0
  9. package/dist/_internal/shared/types/action-result.js +15 -1
  10. package/dist/client/api/surface-error.js +16 -2
  11. package/dist/client/api/useApiMutation.js +17 -1
  12. package/dist/client.d.ts +1 -1
  13. package/dist/client.js +1 -1
  14. package/dist/constants/api-endpoints.d.ts +3 -0
  15. package/dist/constants/api-endpoints.js +1 -0
  16. package/dist/constants/field-names.d.ts +17 -0
  17. package/dist/constants/field-names.js +23 -0
  18. package/dist/core/server-action.js +19 -3
  19. package/dist/errors/error-mapping.js +26 -1
  20. package/dist/features/account/components/UserOffersPanel.js +5 -3
  21. package/dist/features/admin/components/AdminActionIndexView.js +100 -5
  22. package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
  23. package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
  24. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
  25. package/dist/features/admin/components/AdminCarouselView.js +96 -4
  26. package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
  27. package/dist/features/admin/components/AdminMediaView.js +36 -3
  28. package/dist/features/admin/components/AdminNavigationView.js +4 -1
  29. package/dist/features/admin/components/AdminOffersView.js +2 -1
  30. package/dist/features/admin/components/AdminPaymentClustersView.js +19 -2
  31. package/dist/features/admin/components/AdminProductEditorView.js +2 -2
  32. package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
  33. package/dist/features/admin/components/AdminSectionsView.js +486 -224
  34. package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
  35. package/dist/features/admin/hooks/useOptimisticRowFlag.d.ts +33 -0
  36. package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
  37. package/dist/features/admin/repository/notification.repository.d.ts +15 -0
  38. package/dist/features/admin/repository/notification.repository.js +32 -0
  39. package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
  40. package/dist/features/admin/schemas/action-index-form.js +115 -0
  41. package/dist/features/auth/components/RegisterForm.js +2 -1
  42. package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
  43. package/dist/features/events/components/AdminEventEditorView.js +8 -6
  44. package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
  45. package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
  46. package/dist/features/orders/components/RefundRequestView.js +4 -4
  47. package/dist/features/orders/schemas/index.d.ts +6 -6
  48. package/dist/features/products/components/GroupMemberPicker.js +4 -1
  49. package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
  50. package/dist/features/products/components/ProductDetailActions.js +2 -1
  51. package/dist/features/seller/components/FulfillmentView.js +3 -2
  52. package/dist/features/seller/components/SellerAddressesView.js +32 -4
  53. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
  54. package/dist/features/seller/components/SellerBundlesView.js +53 -14
  55. package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
  56. package/dist/features/seller/components/SellerListingTypeView.js +56 -10
  57. package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
  58. package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
  59. package/dist/features/seller/components/SellerProductsView.js +5 -1
  60. package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
  61. package/dist/features/seller/components/SellerShippingView.js +2 -1
  62. package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
  63. package/dist/features/seller/components/SellerSupportView.js +69 -2
  64. package/dist/features/shell/SectionForm.js +20 -16
  65. package/dist/features/shell/build-sections.js +14 -1
  66. package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
  67. package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
  68. package/dist/features/sticker-trace/components/StickerCommitPanel.js +2 -1
  69. package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
  70. package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
  71. package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
  72. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
  73. package/dist/index.d.ts +5 -1
  74. package/dist/index.js +3 -1
  75. package/dist/next/routing/route-map.d.ts +4 -0
  76. package/dist/next/routing/route-map.js +9 -0
  77. package/dist/schemas/registry.d.ts +4 -4
  78. package/dist/schemas/sieve.d.ts +3 -3
  79. package/dist/server.d.ts +1 -1
  80. package/dist/server.js +1 -1
  81. package/dist/ui/components/Checkbox.js +10 -2
  82. package/dist/ui/components/DateInput.js +8 -1
  83. package/dist/ui/components/Input.js +22 -2
  84. package/dist/ui/components/PaginatedSelect.d.ts +8 -19
  85. package/dist/ui/components/PaginatedSelect.js +43 -9
  86. package/dist/ui/components/Radio.js +8 -1
  87. package/dist/ui/components/Select.js +10 -2
  88. package/dist/ui/components/TagInput.d.ts +8 -1
  89. package/dist/ui/components/TagInput.js +4 -3
  90. package/dist/ui/components/Textarea.js +10 -2
  91. package/dist/ui/forms/FieldCheckbox.js +1 -1
  92. package/dist/ui/forms/FormErrorList.d.ts +0 -16
  93. package/dist/ui/forms/FormErrorList.js +34 -1
  94. package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
  95. package/dist/ui/forms/apply-zod-issues.js +23 -1
  96. package/package.json +1 -1
@@ -2,8 +2,10 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useApiMutation } from "@mohasinac/appkit/client";
4
4
  import React, { useState, useCallback } from "react";
5
+ import { useRouter } from "next/navigation.js";
5
6
  import { useQueryClient } from "@tanstack/react-query";
6
- import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, StickyToolbar } from "../../../ui/index.js";
7
+ import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, TextLink, StickyToolbar, Toggle, useToast } from "../../../ui/index.js";
8
+ import { ROUTES } from "../../../next/routing/route-map.js";
7
9
  import { useBottomActions } from "../../layout/index.js";
8
10
  import { MediaImage } from "../../media/MediaImage.js";
9
11
  import { ADMIN_ENDPOINTS, HOMEPAGE_ENDPOINTS } from "../../../constants/api-endpoints.js";
@@ -14,6 +16,8 @@ import { useAdminListing } from "../hooks/useAdminListing.js";
14
16
  import { DataTable } from "./DataTable.js";
15
17
  import { RowActionMenu } from "../../../ui/components/RowActionMenu.js";
16
18
  import { apiClient } from "../../../http/index.js";
19
+ import { normalizeError } from "../../../errors/normalize.js";
20
+ import { toUserMessage } from "../../../errors/error-display-map.js";
17
21
  const PAGE_SIZE = 50;
18
22
  const FILTER_KEYS = ["active"];
19
23
  const DEFAULT_SORT = "order";
@@ -33,6 +37,8 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
33
37
  const [deleteTargetId, setDeleteTargetId] = useState(null);
34
38
  const [localRows, setLocalRows] = useState([]);
35
39
  const queryClient = useQueryClient();
40
+ const router = useRouter();
41
+ const { showToast } = useToast();
36
42
  const { view, setView, table, searchInput, setSearchInput, commitSearch, filterOpen, setFilterOpen, openFilters, applyFilters, clearFilters, pendingFilters, setPendingFilters, activeFilterCount, hasActiveState, resetAll, rows: fetchedRows, isLoading, errorMessage, currentPage, totalPages, selection, } = useAdminListing({
37
43
  filterKeys: FILTER_KEYS,
38
44
  defaultSort: DEFAULT_SORT,
@@ -111,8 +117,58 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
111
117
  setDraggingId(null);
112
118
  reorderMutation.mutate(next.map((r) => r.id));
113
119
  };
120
+ const setActiveMutation = useApiMutation({
121
+ errorMessage: "Could not change that slide's status.",
122
+ mutationFn: ({ id, active }) =>
123
+ // PUT, not PATCH: this route has no PATCH, and every field on
124
+ // `updateSlideSchema` is optional, so a lone `{ active }` is a valid
125
+ // partial update.
126
+ apiClient.put(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id), { active }),
127
+ onSuccess: () => {
128
+ queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
129
+ // The public hero carousel reads under its own key.
130
+ queryClient.invalidateQueries({ queryKey: ["carousel"] });
131
+ },
132
+ });
133
+ /*
134
+ * 🛑 The bulk delete used to be gated entirely on an `onBulkDelete` PROP —
135
+ * and the page renders `<AdminCarouselView />` with no props at all. So
136
+ * `bulkActions` was always `[]`, the BulkActionBar never rendered, and
137
+ * DataTable was handed no selection props either, which meant no checkbox
138
+ * column existed to select with in the first place.
139
+ * `ACTIONS.ADMIN["delete-carousel"]` and its confirmation dialog were
140
+ * unreachable from this page.
141
+ *
142
+ * The prop is kept so an embedder can still override the behaviour, but it
143
+ * no longer decides whether the action EXISTS — this view owns a delete
144
+ * mutation already and can perfectly well call it N times.
145
+ */
146
+ const handleBulkDelete = useCallback(async () => {
147
+ const ids = selection.selectedIds;
148
+ try {
149
+ if (onBulkDelete) {
150
+ await onBulkDelete(ids);
151
+ }
152
+ else {
153
+ await Promise.all(ids.map((id) => apiClient.delete(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id))));
154
+ await queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
155
+ }
156
+ selection.clearSelection();
157
+ showToast(`${ids.length} slide${ids.length === 1 ? "" : "s"} deleted.`, "success");
158
+ }
159
+ catch (err) {
160
+ const normalized = normalizeError(err);
161
+ /*
162
+ * The selection is deliberately NOT cleared on failure. `Promise.all`
163
+ * rejects on the first error, so some of these ids may be gone and
164
+ * some may not — clearing would hide which rows are still there and
165
+ * leave the admin no way to retry the remainder.
166
+ */
167
+ showToast(toUserMessage(normalized.code, undefined, { fallback: "Could not delete the selected slides." }), "error");
168
+ }
169
+ }, [onBulkDelete, selection, queryClient, showToast]);
114
170
  const bulkActions = [
115
- ...(onBulkDelete ? [buildBulkAction(ACTIONS.ADMIN["delete-carousel"], async () => { await onBulkDelete(selection.selectedIds); selection.clearSelection(); })] : []),
171
+ buildBulkAction(ACTIONS.ADMIN["delete-carousel"], handleBulkDelete),
116
172
  ];
117
173
  useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
118
174
  if (hasChildren) {
@@ -138,6 +194,24 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
138
194
  header: "Details",
139
195
  render: (row) => row.secondary,
140
196
  },
197
+ {
198
+ /*
199
+ * 🛑 Both preventDefault AND stopPropagation.
200
+ *
201
+ * `rowHrefTemplate` makes the whole row an anchor, so stopping
202
+ * propagation alone leaves the anchor's DEFAULT action intact — the
203
+ * toggle would flip and then navigate to the edit page, which reads as
204
+ * "clicking the switch opens the editor". A normal column cell is also
205
+ * outside the two `<Td>`s DataTable stops propagation on; only the
206
+ * selection and row-action cells get that for free.
207
+ */
208
+ key: "active",
209
+ header: "Active",
210
+ className: "w-24",
211
+ render: (row) => (_jsx(Row, { align: "center", gap: "sm", role: "presentation", onClick: (e) => { e.preventDefault(); e.stopPropagation(); }, onKeyDown: (e) => e.stopPropagation(), children: _jsx(Toggle, { size: "sm", checked: row.status === "Active", ariaLabel: row.status === "Active"
212
+ ? `${ACTIONS.ADMIN["deactivate-carousel"].ariaLabel}: ${row.primary}`
213
+ : `${ACTIONS.ADMIN["activate-carousel"].ariaLabel}: ${row.primary}`, onChange: (next) => setActiveMutation.mutate({ id: row.id, active: next }) }) })),
214
+ },
141
215
  {
142
216
  key: "status",
143
217
  header: "Status",
@@ -152,7 +226,10 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
152
226
  key: "actions",
153
227
  header: "",
154
228
  render: (row) => (_jsx(RowActionMenu, { actions: [
155
- { label: ACTIONS.ADMIN["edit-carousel"].label, onClick: () => { window.location.href = `/admin/carousel/${row.id}/edit`; } },
229
+ // router.push, not window.location.href — a full page load to
230
+ // reach a page the row already links to is strictly worse, and
231
+ // this is the same navigation DataTable's row anchor performs.
232
+ { label: ACTIONS.ADMIN["edit-carousel"].label, onClick: () => router.push(String(ROUTES.ADMIN.CAROUSEL_EDIT(row.id))) },
156
233
  {
157
234
  label: ACTIONS.ADMIN["delete-carousel"].label,
158
235
  destructive: true,
@@ -163,5 +240,20 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
163
240
  ];
164
241
  return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput,
165
242
  // Search intentionally absent: this endpoint does not read `q`, so the box accepted typing and changed nothing. Restore it when the collection gains searchTxt — audit-listing-search-capability tracks it.
166
- onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides \u2014 use 'New slide' to add one", rowHrefTemplate: "/admin/carousel/{id}/edit" })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Carousel Slide", message: "Delete this carousel slide? This cannot be undone.", onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
243
+ onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState,
244
+ /*
245
+ * 🛑 The empty state named a button that did not exist: "No carousel
246
+ * slides — use 'New slide' to add one", with no New slide control
247
+ * anywhere on the page. `/admin/carousel/new` has always been a real
248
+ * route and nothing linked to it.
249
+ */
250
+ extra: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }) }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides yet", emptyAction: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }), rowHrefTemplate: "/admin/carousel/{id}/edit",
251
+ /*
252
+ * Selection was destructured and a bulk-action array built from it,
253
+ * and these three props were never passed — so no checkbox column
254
+ * rendered and nothing could be selected to act on.
255
+ */
256
+ selectedIds: selection.selectedIdSet, onToggleSelect: (id) => selection.toggle(id), onToggleSelectAll: () => selection.toggleAll() })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true,
257
+ /* Rule #7: confirmation copy lives on the ActionDef, not here. */
258
+ title: ACTIONS.ADMIN["delete-carousel"].confirmation?.title, message: ACTIONS.ADMIN["delete-carousel"].confirmation?.body, confirmText: ACTIONS.ADMIN["delete-carousel"].confirmation?.confirmLabel, onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
167
259
  }
@@ -9,7 +9,8 @@ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
9
9
  import { apiClient } from "../../../http/index.js";
10
10
  import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
11
11
  import { normalizeError } from "../../../errors/normalize.js";
12
- import { useToast } from "../../../ui/index.js";
12
+ import { useToast, TextLink } from "../../../ui/index.js";
13
+ import { ROUTES } from "../../../next/routing/route-map.js";
13
14
  async function loadStoreOptions(query, page) {
14
15
  const params = new URLSearchParams({ page: String(page), pageSize: "25" });
15
16
  if (query)
@@ -105,5 +106,5 @@ export function AdminFulfillmentView({ initialStoreId, initialStoreName, }) {
105
106
  setStoreId(null);
106
107
  setOrders([]);
107
108
  }, children: "Change Store" }))] }), !storeId && (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", color: "muted", children: "Select a store to view its fulfilment queue." }), _jsx(PaginatedSelect, { value: null, onChange: (v) => { if (v)
108
- setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["#", order.id] }), _jsx(Badge, { variant: statusBadgeVariant(order.status), children: order.status }), order.pickedAt && _jsx(Badge, { variant: "success", children: "Picked" }), order.packedAt && _jsx(Badge, { variant: "success", children: "Packed" })] }), _jsxs(Text, { size: "xs", color: "muted", children: [order.userName, order.items && order.items.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 ", order.items.map((i) => i.productTitle).join(", ")] }))] }), order.assignedWorkerId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Assigned to: ", order.assignedWorkerId] })), _jsxs(Row, { gap: "xs", children: [!order.pickedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPicked(order.id), isLoading: actionLoadingId === order.id + ":picked", children: "Mark Picked" })), order.pickedAt && !order.packedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPacked(order.id), isLoading: actionLoadingId === order.id + ":packed", children: "Mark Packed" })), order.assignedWorkerId && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleUnassign(order.id), isLoading: actionLoadingId === order.id + ":unassign", children: "Unassign" }))] })] }, order.id)))] })), initialStoreName && !storeId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Previously viewing: ", initialStoreName] }))] }));
109
+ setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(TextLink, { href: String(ROUTES.ADMIN.ORDER_DETAIL(order.id)), size: "sm", weight: "medium", children: ["#", order.id] }), _jsx(Badge, { variant: statusBadgeVariant(order.status), children: order.status }), order.pickedAt && _jsx(Badge, { variant: "success", children: "Picked" }), order.packedAt && _jsx(Badge, { variant: "success", children: "Packed" })] }), _jsxs(Text, { size: "xs", color: "muted", children: [order.userName, order.items && order.items.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 ", order.items.map((i) => i.productTitle).join(", ")] }))] }), order.assignedWorkerId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Assigned to: ", order.assignedWorkerId] })), _jsxs(Row, { gap: "xs", children: [!order.pickedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPicked(order.id), isLoading: actionLoadingId === order.id + ":picked", children: "Mark Picked" })), order.pickedAt && !order.packedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPacked(order.id), isLoading: actionLoadingId === order.id + ":packed", children: "Mark Packed" })), order.assignedWorkerId && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleUnassign(order.id), isLoading: actionLoadingId === order.id + ":unassign", children: "Unassign" }))] })] }, order.id)))] })), initialStoreName && !storeId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Previously viewing: ", initialStoreName] }))] }));
109
110
  }
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
- import { Alert, Button, Div, Grid, Input, Row, Select, Stack, StackedViewShell, Text, useToast } from "../../../ui/index.js";
4
+ import { Alert, Button, ConfirmDeleteModal, Div, Grid, Input, Row, Select, Stack, StackedViewShell, Text, TextLink, useToast } from "../../../ui/index.js";
5
5
  import { normalizeError } from "../../../errors/normalize.js";
6
6
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
7
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
8
8
  import { MediaImage, MediaUploadField, MediaUploadList, useMediaCleanup, useMediaUpload, } from "../../media/index.js";
9
+ import { toUserMessage } from "../../../errors/error-display-map.js";
9
10
  const __P = {
10
11
  p4: "p-[var(--appkit-space-4)]",
11
12
  };
@@ -28,6 +29,9 @@ function MediaBrowser({ onCopy }) {
28
29
  const [nextPageToken, setNextPageToken] = React.useState(null);
29
30
  const [isLoading, setIsLoading] = React.useState(false);
30
31
  const [error, setError] = React.useState(null);
32
+ const [deleteTarget, setDeleteTarget] = React.useState(null);
33
+ const [isDeleting, setIsDeleting] = React.useState(false);
34
+ const toast = useToast();
31
35
  const load = React.useCallback(async (token) => {
32
36
  setIsLoading(true);
33
37
  setError(null);
@@ -54,6 +58,33 @@ function MediaBrowser({ onCopy }) {
54
58
  setIsLoading(false);
55
59
  }
56
60
  }, [prefix]);
61
+ const onDelete = React.useCallback((file) => setDeleteTarget(file), []);
62
+ const confirmDelete = React.useCallback(async () => {
63
+ if (!deleteTarget)
64
+ return;
65
+ setIsDeleting(true);
66
+ try {
67
+ const res = await fetch(`${ADMIN_ENDPOINTS.MEDIA_LIST}?path=${encodeURIComponent(deleteTarget.name)}`, { method: "DELETE", credentials: "include" });
68
+ if (!res.ok)
69
+ throw new Error("Failed to delete file");
70
+ /*
71
+ * Drop it from local state rather than refetching: this list is
72
+ * token-paginated and accumulates pages, so a reload would discard
73
+ * every page past the first and silently scroll the admin back to the
74
+ * top of a library they were partway through.
75
+ */
76
+ setFiles((prev) => prev.filter((f) => f.name !== deleteTarget.name));
77
+ toast.showToast("File deleted.", "success");
78
+ }
79
+ catch (err) {
80
+ const normalized = normalizeError(err);
81
+ toast.showToast(toUserMessage(normalized.code, undefined, { fallback: "Failed to delete file." }), "error");
82
+ }
83
+ finally {
84
+ setIsDeleting(false);
85
+ setDeleteTarget(null);
86
+ }
87
+ }, [deleteTarget, toast]);
57
88
  React.useEffect(() => {
58
89
  void load(null);
59
90
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -66,8 +97,10 @@ function MediaBrowser({ onCopy }) {
66
97
  }, [files, search]);
67
98
  return (_jsxs(Stack, { className: `${__P.p4}`, gap: "3", rounded: "xl", surface: "default", border: "default", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Browse existing media" }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Select, { value: prefix, onChange: (e) => setPrefix(e.target.value), options: PREFIX_OPTIONS, "aria-label": "Prefix filter", wrapperClassName: "min-w-[140px]" }), _jsx(Input, { type: "text", placeholder: "Search filename\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "flex-1 min-w-[160px]" })] }), error && (_jsx(Alert, { variant: "error", title: "Load failed", children: error })), _jsxs(Grid, { cols: "navTiles", gap: "sm", children: [filtered.map((f) => {
68
99
  const isImage = (f.contentType ?? "").startsWith("image/");
69
- return (_jsxs(Div, { className: "group overflow-hidden", surface: "muted", rounded: "lg", border: "default", children: [_jsx(Row, { color: "muted", textSize: "xs", className: "relative aspect-square", surface: "subtle", align: "center", justify: "center", children: isImage ? (_jsx(MediaImage, { src: f.downloadURL, alt: f.name, size: "thumbnail" })) : (_jsx(Text, { align: "center", paddingX: "x-xs", className: "break-all", children: f.contentType ?? "file" })) }), _jsxs(Div, { className: "p-[0.375rem]", children: [_jsx(Text, { className: "truncate text-[10px] font-mono", color: "muted", children: f.name.split("/").pop() }), _jsx(Button, { type: "button", variant: "ghost", textSize: "xs", className: "mt-1 w-full", onClick: () => onCopy(f.downloadURL), children: ACTIONS.MEDIA["copy-url"].label })] })] }, f.name));
70
- }), filtered.length === 0 && !isLoading && (_jsx(Text, { paddingY: "lg", className: "col-span-full", color: "muted", size: "sm", align: "center", children: "No files found." }))] }), _jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { size: "xs", color: "muted", children: [filtered.length, " file(s)", nextPageToken ? " · more available" : ""] }), nextPageToken && (_jsx(Button, { type: "button", variant: "outline", onClick: () => void load(nextPageToken), disabled: isLoading, children: isLoading ? "Loading…" : "Load more" }))] })] }));
100
+ return (_jsxs(Div, { className: "group overflow-hidden", surface: "muted", rounded: "lg", border: "default", children: [_jsx(Row, { color: "muted", textSize: "xs", className: "relative aspect-square", surface: "subtle", align: "center", justify: "center", children: isImage ? (_jsx(MediaImage, { src: f.downloadURL, alt: f.name, size: "thumbnail" })) : (_jsx(Text, { align: "center", paddingX: "x-xs", className: "break-all", children: f.contentType ?? "file" })) }), _jsxs(Div, { className: "p-[0.375rem]", children: [_jsx(TextLink, { href: f.downloadURL, target: "_blank", rel: "noopener noreferrer", title: f.name, children: _jsx(Text, { className: "truncate text-[10px] font-mono", color: "muted", children: f.name.split("/").pop() }) }), _jsxs(Row, { gap: "xs", className: "mt-1", children: [_jsx(Button, { type: "button", variant: "ghost", textSize: "xs", className: "flex-1", onClick: () => onCopy(f.downloadURL), children: ACTIONS.MEDIA["copy-url"].label }), _jsx(Button, { type: "button", variant: "ghost", textSize: "xs", "aria-label": `${ACTIONS.MEDIA["delete-media"].ariaLabel}: ${f.name}`, onClick: () => onDelete(f), children: ACTIONS.MEDIA["delete-media"].label })] })] })] }, f.name));
101
+ }), filtered.length === 0 && !isLoading && (_jsx(Text, { paddingY: "lg", className: "col-span-full", color: "muted", size: "sm", align: "center", children: "No files found." }))] }), deleteTarget && (_jsx(ConfirmDeleteModal, { isOpen: true,
102
+ /* Rule #7: the copy lives on the ActionDef, not here. */
103
+ title: ACTIONS.MEDIA["delete-media"].confirmation?.title, message: `${deleteTarget.name}\n\n${ACTIONS.MEDIA["delete-media"].confirmation?.body ?? ""}`, confirmText: ACTIONS.MEDIA["delete-media"].confirmation?.confirmLabel, isDeleting: isDeleting, onConfirm: () => void confirmDelete(), onClose: () => setDeleteTarget(null) })), _jsxs(Row, { align: "center", justify: "between", children: [_jsxs(Text, { size: "xs", color: "muted", children: [filtered.length, " file(s)", nextPageToken ? " · more available" : ""] }), nextPageToken && (_jsx(Button, { type: "button", variant: "outline", onClick: () => void load(nextPageToken), disabled: isLoading, children: isLoading ? "Loading…" : "Load more" }))] })] }));
71
104
  }
72
105
  function MediaUploaderPanel({ onUpload, isUploadPending, isCleanupPending, heroAssetUrl, setHeroAssetUrl, galleryAssets, setGalleryAssets, stagedUrls, setHeroStagedUrls, setGalleryStagedUrls, handleAbort, copiedUrl, onCopy, onClearPreviews, onDiscardStaged, }) {
73
106
  return (_jsxs(Stack, { surface: "default", rounded: "xl", padding: "md", border: "default", gap: "5", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: "Upload & Copy URL" }), _jsx(MediaUploadField, { label: "Primary media asset", value: heroAssetUrl, onChange: setHeroAssetUrl, onUpload: onUpload, helperText: "Uploads via signed URL (sign \u2192 PUT \u2192 finalize) and previews the returned URL.", onAbort: handleAbort, onStagedUrlsChange: setHeroStagedUrls }), heroAssetUrl && (_jsxs(Row, { surface: "muted", padding: "inlineSm", align: "center", gap: "sm", rounded: "lg", border: "default", children: [_jsx(Text, { className: "flex-1 truncate font-mono", color: "muted", size: "xs", children: heroAssetUrl }), _jsx(Button, { type: "button", variant: "outline", onClick: () => onCopy(heroAssetUrl), children: copiedUrl === heroAssetUrl ? "Copied!" : ACTIONS.MEDIA["copy-url"].label })] })), _jsx(MediaUploadList, { label: "Gallery assets", value: galleryAssets, onChange: setGalleryAssets, onUpload: onUpload, helperText: "Attach multiple images or videos for batch checks.", onAbort: handleAbort, onStagedUrlsChange: setGalleryStagedUrls, maxItems: 12 }), galleryAssets.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", weight: "medium", color: "muted", children: "Gallery URLs" }), galleryAssets.map((asset, i) => (_jsxs(Row, { paddingY: "y-2xs", padding: "x-xs", surface: "muted", rounded: "default", align: "center", gap: "sm", border: "default", children: [_jsx(Text, { className: "flex-1 truncate font-mono", color: "muted", size: "xs", children: asset.url }), _jsx(Button, { type: "button", variant: "ghost", onClick: () => onCopy(asset.url), children: copiedUrl === asset.url ? "Copied!" : "Copy" })] }, i)))] })), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClearPreviews, disabled: isUploadPending || isCleanupPending, children: ACTIONS.MEDIA["clear-previews"].label }), _jsx(Button, { type: "button", variant: "outline", onClick: onDiscardStaged, disabled: isUploadPending || isCleanupPending || stagedUrls.length === 0, children: isCleanupPending ? "Discarding…" : ACTIONS.MEDIA["discard-staged"].label }), _jsxs(Text, { size: "xs", color: "muted", children: [stagedUrls.length, " staged upload(s)"] })] })] }));
@@ -8,11 +8,14 @@ import { apiClient } from "../../../http/index.js";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
9
9
  import { AdminNavEditorView } from "./AdminNavEditorView.js";
10
10
  import { ROW_ACTION_META, ROW_ACTION_ID } from "../../../features/products/constants/action-defs.js";
11
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
11
12
  const __O = {
12
13
  hidden: "overflow-hidden",
13
14
  };
14
15
  function NavItemRow({ item, idx, total, reorderPending, onMoveUp, onMoveDown, onVisibilityChange, onEdit, onDelete, }) {
15
- return (_jsxs(Row, { surface: "default", padding: "inline", align: "center", gap: "3", children: [_jsxs(Stack, { className: "shrink-0 gap-[0.125rem]", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: () => onMoveUp(idx), disabled: idx === 0 || reorderPending, className: "text-zinc-400 hover:text-[var(--appkit-color-text)] disabled:opacity-30 leading-none text-[length:var(--appkit-text-xs)] p-[var(--appkit-space-0)] min-h-0 h-auto rounded-none", "aria-label": "Move up", children: "\u25B2" }), _jsx(Button, { variant: "ghost", type: "button", onClick: () => onMoveDown(idx), disabled: idx >= total - 1 || reorderPending, className: "text-zinc-400 hover:text-[var(--appkit-color-text)] disabled:opacity-30 leading-none text-[length:var(--appkit-text-xs)] p-[var(--appkit-space-0)] min-h-0 h-auto rounded-none", "aria-label": "Move down", children: "\u25BC" })] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Text, { className: "truncate", color: "primary", size: "sm", weight: "medium", children: [item.parentId ? _jsx(Span, { className: "mr-1", color: "faint", children: "\u21B3" }) : null, item.label] }), _jsx(Text, { className: "truncate", color: "muted", size: "xs", children: item.href })] }), _jsx(Toggle, { checked: item.isVisible ?? true, onChange: (val) => onVisibilityChange(item.id, val), label: "" }), _jsx(RowActionMenu, { actions: [
16
+ return (_jsxs(Row, { surface: "default", padding: "inline", align: "center", gap: "3", children: [_jsxs(Stack, { className: "shrink-0 gap-[0.125rem]", children: [_jsx(Button, { variant: "ghost", type: "button", onClick: () => onMoveUp(idx), disabled: idx === 0 || reorderPending, className: "text-zinc-400 hover:text-[var(--appkit-color-text)] disabled:opacity-30 leading-none text-[length:var(--appkit-text-xs)] p-[var(--appkit-space-0)] min-h-0 h-auto rounded-none", "aria-label": "Move up", children: "\u25B2" }), _jsx(Button, { variant: "ghost", type: "button", onClick: () => onMoveDown(idx), disabled: idx >= total - 1 || reorderPending, className: "text-zinc-400 hover:text-[var(--appkit-color-text)] disabled:opacity-30 leading-none text-[length:var(--appkit-text-xs)] p-[var(--appkit-space-0)] min-h-0 h-auto rounded-none", "aria-label": "Move down", children: "\u25BC" })] }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Text, { className: "truncate", color: "primary", size: "sm", weight: "medium", children: [item.parentId ? _jsx(Span, { className: "mr-1", color: "faint", children: "\u21B3" }) : null, item.label] }), _jsx(Text, { className: "truncate", color: "muted", size: "xs", children: item.href })] }), _jsx(Toggle, { checked: item.isVisible ?? true, onChange: (val) => onVisibilityChange(item.id, val), ariaLabel: (item.isVisible ?? true)
17
+ ? `${ACTIONS.ADMIN["hide-nav-item"].ariaLabel}: ${item.label}`
18
+ : `${ACTIONS.ADMIN["show-nav-item"].ariaLabel}: ${item.label}` }), _jsx(RowActionMenu, { actions: [
16
19
  { label: ROW_ACTION_META[ROW_ACTION_ID.EDIT].label, onClick: () => onEdit(item) },
17
20
  { label: ROW_ACTION_META[ROW_ACTION_ID.DELETE].label, destructive: true, onClick: () => onDelete(item) },
18
21
  ] })] }));
@@ -47,6 +47,7 @@ import { toRecordArray, toRelativeDate, toCurrency, toStringValue, } from "../ho
47
47
  import { DataListingView } from "./DataListingView.js";
48
48
  import { apiClient } from "../../../http/index.js";
49
49
  import { normalizeError } from "../../../errors/normalize.js";
50
+ import { toUserMessage } from "../../../errors/error-display-map.js";
50
51
  /** Terminal states — an offer here has nothing left to cancel. */
51
52
  const TERMINAL_STATUSES = new Set(["paid", "declined", "expired", "withdrawn"]);
52
53
  export function AdminOffersView({ children, ...props }) {
@@ -79,7 +80,7 @@ export function AdminOffersView({ children, ...props }) {
79
80
  // The row's own data is already on screen, so this degrades to "no
80
81
  // chain" rather than to an empty modal. Say so instead of failing silently.
81
82
  const normalized = normalizeError(err);
82
- showToast(normalized.message || "Couldn't load the full offer history.", "error");
83
+ showToast(toUserMessage(normalized.code, undefined, { fallback: "Couldn't load the full offer history." }), "error");
83
84
  }
84
85
  };
85
86
  const cancelMutation = useApiMutation({
@@ -2,7 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { useQueryClient } from "@tanstack/react-query";
5
- import { Badge, Button, Div, Heading, Row, Span, Stack, Text, useToast } from "../../../ui/index.js";
5
+ import { Badge, Button, Div, Heading, Row, Span, Stack, Text, TextLink, useToast } from "../../../ui/index.js";
6
+ import { ROUTES } from "../../../next/routing/route-map.js";
6
7
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints.js";
7
8
  import { apiClient } from "../../../http/index.js";
8
9
  import { useApiMutation } from "@mohasinac/appkit/client";
@@ -30,6 +31,22 @@ export function AdminPaymentClustersView(_props) {
30
31
  void queryClient.invalidateQueries({ queryKey: ["admin", "payment-clusters"] });
31
32
  },
32
33
  });
34
+ /*
35
+ * 🛑 "Flag Suspicious" was ONE-WAY here too: no unflag, so a wrong call
36
+ * left a permanent mark on a real buyer with no way back from this
37
+ * screen. `clear_ban` is already in the route's action enum and calls
38
+ * `clearBanById`; only the control was missing.
39
+ */
40
+ const clearFlagMutation = useApiMutation({
41
+ errorMessage: "Failed to clear that flag.",
42
+ mutationFn: async (id) => {
43
+ await apiClient.patch(ADMIN_ENDPOINTS.PAYMENT_METHOD_BY_ID(id), { action: "clear_ban" });
44
+ },
45
+ onSuccess: () => {
46
+ showToast("Flag cleared.", "success");
47
+ void queryClient.invalidateQueries({ queryKey: ["admin", "payment-clusters"] });
48
+ },
49
+ });
33
50
  const clusters = data?.clusters ?? [];
34
- return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Payment Method Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Multiple accounts sharing the same payment identifier. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load payment clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared payment identifiers found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Identifier Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.identifierHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.methods.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.methods.map((m) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "primary", className: "truncate", children: [m.type.toUpperCase(), " \u00B7 ", m.displayLabel] }), _jsxs(Text, { size: "xs", color: "muted", children: ["User: ", m.userId] })] }), _jsxs(Row, { gap: "xs", align: "center", children: [m.banStatus && (_jsx(Badge, { variant: BAN_BADGE[m.banStatus] ?? "secondary", size: "sm", children: m.banStatus.replace(/_/g, " ") })), m.banStatus !== "suspicious" && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(m.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, m.id))) })] }, cluster.identifierHash)))] }));
51
+ return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Payment Method Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Multiple accounts sharing the same payment identifier. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load payment clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared payment identifiers found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Identifier Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.identifierHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.methods.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.methods.map((m) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "primary", className: "truncate", children: [m.type.toUpperCase(), " \u00B7 ", m.displayLabel] }), _jsxs(TextLink, { href: String(ROUTES.ADMIN.USER_DETAIL(m.userId)), size: "xs", children: ["User: ", m.userId] })] }), _jsxs(Row, { gap: "xs", align: "center", children: [m.banStatus && (_jsx(Badge, { variant: BAN_BADGE[m.banStatus] ?? "secondary", size: "sm", children: m.banStatus.replace(/_/g, " ") })), m.banStatus === "suspicious" ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => clearFlagMutation.mutate(m.id), disabled: clearFlagMutation.isPending, children: "Clear flag" })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(m.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, m.id))) })] }, cluster.identifierHash)))] }));
35
52
  }
@@ -194,7 +194,7 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
194
194
  label: "Details",
195
195
  required: true,
196
196
  fields: ["title", "price", "storeId", "categorySlug", "brandSlug"],
197
- render: () => (_jsx(Stack, { gap: "md", children: _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Classification" }), _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: "Store" }), _jsx(PaginatedSelect, { value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct,
197
+ render: () => (_jsx(Stack, { gap: "md", children: _jsxs(Card, { variant: "outlined", padding: "lg", children: [_jsx(Text, { className: __P.sectionLabel, color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Classification" }), _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: "Store" }), _jsx(PaginatedSelect, { name: "storeId", value: product.storeId ?? null, onChange: handleStoreSelect, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] }), _jsx(ProductForm, { product: product, onChange: setProduct,
198
198
  /*
199
199
  * The ONLY place the Tax & GST section is shown. LetItRip
200
200
  * invoices under its own GSTIN, so the rate is an admin
@@ -217,7 +217,7 @@ export function AdminProductEditorView({ productId, onSaved, onDeleted, embedded
217
217
  groupRequiredMemberIds: p.groupRequiredMemberIds,
218
218
  groupDiscountPercent: p.groupDiscountPercent,
219
219
  }, isAuction: modeFromProduct(p) === "auction", storeProductsEndpoint: ADMIN_ENDPOINTS.PRODUCTS, onGroupChanged: () => productQuery.refetch() }))
220
- : undefined, renderGroupJoinField: ({ label, value, onChange, disabled }) => (_jsx(GroupInlineSelect, { scope: "admin", value: value ?? "", onChange: (id) => onChange(id || undefined), disabled: disabled, label: label, placeholder: "None (standalone product)", allowCreate: false })), renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: label }), _jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: args.label }), _jsx(PaginatedSelect, { value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }) })),
220
+ : undefined, renderGroupJoinField: ({ label, value, onChange, disabled }) => (_jsx(GroupInlineSelect, { scope: "admin", value: value ?? "", onChange: (id) => onChange(id || undefined), disabled: disabled, label: label, placeholder: "None (standalone product)", allowCreate: false })), renderCategorySelector: ({ label, value, onChange, disabled }) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: label }), _jsx(PaginatedSelect, { name: "categorySlug", value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadCategoryOptions, placeholder: "Search categories\u2026", searchPlaceholder: "Type category name\u2026", noResultsText: "No categories found", ariaLabel: label, disabled: disabled, createLabel: "Category", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })), renderBrandSelector: (args) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", color: "primary", children: args.label }), _jsx(PaginatedSelect, { name: "brandSlug", value: args.multi ? null : (args.value || null), onChange: (v) => args.onValueChange(v ?? ""), loadOptions: loadBrandOptions, placeholder: "Search brands\u2026", searchPlaceholder: "Type brand name\u2026", noResultsText: "No brands found", ariaLabel: args.label, disabled: args.disabled, createLabel: "Brand", renderCreateForm: ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel })) })] })) })] })] }) })),
221
221
  },
222
222
  {
223
223
  id: "inventory",
@@ -2,4 +2,14 @@ import React from "react";
2
2
  export interface AdminSectionsViewProps {
3
3
  children?: React.ReactNode;
4
4
  }
5
+ /**
6
+ * The homepage/surface sections listing.
7
+ *
8
+ * Was a bare `<DataTable rows>` with no `columns`, no `renderRowActions`, no
9
+ * `onRowClick` and no `rowHrefTemplate` — so `isInteractive` was false and
10
+ * every row was inert, including the blank avatar circles that come from
11
+ * `buildDefaultColumns(undefined)`. The only way to edit a section was a
12
+ * toolbar button, a "Mode" dropdown, and a second dropdown to re-find the row
13
+ * you had just clicked past.
14
+ */
5
15
  export declare function AdminSectionsView({ children }: AdminSectionsViewProps): React.JSX.Element;