@mohasinac/appkit 2.6.5 → 2.6.7

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 (64) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +8 -1
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +18 -2
  3. package/dist/_internal/server/features/auth/capabilities.d.ts +17 -0
  4. package/dist/_internal/server/features/auth/capabilities.js +31 -0
  5. package/dist/_internal/server/features/auth/index.d.ts +2 -0
  6. package/dist/_internal/server/features/auth/index.js +2 -0
  7. package/dist/_internal/server/features/auth/permissions.d.ts +49 -0
  8. package/dist/_internal/server/features/auth/permissions.js +76 -0
  9. package/dist/client.d.ts +2 -2
  10. package/dist/client.js +1 -1
  11. package/dist/configs/next.d.ts +6 -1
  12. package/dist/configs/next.js +45 -18
  13. package/dist/constants/api-endpoints.d.ts +69 -0
  14. package/dist/constants/api-endpoints.js +34 -0
  15. package/dist/contracts/registry.d.ts +7 -0
  16. package/dist/features/account/components/UserSupportView.d.ts +3 -0
  17. package/dist/features/account/components/UserSupportView.js +90 -0
  18. package/dist/features/account/components/index.d.ts +2 -0
  19. package/dist/features/account/components/index.js +1 -0
  20. package/dist/features/admin/components/AdminEmployeeEditorView.d.ts +10 -0
  21. package/dist/features/admin/components/AdminEmployeeEditorView.js +168 -0
  22. package/dist/features/admin/components/AdminSidebar.d.ts +2 -0
  23. package/dist/features/admin/components/AdminStoreEditorView.d.ts +2 -1
  24. package/dist/features/admin/components/AdminStoreEditorView.js +55 -3
  25. package/dist/features/admin/components/AdminStoresView.js +3 -1
  26. package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +23 -0
  27. package/dist/features/admin/components/AdminSupportTicketDetailView.js +83 -0
  28. package/dist/features/admin/components/AdminSupportTicketsView.d.ts +4 -0
  29. package/dist/features/admin/components/AdminSupportTicketsView.js +151 -0
  30. package/dist/features/admin/components/AdminTeamView.d.ts +4 -0
  31. package/dist/features/admin/components/AdminTeamView.js +139 -0
  32. package/dist/features/admin/components/AdminUserEditorView.d.ts +14 -1
  33. package/dist/features/admin/components/AdminUserEditorView.js +116 -14
  34. package/dist/features/admin/components/AdminUsersView.js +39 -12
  35. package/dist/features/admin/components/index.d.ts +8 -0
  36. package/dist/features/admin/components/index.js +4 -0
  37. package/dist/features/admin/constants/filter-tabs.d.ts +37 -0
  38. package/dist/features/admin/constants/filter-tabs.js +17 -0
  39. package/dist/features/auth/server/checkSoftBan.d.ts +15 -0
  40. package/dist/features/auth/server/checkSoftBan.js +36 -0
  41. package/dist/features/auth/server.d.ts +1 -0
  42. package/dist/features/auth/server.js +1 -0
  43. package/dist/features/media/types/index.js +2 -1
  44. package/dist/features/stores/repository/store.repository.d.ts +5 -1
  45. package/dist/features/stores/repository/store.repository.js +27 -2
  46. package/dist/features/support/repository/support.repository.d.ts +18 -0
  47. package/dist/features/support/repository/support.repository.js +117 -0
  48. package/dist/index.d.ts +12 -0
  49. package/dist/index.js +12 -0
  50. package/dist/next/api/routeHandler.d.ts +8 -0
  51. package/dist/next/api/routeHandler.js +24 -3
  52. package/dist/next/routing/route-map.d.ts +2 -0
  53. package/dist/next/routing/route-map.js +1 -0
  54. package/dist/repositories/index.d.ts +2 -0
  55. package/dist/repositories/index.js +1 -0
  56. package/dist/security/pii-schemas.d.ts +2 -0
  57. package/dist/security/pii-schemas.js +2 -0
  58. package/dist/seed/stores-seed-data.js +8 -0
  59. package/dist/server.d.ts +6 -0
  60. package/dist/server.js +6 -0
  61. package/dist/tailwind-utilities.css +1 -1
  62. package/dist/utils/media-url.d.ts +9 -0
  63. package/dist/utils/media-url.js +29 -0
  64. package/package.json +1 -1
