@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.
- 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/jobs/core/paymentReconcile.js +87 -97
- package/dist/_internal/shared/actions/action-registry.js +149 -9
- package/dist/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +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/errors/error-mapping.js +26 -1
- 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/AdminFulfillmentView.js +3 -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/ReferenceRecordEditor.js +1 -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/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -0
- package/dist/features/auth/components/RegisterForm.js +2 -1
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +3 -3
- package/dist/features/events/components/AdminEventEditorView.js +8 -6
- package/dist/features/layout/hooks/useFormBottomActions.js +20 -1
- package/dist/features/orders/components/EmiInstallmentsPanel.js +25 -7
- package/dist/features/orders/components/RefundRequestView.js +4 -4
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/products/components/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- 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/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- package/dist/features/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/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/next/routing/route-map.d.ts +4 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/schemas/sieve.d.ts +3 -3
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- 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 +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/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
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
4
4
|
import React, { useState, useCallback } from "react";
|
|
5
|
+
import { useRouter } from "next/navigation.js";
|
|
5
6
|
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, StickyToolbar } from "../../../ui/index.js";
|
|
7
|
+
import { Badge, BulkActionBar, Button, ConfirmDeleteModal, Div, ListingFilterDrawer, ListingLayout, ListingToolbar, Pagination, Row, Span, Stack, Text, TextLink, StickyToolbar, Toggle, useToast } from "../../../ui/index.js";
|
|
8
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
9
|
import { useBottomActions } from "../../layout/index.js";
|
|
8
10
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
11
|
import { ADMIN_ENDPOINTS, HOMEPAGE_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
@@ -14,6 +16,8 @@ import { useAdminListing } from "../hooks/useAdminListing.js";
|
|
|
14
16
|
import { DataTable } from "./DataTable.js";
|
|
15
17
|
import { RowActionMenu } from "../../../ui/components/RowActionMenu.js";
|
|
16
18
|
import { apiClient } from "../../../http/index.js";
|
|
19
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
20
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
17
21
|
const PAGE_SIZE = 50;
|
|
18
22
|
const FILTER_KEYS = ["active"];
|
|
19
23
|
const DEFAULT_SORT = "order";
|
|
@@ -33,6 +37,8 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
33
37
|
const [deleteTargetId, setDeleteTargetId] = useState(null);
|
|
34
38
|
const [localRows, setLocalRows] = useState([]);
|
|
35
39
|
const queryClient = useQueryClient();
|
|
40
|
+
const router = useRouter();
|
|
41
|
+
const { showToast } = useToast();
|
|
36
42
|
const { view, setView, table, searchInput, setSearchInput, commitSearch, filterOpen, setFilterOpen, openFilters, applyFilters, clearFilters, pendingFilters, setPendingFilters, activeFilterCount, hasActiveState, resetAll, rows: fetchedRows, isLoading, errorMessage, currentPage, totalPages, selection, } = useAdminListing({
|
|
37
43
|
filterKeys: FILTER_KEYS,
|
|
38
44
|
defaultSort: DEFAULT_SORT,
|
|
@@ -111,8 +117,58 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
111
117
|
setDraggingId(null);
|
|
112
118
|
reorderMutation.mutate(next.map((r) => r.id));
|
|
113
119
|
};
|
|
120
|
+
const setActiveMutation = useApiMutation({
|
|
121
|
+
errorMessage: "Could not change that slide's status.",
|
|
122
|
+
mutationFn: ({ id, active }) =>
|
|
123
|
+
// PUT, not PATCH: this route has no PATCH, and every field on
|
|
124
|
+
// `updateSlideSchema` is optional, so a lone `{ active }` is a valid
|
|
125
|
+
// partial update.
|
|
126
|
+
apiClient.put(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id), { active }),
|
|
127
|
+
onSuccess: () => {
|
|
128
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
|
|
129
|
+
// The public hero carousel reads under its own key.
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: ["carousel"] });
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
/*
|
|
134
|
+
* 🛑 The bulk delete used to be gated entirely on an `onBulkDelete` PROP —
|
|
135
|
+
* and the page renders `<AdminCarouselView />` with no props at all. So
|
|
136
|
+
* `bulkActions` was always `[]`, the BulkActionBar never rendered, and
|
|
137
|
+
* DataTable was handed no selection props either, which meant no checkbox
|
|
138
|
+
* column existed to select with in the first place.
|
|
139
|
+
* `ACTIONS.ADMIN["delete-carousel"]` and its confirmation dialog were
|
|
140
|
+
* unreachable from this page.
|
|
141
|
+
*
|
|
142
|
+
* The prop is kept so an embedder can still override the behaviour, but it
|
|
143
|
+
* no longer decides whether the action EXISTS — this view owns a delete
|
|
144
|
+
* mutation already and can perfectly well call it N times.
|
|
145
|
+
*/
|
|
146
|
+
const handleBulkDelete = useCallback(async () => {
|
|
147
|
+
const ids = selection.selectedIds;
|
|
148
|
+
try {
|
|
149
|
+
if (onBulkDelete) {
|
|
150
|
+
await onBulkDelete(ids);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await Promise.all(ids.map((id) => apiClient.delete(ADMIN_ENDPOINTS.CAROUSEL_BY_ID(id))));
|
|
154
|
+
await queryClient.invalidateQueries({ queryKey: ["admin", "carousel", "listing"] });
|
|
155
|
+
}
|
|
156
|
+
selection.clearSelection();
|
|
157
|
+
showToast(`${ids.length} slide${ids.length === 1 ? "" : "s"} deleted.`, "success");
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const normalized = normalizeError(err);
|
|
161
|
+
/*
|
|
162
|
+
* The selection is deliberately NOT cleared on failure. `Promise.all`
|
|
163
|
+
* rejects on the first error, so some of these ids may be gone and
|
|
164
|
+
* some may not — clearing would hide which rows are still there and
|
|
165
|
+
* leave the admin no way to retry the remainder.
|
|
166
|
+
*/
|
|
167
|
+
showToast(toUserMessage(normalized.code, undefined, { fallback: "Could not delete the selected slides." }), "error");
|
|
168
|
+
}
|
|
169
|
+
}, [onBulkDelete, selection, queryClient, showToast]);
|
|
114
170
|
const bulkActions = [
|
|
115
|
-
|
|
171
|
+
buildBulkAction(ACTIONS.ADMIN["delete-carousel"], handleBulkDelete),
|
|
116
172
|
];
|
|
117
173
|
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: bulkActions } } : {});
|
|
118
174
|
if (hasChildren) {
|
|
@@ -138,6 +194,24 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
138
194
|
header: "Details",
|
|
139
195
|
render: (row) => row.secondary,
|
|
140
196
|
},
|
|
197
|
+
{
|
|
198
|
+
/*
|
|
199
|
+
* 🛑 Both preventDefault AND stopPropagation.
|
|
200
|
+
*
|
|
201
|
+
* `rowHrefTemplate` makes the whole row an anchor, so stopping
|
|
202
|
+
* propagation alone leaves the anchor's DEFAULT action intact — the
|
|
203
|
+
* toggle would flip and then navigate to the edit page, which reads as
|
|
204
|
+
* "clicking the switch opens the editor". A normal column cell is also
|
|
205
|
+
* outside the two `<Td>`s DataTable stops propagation on; only the
|
|
206
|
+
* selection and row-action cells get that for free.
|
|
207
|
+
*/
|
|
208
|
+
key: "active",
|
|
209
|
+
header: "Active",
|
|
210
|
+
className: "w-24",
|
|
211
|
+
render: (row) => (_jsx(Row, { align: "center", gap: "sm", role: "presentation", onClick: (e) => { e.preventDefault(); e.stopPropagation(); }, onKeyDown: (e) => e.stopPropagation(), children: _jsx(Toggle, { size: "sm", checked: row.status === "Active", ariaLabel: row.status === "Active"
|
|
212
|
+
? `${ACTIONS.ADMIN["deactivate-carousel"].ariaLabel}: ${row.primary}`
|
|
213
|
+
: `${ACTIONS.ADMIN["activate-carousel"].ariaLabel}: ${row.primary}`, onChange: (next) => setActiveMutation.mutate({ id: row.id, active: next }) }) })),
|
|
214
|
+
},
|
|
141
215
|
{
|
|
142
216
|
key: "status",
|
|
143
217
|
header: "Status",
|
|
@@ -152,7 +226,10 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
152
226
|
key: "actions",
|
|
153
227
|
header: "",
|
|
154
228
|
render: (row) => (_jsx(RowActionMenu, { actions: [
|
|
155
|
-
|
|
229
|
+
// router.push, not window.location.href — a full page load to
|
|
230
|
+
// reach a page the row already links to is strictly worse, and
|
|
231
|
+
// this is the same navigation DataTable's row anchor performs.
|
|
232
|
+
{ label: ACTIONS.ADMIN["edit-carousel"].label, onClick: () => router.push(String(ROUTES.ADMIN.CAROUSEL_EDIT(row.id))) },
|
|
156
233
|
{
|
|
157
234
|
label: ACTIONS.ADMIN["delete-carousel"].label,
|
|
158
235
|
destructive: true,
|
|
@@ -163,5 +240,20 @@ export function AdminCarouselView({ children, onBulkDelete, ...props }) {
|
|
|
163
240
|
];
|
|
164
241
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput,
|
|
165
242
|
// Search intentionally absent: this endpoint does not read `q`, so the box accepted typing and changed nothing. Restore it when the collection gains searchTxt — audit-listing-search-capability tracks it.
|
|
166
|
-
onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState
|
|
243
|
+
onSearchChange: setSearchInput, onSearchCommit: commitSearch, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState,
|
|
244
|
+
/*
|
|
245
|
+
* 🛑 The empty state named a button that did not exist: "No carousel
|
|
246
|
+
* slides — use 'New slide' to add one", with no New slide control
|
|
247
|
+
* anywhere on the page. `/admin/carousel/new` has always been a real
|
|
248
|
+
* route and nothing linked to it.
|
|
249
|
+
*/
|
|
250
|
+
extra: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }) }), totalPages > 1 && (_jsx(StickyToolbar, { offset: "header+pagination", tone: "translucent", border: true, padding: "toolbar", children: _jsx(Row, { justify: "center", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) }) })), selection.selectedCount > 0 && bulkActions.length > 0 && (_jsx(BulkActionBar, { selectedCount: selection.selectedCount, actions: bulkActions, onClearSelection: selection.clearSelection })), _jsxs(Div, { paddingX: "x-sm-md", padding: "y-md", children: [errorMessage && (_jsx(Div, { textSize: "sm", className: "mb-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "xl", children: errorMessage })), _jsx(DataTable, { rows: displayRows, columns: columns, isLoading: isLoading, emptyLabel: "No carousel slides yet", emptyAction: _jsx(Button, { gap: "sm", size: "sm", asChild: true, children: _jsx(TextLink, { variant: "inherit", href: String(ROUTES.ADMIN.CAROUSEL_NEW), children: "New slide" }) }), rowHrefTemplate: "/admin/carousel/{id}/edit",
|
|
251
|
+
/*
|
|
252
|
+
* Selection was destructured and a bulk-action array built from it,
|
|
253
|
+
* and these three props were never passed — so no checkbox column
|
|
254
|
+
* rendered and nothing could be selected to act on.
|
|
255
|
+
*/
|
|
256
|
+
selectedIds: selection.selectedIdSet, onToggleSelect: (id) => selection.toggle(id), onToggleSelectAll: () => selection.toggleAll() })] }), _jsx(ListingFilterDrawer, { open: filterOpen, onClose: () => setFilterOpen(false), onApply: applyFilters, onClear: clearFilters, activeCount: activeFilterCount, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { className: "tracking-widest", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "Status" }), _jsx(Row, { wrap: true, gap: "sm", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx(Button, { variant: "ghost", type: "button", onClick: () => setPendingFilters((p) => ({ ...p, active: opt.value })), className: `rounded-full px-[var(--appkit-space-3)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] font-medium border transition-colors min-h-0 h-auto ${(pendingFilters.active || "") === opt.value ? "bg-primary text-white border-primary" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover"}`, children: opt.label }, opt.label))) })] }) }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true,
|
|
257
|
+
/* Rule #7: confirmation copy lives on the ActionDef, not here. */
|
|
258
|
+
title: ACTIONS.ADMIN["delete-carousel"].confirmation?.title, message: ACTIONS.ADMIN["delete-carousel"].confirmation?.body, confirmText: ACTIONS.ADMIN["delete-carousel"].confirmation?.confirmLabel, onConfirm: () => { deleteMutation.mutate(deleteTargetId); setDeleteTargetId(null); }, onClose: () => setDeleteTargetId(null), isDeleting: deleteMutation.isPending }))] }));
|
|
167
259
|
}
|
|
@@ -9,7 +9,8 @@ import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
|
9
9
|
import { apiClient } from "../../../http/index.js";
|
|
10
10
|
import { ADMIN_ENDPOINTS, SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
11
11
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
12
|
-
import { useToast } from "../../../ui/index.js";
|
|
12
|
+
import { useToast, TextLink } from "../../../ui/index.js";
|
|
13
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
13
14
|
async function loadStoreOptions(query, page) {
|
|
14
15
|
const params = new URLSearchParams({ page: String(page), pageSize: "25" });
|
|
15
16
|
if (query)
|
|
@@ -105,5 +106,5 @@ export function AdminFulfillmentView({ initialStoreId, initialStoreName, }) {
|
|
|
105
106
|
setStoreId(null);
|
|
106
107
|
setOrders([]);
|
|
107
108
|
}, children: "Change Store" }))] }), !storeId && (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", color: "muted", children: "Select a store to view its fulfilment queue." }), _jsx(PaginatedSelect, { value: null, onChange: (v) => { if (v)
|
|
108
|
-
setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(
|
|
109
|
+
setStoreId(v); }, loadOptions: loadStoreOptions, placeholder: "Search stores\u2026", searchPlaceholder: "Type store name\u2026", noResultsText: "No stores found", ariaLabel: "Store" })] })), storeId && (_jsxs(Stack, { gap: "sm", children: [isLoading && (_jsx(Text, { size: "sm", color: "muted", children: "Loading orders\u2026" })), !isLoading && orders.length === 0 && (_jsx(Text, { color: "muted", children: "No orders in the fulfillment queue \u2014 great work!" })), orders.map((order) => (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "md", gap: "xs", children: [_jsxs(Row, { align: "center", gap: "xs", wrap: true, children: [_jsxs(TextLink, { href: String(ROUTES.ADMIN.ORDER_DETAIL(order.id)), size: "sm", weight: "medium", children: ["#", order.id] }), _jsx(Badge, { variant: statusBadgeVariant(order.status), children: order.status }), order.pickedAt && _jsx(Badge, { variant: "success", children: "Picked" }), order.packedAt && _jsx(Badge, { variant: "success", children: "Packed" })] }), _jsxs(Text, { size: "xs", color: "muted", children: [order.userName, order.items && order.items.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 ", order.items.map((i) => i.productTitle).join(", ")] }))] }), order.assignedWorkerId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Assigned to: ", order.assignedWorkerId] })), _jsxs(Row, { gap: "xs", children: [!order.pickedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPicked(order.id), isLoading: actionLoadingId === order.id + ":picked", children: "Mark Picked" })), order.pickedAt && !order.packedAt && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleMarkPacked(order.id), isLoading: actionLoadingId === order.id + ":packed", children: "Mark Packed" })), order.assignedWorkerId && (_jsx(Button, { size: "sm", variant: "secondary", onClick: () => handleUnassign(order.id), isLoading: actionLoadingId === order.id + ":unassign", children: "Unassign" }))] })] }, order.id)))] })), initialStoreName && !storeId && (_jsxs(Text, { size: "xs", color: "muted", children: ["Previously viewing: ", initialStoreName] }))] }));
|
|
109
110
|
}
|
|
@@ -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: "
|
|
70
|
-
}), filtered.length === 0 && !isLoading && (_jsx(Text, { paddingY: "lg", className: "col-span-full", color: "muted", size: "sm", align: "center", children: "No files found." }))] }),
|
|
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),
|
|
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.
|
|
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(
|
|
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;
|