@@ -0,0 +1,151 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { useState, useCallback } from "react";
4
+ import { X } from "lucide-react";
5
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { FilterChipGroup, ListingToolbar, ListingViewShell, Pagination, RowActionMenu, } from "../../../ui";
7
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
+ import { ADMIN_SUPPORT_TICKET_STATUS_TABS, ADMIN_SUPPORT_TICKET_PRIORITY_TABS, } from "../constants/filter-tabs";
9
+ import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
10
+ import { DataTable } from "./DataTable";
11
+ import { AdminSupportTicketDetailView } from "./AdminSupportTicketDetailView";
12
+ const PAGE_SIZE = 25;
13
+ const FILTER_KEYS = ["status", "priority"];
14
+ const DEFAULT_SORT = "-createdAt";
15
+ const SORT_OPTIONS = [
16
+ { value: "-createdAt", label: "Newest" },
17
+ { value: "createdAt", label: "Oldest" },
18
+ { value: "-updatedAt", label: "Recently updated" },
19
+ ];
20
+ const PRIORITY_BADGE = {
21
+ urgent: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
22
+ high: "bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300",
23
+ normal: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
24
+ low: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
25
+ };
26
+ const STATUS_BADGE = {
27
+ open: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300",
28
+ in_progress: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300",
29
+ waiting_on_user: "bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300",
30
+ resolved: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300",
31
+ closed: "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400",
32
+ };
33
+ export function AdminSupportTicketsView({ children, ...props }) {
34
+ const hasChildren = React.Children.count(children) > 0;
35
+ const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
36
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
37
+ const [filterOpen, setFilterOpen] = useState(false);
38
+ const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
39
+ const [drawerOpen, setDrawerOpen] = useState(false);
40
+ const [selectedRow, setSelectedRow] = useState(null);
41
+ const openFilters = useCallback(() => {
42
+ setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
43
+ setFilterOpen(true);
44
+ }, [table]);
45
+ const applyFilters = useCallback(() => {
46
+ const updates = { page: "1" };
47
+ for (const k of FILTER_KEYS)
48
+ updates[k] = pendingFilters[k] ?? "";
49
+ table.setMany(updates);
50
+ setFilterOpen(false);
51
+ }, [pendingFilters, table]);
52
+ const clearFilters = useCallback(() => {
53
+ setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
54
+ }, []);
55
+ const resetAll = useCallback(() => {
56
+ const updates = { q: "", sort: "" };
57
+ for (const k of FILTER_KEYS)
58
+ updates[k] = "";
59
+ table.setMany(updates);
60
+ setSearchInput("");
61
+ }, [table]);
62
+ const commitSearch = useCallback(() => {
63
+ table.set("q", searchInput.trim());
64
+ }, [searchInput, table]);
65
+ const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
66
+ const hasActiveState = !!table.get("q") || table.get("sort") !== DEFAULT_SORT || activeFilterCount > 0;
67
+ const filterParts = [];
68
+ const statusRaw = table.get("status");
69
+ if (statusRaw && statusRaw !== "All")
70
+ filterParts.push(`status==${statusRaw}`);
71
+ const priorityRaw = table.get("priority");
72
+ if (priorityRaw && priorityRaw !== "All")
73
+ filterParts.push(`priority==${priorityRaw}`);
74
+ const filters = filterParts.join(",") || undefined;
75
+ const { rows, total, isLoading, errorMessage } = useAdminListingData({
76
+ queryKey: ["admin", "support-tickets", "listing"],
77
+ endpoint: ADMIN_ENDPOINTS.SUPPORT_TICKETS,
78
+ page: table.getNumber("page", 1),
79
+ pageSize: PAGE_SIZE,
80
+ sorts: table.get("sort") || DEFAULT_SORT,
81
+ filters,
82
+ q: table.get("q") || undefined,
83
+ mapRows: (response) => toRecordArray(response.tickets).map((item, index) => ({
84
+ id: toStringValue(item.id, `ticket-${index}`),
85
+ primary: toStringValue(item.subject, "No subject"),
86
+ secondary: [
87
+ toStringValue(item.userDisplayName, "Unknown user"),
88
+ toStringValue(item.category, "general"),
89
+ item.orderId ? `Order: ${toStringValue(item.orderId, "")}` : null,
90
+ ]
91
+ .filter(Boolean)
92
+ .join(" · "),
93
+ status: toStringValue(item.status, "open"),
94
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
95
+ _raw: item,
96
+ })),
97
+ getTotal: (response, mappedRows) => {
98
+ if (typeof response.meta?.filteredTotal === "number")
99
+ return response.meta.filteredTotal;
100
+ if (typeof response.meta?.total === "number")
101
+ return response.meta.total;
102
+ if (typeof response.total === "number")
103
+ return response.total;
104
+ return mappedRows.length;
105
+ },
106
+ });
107
+ const currentPage = table.getNumber("page", 1);
108
+ const totalPages = Math.ceil(total / PAGE_SIZE);
109
+ if (hasChildren) {
110
+ return (_jsx(ListingViewShell, { portal: "admin", ...props, children: children }));
111
+ }
112
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by subject", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => {
113
+ table.set("sort", v);
114
+ table.setPage(1);
115
+ }, hideViewToggle: true, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No support tickets found", renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
116
+ {
117
+ label: "View",
118
+ onClick: () => {
119
+ setSelectedRow(row);
120
+ setDrawerOpen(true);
121
+ },
122
+ },
123
+ ] })), columns: [
124
+ {
125
+ key: "primary",
126
+ header: "Subject",
127
+ render: (row) => {
128
+ const r = row;
129
+ const priority = toStringValue(r._raw?.priority, "normal");
130
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "font-medium text-zinc-900 dark:text-zinc-100", children: r.primary }), r.secondary ? (_jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: r.secondary })) : null, _jsx("span", { className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${PRIORITY_BADGE[priority] ?? PRIORITY_BADGE.normal}`, children: priority })] }));
131
+ },
132
+ },
133
+ {
134
+ key: "status",
135
+ header: "Status",
136
+ className: "w-36",
137
+ render: (row) => {
138
+ const r = row;
139
+ return (_jsx("span", { className: `inline-flex rounded-full px-2.5 py-1 text-xs font-medium ${STATUS_BADGE[r.status] ?? STATUS_BADGE.open}`, children: r.status.replace(/_/g, " ") }));
140
+ },
141
+ },
142
+ {
143
+ key: "updatedAt",
144
+ header: "Updated",
145
+ className: "w-32",
146
+ render: (row) => (_jsx("span", { className: "text-sm text-zinc-500 dark:text-zinc-400", children: row.updatedAt })),
147
+ },
148
+ ] })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_SUPPORT_TICKET_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Priority", tabs: ADMIN_SUPPORT_TICKET_PRIORITY_TABS, value: pendingFilters.priority ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, priority: id })) })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }), _jsx(AdminSupportTicketDetailView, { open: drawerOpen, onClose: () => setDrawerOpen(false), ticketId: selectedRow?.id, subject: selectedRow?.primary, userDisplayName: toStringValue(selectedRow?._raw?.userDisplayName, ""), category: toStringValue(selectedRow?._raw?.category, "general"), currentStatus: selectedRow?.status, currentPriority: toStringValue(selectedRow?._raw?.priority, "normal"), description: toStringValue(selectedRow?._raw?.description, ""), messages: Array.isArray(selectedRow?._raw?.messages)
149
+ ? selectedRow._raw.messages
150
+ : [], internalNotes: toStringValue(selectedRow?._raw?.internalNotes, "") || undefined, orderId: toStringValue(selectedRow?._raw?.orderId, "") || undefined })] }));
151
+ }
@@ -0,0 +1,4 @@
1
+ import type { ListingViewShellProps } from "../../../ui";
2
+ export interface AdminTeamViewProps extends ListingViewShellProps {
3
+ }
4
+ export declare function AdminTeamView({ children, ...props }: AdminTeamViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,139 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import React, { useState, useCallback } from "react";
4
+ import { X, UserPlus } from "lucide-react";
5
+ import { useUrlTable } from "../../../react/hooks/useUrlTable";
6
+ import { Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, } from "../../../ui";
7
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
8
+ import { ALL_TAB } from "../constants/filter-tabs";
9
+ import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
10
+ import { DataTable } from "./DataTable";
11
+ import { AdminEmployeeEditorView } from "./AdminEmployeeEditorView";
12
+ const PAGE_SIZE = 25;
13
+ const FILTER_KEY = "group";
14
+ const DEFAULT_SORT = "-createdAt";
15
+ const SORT_OPTIONS = [
16
+ { value: "-createdAt", label: "Newest" },
17
+ { value: "createdAt", label: "Oldest" },
18
+ { value: "displayName", label: "Name A–Z" },
19
+ ];
20
+ const GROUP_TABS = [
21
+ ALL_TAB,
22
+ { id: "content_moderator", label: "Content Mod" },
23
+ { id: "review_manager", label: "Reviews" },
24
+ { id: "blog_poster", label: "Blog" },
25
+ { id: "community_manager", label: "Community" },
26
+ { id: "event_handler", label: "Events" },
27
+ { id: "newsletter_manager", label: "Newsletter" },
28
+ { id: "seo_manager", label: "SEO" },
29
+ { id: "ad_manager", label: "Ads" },
30
+ { id: "site_manager", label: "Site" },
31
+ { id: "catalog_manager", label: "Catalog" },
32
+ { id: "finance_manager", label: "Finance" },
33
+ { id: "data_analyst", label: "Analytics" },
34
+ { id: "customer_support", label: "Support" },
35
+ { id: "support_agent", label: "Agent" },
36
+ { id: "store_onboarding", label: "Onboarding" },
37
+ { id: "trust_and_safety", label: "T&S" },
38
+ { id: "auction_monitor", label: "Auctions" },
39
+ { id: "scam_moderator", label: "Scams" },
40
+ { id: "custom", label: "Custom" },
41
+ ];
42
+ function formatGroup(group) {
43
+ return group.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
44
+ }
45
+ export function AdminTeamView({ children, ...props }) {
46
+ const hasChildren = React.Children.count(children) > 0;
47
+ const table = useUrlTable({
48
+ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT },
49
+ });
50
+ const [searchInput, setSearchInput] = useState(table.get("q") || "");
51
+ const [filterOpen, setFilterOpen] = useState(false);
52
+ const [pendingGroup, setPendingGroup] = useState(table.get(FILTER_KEY) ?? "");
53
+ const [drawerOpen, setDrawerOpen] = useState(false);
54
+ const [selectedRow, setSelectedRow] = useState(null);
55
+ const [inviteMode, setInviteMode] = useState(false);
56
+ const openFilters = useCallback(() => {
57
+ setPendingGroup(table.get(FILTER_KEY) ?? "");
58
+ setFilterOpen(true);
59
+ }, [table]);
60
+ const applyFilters = useCallback(() => {
61
+ table.setMany({ page: "1", [FILTER_KEY]: pendingGroup });
62
+ setFilterOpen(false);
63
+ }, [pendingGroup, table]);
64
+ const clearFilters = useCallback(() => {
65
+ setPendingGroup("");
66
+ }, []);
67
+ const resetAll = useCallback(() => {
68
+ table.setMany({ q: "", sort: "", [FILTER_KEY]: "" });
69
+ setSearchInput("");
70
+ }, [table]);
71
+ const commitSearch = useCallback(() => {
72
+ table.set("q", searchInput.trim());
73
+ }, [searchInput, table]);
74
+ const openInvite = useCallback(() => {
75
+ setSelectedRow(null);
76
+ setInviteMode(true);
77
+ setDrawerOpen(true);
78
+ }, []);
79
+ const openEdit = useCallback((row) => {
80
+ setSelectedRow(row);
81
+ setInviteMode(false);
82
+ setDrawerOpen(true);
83
+ }, []);
84
+ const activeFilterCount = table.get(FILTER_KEY) ? 1 : 0;
85
+ const hasActiveState = !!table.get("q") ||
86
+ table.get("sort") !== DEFAULT_SORT ||
87
+ activeFilterCount > 0;
88
+ const groupRaw = table.get(FILTER_KEY);
89
+ const filters = groupRaw && groupRaw !== "All"
90
+ ? `permissionGroup==${groupRaw}`
91
+ : undefined;
92
+ const { rows, total, isLoading, errorMessage } = useAdminListingData({
93
+ queryKey: ["admin", "team", "listing"],
94
+ endpoint: ADMIN_ENDPOINTS.TEAM,
95
+ page: table.getNumber("page", 1),
96
+ pageSize: PAGE_SIZE,
97
+ sorts: table.get("sort") || DEFAULT_SORT,
98
+ filters,
99
+ q: table.get("q") || undefined,
100
+ mapRows: (response) => toRecordArray(response.users).map((item, index) => ({
101
+ id: toStringValue(item.id ?? item.uid, `employee-${index}`),
102
+ primary: toStringValue(item.displayName, "Unnamed employee"),
103
+ secondary: [
104
+ toStringValue(item.email, "No email"),
105
+ item.permissionGroup
106
+ ? formatGroup(toStringValue(item.permissionGroup, ""))
107
+ : "Custom",
108
+ ].join(" · "),
109
+ status: typeof item.disabled === "boolean"
110
+ ? item.disabled ? "Disabled" : "Active"
111
+ : "Active",
112
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
113
+ _raw: item,
114
+ })),
115
+ getTotal: (response, mappedRows) => {
116
+ if (typeof response.meta?.total === "number")
117
+ return response.meta.total;
118
+ if (typeof response.total === "number")
119
+ return response.total;
120
+ return mappedRows.length;
121
+ },
122
+ });
123
+ const currentPage = table.getNumber("page", 1);
124
+ const totalPages = Math.ceil(total / PAGE_SIZE);
125
+ if (hasChildren) {
126
+ return (_jsx(ListingViewShell, { portal: "admin", ...props, children: children }));
127
+ }
128
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search by name or email", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => {
129
+ table.set("sort", v);
130
+ table.setPage(1);
131
+ }, hideViewToggle: true, onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { type: "button", variant: "primary", onClick: openInvite, className: "flex items-center gap-1.5 whitespace-nowrap", children: [_jsx(UserPlus, { className: "h-4 w-4" }), "Invite Employee"] }) }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No employees found", renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
132
+ {
133
+ label: "Edit Permissions",
134
+ onClick: () => openEdit(row),
135
+ },
136
+ ] })) })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4", children: _jsx(FilterChipGroup, { label: "Permission Group", tabs: GROUP_TABS, value: pendingGroup, onChange: (id) => setPendingGroup(id) }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }), _jsx(AdminEmployeeEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), mode: inviteMode ? "invite" : "edit", userId: selectedRow?.id, displayName: selectedRow?.primary, currentPermissionGroup: toStringValue(selectedRow?._raw?.permissionGroup, "custom"), currentPermissions: Array.isArray(selectedRow?._raw?.permissions)
137
+ ? selectedRow._raw.permissions
138
+ : [] })] }));
139
+ }
@@ -1,3 +1,10 @@
1
+ interface SoftBanEntry {
2
+ action: string;
3
+ reason: string;
4
+ bannedAt: string;
5
+ expiresAt?: string | null;
6
+ bannedBy: string;
7
+ }
1
8
  export interface AdminUserEditorViewProps {
2
9
  open: boolean;
3
10
  onClose: () => void;
@@ -9,5 +16,11 @@ export interface AdminUserEditorViewProps {
9
16
  /** Store the user owns (for sellers/admins). storeId === storeSlug in this project. */
10
17
  ownedStoreId?: string;
11
18
  ownedStoreName?: string;
19
+ /** Soft bans from the user document (serialized from Firestore). */
20
+ currentSoftBans?: SoftBanEntry[];
21
+ /** Whether the user is hard-banned (isDisabled + hardBanReason set). */
22
+ currentIsHardBanned?: boolean;
23
+ currentHardBanReason?: string;
12
24
  }
13
- export declare function AdminUserEditorView({ open, onClose, userId, displayName, currentRole, currentIsDisabled, currentEmailVerified, ownedStoreId, ownedStoreName, }: AdminUserEditorViewProps): import("react/jsx-runtime").JSX.Element;
25
+ export declare function AdminUserEditorView({ open, onClose, userId, displayName, currentRole, currentIsDisabled, currentEmailVerified, ownedStoreId, ownedStoreName, currentSoftBans, currentIsHardBanned, currentHardBanReason, }: AdminUserEditorViewProps): import("react/jsx-runtime").JSX.Element;
26
+ export {};
@@ -10,37 +10,72 @@ const ROLE_OPTIONS = [
10
10
  { label: "Seller", value: "seller" },
11
11
  { label: "Admin", value: "admin" },
12
12
  ];
13
+ const BANNED_ACTION_OPTIONS = [
14
+ { label: "Write reviews", value: "write_reviews" },
15
+ { label: "Write blog comments", value: "write_blog_comments" },
16
+ { label: "Join events", value: "join_events" },
17
+ { label: "Place bids", value: "place_bids" },
18
+ { label: "Create listings", value: "create_listings" },
19
+ { label: "Send messages", value: "send_messages" },
20
+ { label: "Create support tickets", value: "create_support_tickets" },
21
+ { label: "Report scammers", value: "report_scammers" },
22
+ ];
23
+ function formatBanAction(action) {
24
+ return BANNED_ACTION_OPTIONS.find((o) => o.value === action)?.label ?? action;
25
+ }
26
+ function formatExpiry(expiresAt) {
27
+ if (!expiresAt)
28
+ return "Permanent";
29
+ const d = new Date(expiresAt);
30
+ if (isNaN(d.getTime()))
31
+ return "Permanent";
32
+ if (d < new Date())
33
+ return `Expired ${d.toLocaleDateString()}`;
34
+ return `Until ${d.toLocaleDateString()}`;
35
+ }
13
36
  // --- Component ---------------------------------------------------------------
14
- export function AdminUserEditorView({ open, onClose, userId, displayName, currentRole, currentIsDisabled, currentEmailVerified, ownedStoreId, ownedStoreName, }) {
37
+ export function AdminUserEditorView({ open, onClose, userId, displayName, currentRole, currentIsDisabled, currentEmailVerified, ownedStoreId, ownedStoreName, currentSoftBans, currentIsHardBanned, currentHardBanReason, }) {
15
38
  const queryClient = useQueryClient();
16
39
  const { showToast } = useToast();
40
+ // --- General fields -------------------------------------------------------
17
41
  const [role, setRole] = React.useState(currentRole ?? "user");
18
- const [isDisabled, setIsDisabled] = React.useState(currentIsDisabled ?? false);
19
- const [banReason, setBanReason] = React.useState("");
20
42
  const [emailVerified, setEmailVerified] = React.useState(currentEmailVerified ?? false);
21
43
  const [adminNotes, setAdminNotes] = React.useState("");
22
44
  const [deleteOpen, setDeleteOpen] = React.useState(false);
45
+ // --- Hard ban form --------------------------------------------------------
46
+ const [showHardBanForm, setShowHardBanForm] = React.useState(false);
47
+ const [hardBanReasonInput, setHardBanReasonInput] = React.useState("");
48
+ // --- Soft ban form --------------------------------------------------------
49
+ const [showAddSoftBan, setShowAddSoftBan] = React.useState(false);
50
+ const [softBanAction, setSoftBanAction] = React.useState(BANNED_ACTION_OPTIONS[0].value);
51
+ const [softBanReason, setSoftBanReason] = React.useState("");
52
+ const [softBanExpiry, setSoftBanExpiry] = React.useState("");
23
53
  React.useEffect(() => {
24
54
  if (open) {
25
55
  setRole(currentRole ?? "user");
26
- setIsDisabled(currentIsDisabled ?? false);
27
- setBanReason("");
28
56
  setEmailVerified(currentEmailVerified ?? false);
29
57
  setAdminNotes("");
58
+ setShowHardBanForm(false);
59
+ setHardBanReasonInput("");
60
+ setShowAddSoftBan(false);
61
+ setSoftBanAction(BANNED_ACTION_OPTIONS[0].value);
62
+ setSoftBanReason("");
63
+ setSoftBanExpiry("");
30
64
  }
31
- }, [open, currentRole, currentIsDisabled, currentEmailVerified]);
65
+ }, [open, currentRole, currentEmailVerified]);
66
+ const invalidate = () => queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
67
+ // --- Mutations ------------------------------------------------------------
32
68
  const saveMutation = useMutation({
33
69
  mutationFn: async () => {
34
70
  await apiClient.patch(ADMIN_ENDPOINTS.USER_BY_ID(userId), {
35
71
  role,
36
- isDisabled,
37
72
  emailVerified,
38
73
  adminNotes: adminNotes || undefined,
39
74
  });
40
75
  },
41
76
  onSuccess: () => {
42
77
  showToast("User updated.", "success");
43
- queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
78
+ invalidate();
44
79
  onClose();
45
80
  },
46
81
  onError: (err) => {
@@ -53,7 +88,7 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, curren
53
88
  },
54
89
  onSuccess: () => {
55
90
  showToast("User deleted.", "success");
56
- queryClient.invalidateQueries({ queryKey: ["admin", "users"] });
91
+ invalidate();
57
92
  setDeleteOpen(false);
58
93
  onClose();
59
94
  },
@@ -61,12 +96,79 @@ export function AdminUserEditorView({ open, onClose, userId, displayName, curren
61
96
  showToast(err?.message ?? "Failed to delete user.", "error");
62
97
  },
63
98
  });
99
+ const hardBanMutation = useMutation({
100
+ mutationFn: async (reason) => {
101
+ await apiClient.post(ADMIN_ENDPOINTS.USER_HARD_BAN(userId), { reason });
102
+ },
103
+ onSuccess: () => {
104
+ showToast("User hard-banned.", "success");
105
+ invalidate();
106
+ setShowHardBanForm(false);
107
+ setHardBanReasonInput("");
108
+ onClose();
109
+ },
110
+ onError: (err) => {
111
+ showToast(err?.message ?? "Failed to ban user.", "error");
112
+ },
113
+ });
114
+ const unbanMutation = useMutation({
115
+ mutationFn: async () => {
116
+ await apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(userId), {});
117
+ },
118
+ onSuccess: () => {
119
+ showToast("User unbanned.", "success");
120
+ invalidate();
121
+ onClose();
122
+ },
123
+ onError: (err) => {
124
+ showToast(err?.message ?? "Failed to unban user.", "error");
125
+ },
126
+ });
127
+ const softBanMutation = useMutation({
128
+ mutationFn: async (payload) => {
129
+ await apiClient.post(ADMIN_ENDPOINTS.USER_SOFT_BAN(userId), payload);
130
+ },
131
+ onSuccess: () => {
132
+ showToast("Soft ban applied.", "success");
133
+ invalidate();
134
+ setShowAddSoftBan(false);
135
+ setSoftBanAction(BANNED_ACTION_OPTIONS[0].value);
136
+ setSoftBanReason("");
137
+ setSoftBanExpiry("");
138
+ },
139
+ onError: (err) => {
140
+ showToast(err?.message ?? "Failed to apply soft ban.", "error");
141
+ },
142
+ });
143
+ const liftSoftBanMutation = useMutation({
144
+ mutationFn: async (action) => {
145
+ await apiClient.delete(ADMIN_ENDPOINTS.USER_SOFT_BAN_LIFT(userId, action));
146
+ },
147
+ onSuccess: () => {
148
+ showToast("Soft ban lifted.", "success");
149
+ invalidate();
150
+ },
151
+ onError: (err) => {
152
+ showToast(err?.message ?? "Failed to lift soft ban.", "error");
153
+ },
154
+ });
155
+ const isHardBanned = currentIsHardBanned ?? false;
156
+ const softBans = currentSoftBans ?? [];
64
157
  return (_jsxs(_Fragment, { children: [_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: displayName ? `Manage: ${displayName}` : "Manage User", children: _jsxs(Form, { onSubmit: (e) => {
65
158
  e.preventDefault();
66
159
  saveMutation.mutate();
67
- }, className: "space-y-4 p-4", children: [userId && (_jsx("div", { className: "rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs dark:border-zinc-700 dark:bg-zinc-900/40", children: _jsxs("div", { className: "flex flex-col gap-1 text-zinc-700 dark:text-zinc-300", children: [_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Owner ID (Firebase UID):" }), " ", _jsx("code", { className: "select-all font-mono", children: userId })] }), ownedStoreId && (_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Owns store:" }), " ", _jsx("code", { className: "select-all font-mono", children: ownedStoreId }), ownedStoreName ? ` — ${ownedStoreName}` : ""] }))] }) })), _jsx(Select, { label: "Role", options: ROLE_OPTIONS, value: role, onValueChange: setRole }), _jsx(Toggle, { label: "Account disabled (banned)", checked: isDisabled, onChange: (val) => {
68
- setIsDisabled(val);
69
- if (!val)
70
- setBanReason("");
71
- } }), isDisabled && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Ban reason (optional)" }), _jsx("textarea", { value: banReason, onChange: (e) => setBanReason(e.target.value), rows: 2, placeholder: "e.g. Repeated policy violations, fraudulent activity\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] })), _jsx(Toggle, { label: "Email verified", checked: emailVerified, onChange: setEmailVerified }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Admin notes (optional)" }), _jsx("textarea", { value: adminNotes, onChange: (e) => setAdminNotes(e.target.value), rows: 3, placeholder: "Internal notes about this user\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", onClick: () => setDeleteOpen(true), disabled: !userId, children: "Delete user" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !userId || saveMutation.isPending, children: "Save changes" })] })] }) }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: `Delete ${displayName ?? "user"}?`, message: "This action cannot be undone. The user's account and all associated data will be permanently removed.", confirmText: "Delete user", variant: "danger" })] }));
160
+ }, className: "space-y-4 p-4", children: [userId && (_jsx("div", { className: "rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs dark:border-zinc-700 dark:bg-zinc-900/40", children: _jsxs("div", { className: "flex flex-col gap-1 text-zinc-700 dark:text-zinc-300", children: [_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Owner ID (Firebase UID):" }), " ", _jsx("code", { className: "select-all font-mono", children: userId })] }), ownedStoreId && (_jsxs("div", { children: [_jsx("span", { className: "font-semibold", children: "Owns store:" }), " ", _jsx("code", { className: "select-all font-mono", children: ownedStoreId }), ownedStoreName ? ` — ${ownedStoreName}` : ""] }))] }) })), _jsx(Select, { label: "Role", options: ROLE_OPTIONS, value: role, onValueChange: setRole }), _jsx(Toggle, { label: "Email verified", checked: emailVerified, onChange: setEmailVerified }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Admin notes (optional)" }), _jsx("textarea", { value: adminNotes, onChange: (e) => setAdminNotes(e.target.value), rows: 3, placeholder: "Internal notes about this user\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs(FormActions, { align: "right", children: [_jsx(Button, { type: "button", variant: "danger", onClick: () => setDeleteOpen(true), disabled: !userId, children: "Delete user" }), _jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", isLoading: saveMutation.isPending, disabled: !userId || saveMutation.isPending, children: "Save changes" })] }), _jsxs("div", { className: "border-t border-zinc-200 pt-4 dark:border-zinc-700", children: [_jsx("h3", { className: "mb-3 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: "Moderation" }), _jsxs("div", { className: "mb-4 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900/40", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Hard ban" }), isHardBanned ? (_jsx("span", { className: "rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/40 dark:text-red-300", children: "Banned" })) : (_jsx("span", { className: "rounded-full bg-green-100 px-2 py-0.5 text-xs font-semibold text-green-700 dark:bg-green-900/40 dark:text-green-300", children: "Active" }))] }), isHardBanned ? (_jsxs("div", { className: "space-y-2", children: [currentHardBanReason && (_jsxs("p", { className: "text-xs text-zinc-600 dark:text-zinc-400", children: ["Reason: ", currentHardBanReason] })), _jsx(Button, { type: "button", variant: "secondary", size: "sm", isLoading: unbanMutation.isPending, disabled: unbanMutation.isPending, onClick: () => unbanMutation.mutate(), children: "Lift hard ban" })] })) : showHardBanForm ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-zinc-700 dark:text-zinc-300", children: "Ban reason (required)" }), _jsx("textarea", { value: hardBanReasonInput, onChange: (e) => setHardBanReasonInput(e.target.value), rows: 2, placeholder: "e.g. Repeated fraud, scam activity\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-red-500" })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "danger", size: "sm", isLoading: hardBanMutation.isPending, disabled: !hardBanReasonInput.trim() || hardBanMutation.isPending, onClick: () => hardBanMutation.mutate(hardBanReasonInput.trim()), children: "Confirm hard ban" }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => {
161
+ setShowHardBanForm(false);
162
+ setHardBanReasonInput("");
163
+ }, children: "Cancel" })] })] })) : (_jsx(Button, { type: "button", variant: "danger", size: "sm", disabled: !userId, onClick: () => setShowHardBanForm(true), children: "Impose hard ban" }))] }), _jsxs("div", { className: "rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-900/40", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: ["Soft bans", softBans.length > 0 ? ` (${softBans.length})` : ""] }), !showAddSoftBan && (_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: !userId, onClick: () => setShowAddSoftBan(true), children: "Add soft ban" }))] }), softBans.length > 0 && (_jsx("ul", { className: "mb-3 space-y-2", children: softBans.map((ban) => (_jsxs("li", { className: "flex items-start justify-between gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-xs dark:border-zinc-700 dark:bg-zinc-800", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-semibold text-zinc-800 dark:text-zinc-200", children: formatBanAction(ban.action) }), _jsx("div", { className: "text-zinc-500 dark:text-zinc-400", children: ban.reason }), _jsx("div", { className: "text-zinc-400 dark:text-zinc-500", children: formatExpiry(ban.expiresAt) })] }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", isLoading: liftSoftBanMutation.isPending, disabled: liftSoftBanMutation.isPending, onClick: () => liftSoftBanMutation.mutate(ban.action), children: "Lift" })] }, ban.action))) })), showAddSoftBan && (_jsxs("div", { className: "space-y-2", children: [_jsx(Select, { label: "Action to restrict", options: BANNED_ACTION_OPTIONS, value: softBanAction, onValueChange: setSoftBanAction }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-zinc-700 dark:text-zinc-300", children: "Reason (required)" }), _jsx("textarea", { value: softBanReason, onChange: (e) => setSoftBanReason(e.target.value), rows: 2, placeholder: "e.g. Suspicious bid activity\u2026", className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "text-xs font-medium text-zinc-700 dark:text-zinc-300", children: "Expires at (optional \u2014 leave blank for permanent)" }), _jsx("input", { type: "datetime-local", value: softBanExpiry, onChange: (e) => setSoftBanExpiry(e.target.value), className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-primary-500" })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", isLoading: softBanMutation.isPending, disabled: !softBanReason.trim() || softBanMutation.isPending, onClick: () => softBanMutation.mutate({
164
+ action: softBanAction,
165
+ reason: softBanReason.trim(),
166
+ ...(softBanExpiry
167
+ ? { expiresAt: new Date(softBanExpiry).toISOString() }
168
+ : {}),
169
+ }), children: "Apply soft ban" }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => {
170
+ setShowAddSoftBan(false);
171
+ setSoftBanReason("");
172
+ setSoftBanExpiry("");
173
+ }, children: "Cancel" })] })] })), softBans.length === 0 && !showAddSoftBan && (_jsx("p", { className: "text-xs text-zinc-400 dark:text-zinc-500", children: "No active soft bans." }))] })] })] }) }), _jsx(ConfirmDeleteModal, { isOpen: deleteOpen, onClose: () => setDeleteOpen(false), onConfirm: () => deleteMutation.mutate(), isDeleting: deleteMutation.isPending, title: `Delete ${displayName ?? "user"}?`, message: "This action cannot be undone. The user's account and all associated data will be permanently removed.", confirmText: "Delete user", variant: "danger" })] }));
72
174
  }
@@ -79,17 +79,29 @@ export function AdminUsersView({ children, ...props }) {
79
79
  sorts: table.get("sort") || DEFAULT_SORT,
80
80
  filters,
81
81
  q: table.get("q") || undefined,
82
- mapRows: (response) => toRecordArray(response.users).map((item, index) => ({
83
- id: toStringValue(item.id ?? item.uid, `user-${index}`),
84
- primary: toStringValue(item.displayName, "Unnamed user"),
85
- secondary: [
86
- toStringValue(item.email, "No email"),
87
- toStringValue(item.role, "Unknown role"),
88
- ].join(" · "),
89
- status: typeof item.disabled === "boolean" ? (item.disabled ? "Disabled" : "Active") : "Active",
90
- updatedAt: toRelativeDate(item.lastLoginAt ?? item.createdAt),
91
- _raw: item,
92
- })),
82
+ mapRows: (response) => toRecordArray(response.users).map((item, index) => {
83
+ const isDisabled = Boolean(item.isDisabled ?? item.disabled);
84
+ const isHardBanned = isDisabled && Boolean(item.hardBanReason);
85
+ const softBanCount = Array.isArray(item.softBans) ? item.softBans.length : 0;
86
+ let status = "Active";
87
+ if (isHardBanned)
88
+ status = "Hard banned";
89
+ else if (isDisabled)
90
+ status = "Disabled";
91
+ else if (softBanCount > 0)
92
+ status = `Soft bans (${softBanCount})`;
93
+ return {
94
+ id: toStringValue(item.id ?? item.uid, `user-${index}`),
95
+ primary: toStringValue(item.displayName, "Unnamed user"),
96
+ secondary: [
97
+ toStringValue(item.email, "No email"),
98
+ toStringValue(item.role, "Unknown role"),
99
+ ].join(" · "),
100
+ status,
101
+ updatedAt: toRelativeDate(item.lastLoginAt ?? item.createdAt),
102
+ _raw: item,
103
+ };
104
+ }),
93
105
  getTotal: (response, mappedRows) => {
94
106
  if (typeof response.meta?.total === "number")
95
107
  return response.meta.total;
@@ -106,5 +118,20 @@ export function AdminUsersView({ children, ...props }) {
106
118
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search users, email, or seller handles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); table.setPage(1); }, hideViewToggle: true, onResetAll: resetAll, hasActiveState: hasActiveState }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No users found", selectedIds: selectedIds, onToggleSelect: toggleSelect, onToggleSelectAll: (next) => setSelectedIds(next ? new Set(rows.map((r) => r.id)) : new Set()), renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [{
107
119
  label: "Manage",
108
120
  onClick: () => { setSelectedRow(row); setDrawerOpen(true); },
109
- }] })) })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Role", tabs: ROLE_OPTIONS, value: pendingFilters.role ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, role: id })) })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentIsDisabled: selectedRow?.status === "Disabled", currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined })] }));
121
+ }] })) })] }), filterOpen && (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Role", tabs: ROLE_OPTIONS, value: pendingFilters.role ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, role: id })) })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
122
+ selectedRow?._raw?.hardBanReason), currentHardBanReason: toStringValue(selectedRow?._raw?.hardBanReason, "") || undefined, currentSoftBans: Array.isArray(selectedRow?._raw?.softBans)
123
+ ? selectedRow._raw.softBans.map((b) => ({
124
+ action: toStringValue(b.action, ""),
125
+ reason: toStringValue(b.reason, ""),
126
+ bannedAt: toStringValue(b.bannedAt instanceof Date
127
+ ? b.bannedAt.toISOString()
128
+ : b.bannedAt, ""),
129
+ expiresAt: b.expiresAt
130
+ ? toStringValue(b.expiresAt instanceof Date
131
+ ? b.expiresAt.toISOString()
132
+ : b.expiresAt, "") || null
133
+ : null,
134
+ bannedBy: toStringValue(b.bannedBy, ""),
135
+ }))
136
+ : undefined })] }));
110
137
  }
@@ -133,3 +133,11 @@ export { AdminTopBar } from "./AdminTopBar";
133
133
  export type { AdminTopBarProps } from "./AdminTopBar";
134
134
  export { DemoSeedView } from "./DemoSeedView";
135
135
  export type { DemoSeedViewProps } from "./DemoSeedView";
136
+ export { AdminTeamView } from "./AdminTeamView";
137
+ export type { AdminTeamViewProps } from "./AdminTeamView";
138
+ export { AdminEmployeeEditorView } from "./AdminEmployeeEditorView";
139
+ export type { AdminEmployeeEditorViewProps } from "./AdminEmployeeEditorView";
140
+ export { AdminSupportTicketsView } from "./AdminSupportTicketsView";
141
+ export type { AdminSupportTicketsViewProps } from "./AdminSupportTicketsView";
142
+ export { AdminSupportTicketDetailView } from "./AdminSupportTicketDetailView";
143
+ export type { AdminSupportTicketDetailViewProps } from "./AdminSupportTicketDetailView";
@@ -69,3 +69,7 @@ export { AdminStoreAddressesView } from "./AdminStoreAddressesView";
69
69
  export { AdminSidebar } from "./AdminSidebar";
70
70
  export { AdminTopBar } from "./AdminTopBar";
71
71
  export { DemoSeedView } from "./DemoSeedView";
72
+ export { AdminTeamView } from "./AdminTeamView";
73
+ export { AdminEmployeeEditorView } from "./AdminEmployeeEditorView";
74
+ export { AdminSupportTicketsView } from "./AdminSupportTicketsView";
75
+ export { AdminSupportTicketDetailView } from "./AdminSupportTicketDetailView";