@mohasinac/appkit 2.6.5 → 2.6.6

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 (58) 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/configs/next.d.ts +6 -1
  10. package/dist/configs/next.js +45 -18
  11. package/dist/constants/api-endpoints.d.ts +69 -0
  12. package/dist/constants/api-endpoints.js +34 -0
  13. package/dist/contracts/registry.d.ts +7 -0
  14. package/dist/features/admin/components/AdminEmployeeEditorView.d.ts +10 -0
  15. package/dist/features/admin/components/AdminEmployeeEditorView.js +168 -0
  16. package/dist/features/admin/components/AdminSidebar.d.ts +2 -0
  17. package/dist/features/admin/components/AdminStoreEditorView.d.ts +2 -1
  18. package/dist/features/admin/components/AdminStoreEditorView.js +55 -3
  19. package/dist/features/admin/components/AdminStoresView.js +3 -1
  20. package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +23 -0
  21. package/dist/features/admin/components/AdminSupportTicketDetailView.js +83 -0
  22. package/dist/features/admin/components/AdminSupportTicketsView.d.ts +4 -0
  23. package/dist/features/admin/components/AdminSupportTicketsView.js +151 -0
  24. package/dist/features/admin/components/AdminTeamView.d.ts +4 -0
  25. package/dist/features/admin/components/AdminTeamView.js +139 -0
  26. package/dist/features/admin/components/AdminUserEditorView.d.ts +14 -1
  27. package/dist/features/admin/components/AdminUserEditorView.js +116 -14
  28. package/dist/features/admin/components/AdminUsersView.js +39 -12
  29. package/dist/features/admin/components/index.d.ts +8 -0
  30. package/dist/features/admin/components/index.js +4 -0
  31. package/dist/features/admin/constants/filter-tabs.d.ts +37 -0
  32. package/dist/features/admin/constants/filter-tabs.js +17 -0
  33. package/dist/features/auth/server/checkSoftBan.d.ts +15 -0
  34. package/dist/features/auth/server/checkSoftBan.js +36 -0
  35. package/dist/features/auth/server.d.ts +1 -0
  36. package/dist/features/auth/server.js +1 -0
  37. package/dist/features/media/types/index.js +2 -1
  38. package/dist/features/stores/repository/store.repository.d.ts +5 -1
  39. package/dist/features/stores/repository/store.repository.js +27 -2
  40. package/dist/features/support/repository/support.repository.d.ts +18 -0
  41. package/dist/features/support/repository/support.repository.js +117 -0
  42. package/dist/index.d.ts +12 -0
  43. package/dist/index.js +12 -0
  44. package/dist/next/api/routeHandler.d.ts +8 -0
  45. package/dist/next/api/routeHandler.js +24 -3
  46. package/dist/next/routing/route-map.d.ts +2 -0
  47. package/dist/next/routing/route-map.js +1 -0
  48. package/dist/repositories/index.d.ts +2 -0
  49. package/dist/repositories/index.js +1 -0
  50. package/dist/security/pii-schemas.d.ts +2 -0
  51. package/dist/security/pii-schemas.js +2 -0
  52. package/dist/seed/stores-seed-data.js +8 -0
  53. package/dist/server.d.ts +6 -0
  54. package/dist/server.js +6 -0
  55. package/dist/tailwind-utilities.css +1 -1
  56. package/dist/utils/media-url.d.ts +9 -0
  57. package/dist/utils/media-url.js +29 -0
  58. package/package.json +1 -1
@@ -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";
@@ -266,6 +266,43 @@ export declare const ADMIN_EVENT_STATUS_TABS: readonly [{
266
266
  readonly id: "ended";
267
267
  readonly label: "Ended";
268
268
  }];
269
+ /** Admin > Support Tickets — ticket-status filter chip set. */
270
+ export declare const ADMIN_SUPPORT_TICKET_STATUS_TABS: readonly [{
271
+ readonly id: "All";
272
+ readonly label: "All";
273
+ }, {
274
+ readonly id: "open";
275
+ readonly label: "Open";
276
+ }, {
277
+ readonly id: "in_progress";
278
+ readonly label: "In Progress";
279
+ }, {
280
+ readonly id: "waiting_on_user";
281
+ readonly label: "Waiting";
282
+ }, {
283
+ readonly id: "resolved";
284
+ readonly label: "Resolved";
285
+ }, {
286
+ readonly id: "closed";
287
+ readonly label: "Closed";
288
+ }];
289
+ /** Admin > Support Tickets — priority filter chip set. */
290
+ export declare const ADMIN_SUPPORT_TICKET_PRIORITY_TABS: readonly [{
291
+ readonly id: "All";
292
+ readonly label: "All";
293
+ }, {
294
+ readonly id: "urgent";
295
+ readonly label: "Urgent";
296
+ }, {
297
+ readonly id: "high";
298
+ readonly label: "High";
299
+ }, {
300
+ readonly id: "normal";
301
+ readonly label: "Normal";
302
+ }, {
303
+ readonly id: "low";
304
+ readonly label: "Low";
305
+ }];
269
306
  /** Admin > Carts — cart-ownership filter chip set. */
270
307
  export declare const ADMIN_CART_OWNERSHIP_TABS: readonly [{
271
308
  readonly id: "All";
@@ -130,6 +130,23 @@ export const ADMIN_EVENT_STATUS_TABS = [
130
130
  { id: "active", label: "Active" },
131
131
  { id: "ended", label: "Ended" },
132
132
  ];
133
+ /** Admin > Support Tickets — ticket-status filter chip set. */
134
+ export const ADMIN_SUPPORT_TICKET_STATUS_TABS = [
135
+ ALL_TAB,
136
+ { id: "open", label: "Open" },
137
+ { id: "in_progress", label: "In Progress" },
138
+ { id: "waiting_on_user", label: "Waiting" },
139
+ { id: "resolved", label: "Resolved" },
140
+ { id: "closed", label: "Closed" },
141
+ ];
142
+ /** Admin > Support Tickets — priority filter chip set. */
143
+ export const ADMIN_SUPPORT_TICKET_PRIORITY_TABS = [
144
+ ALL_TAB,
145
+ { id: "urgent", label: "Urgent" },
146
+ { id: "high", label: "High" },
147
+ { id: "normal", label: "Normal" },
148
+ { id: "low", label: "Low" },
149
+ ];
133
150
  /** Admin > Carts — cart-ownership filter chip set. */
134
151
  export const ADMIN_CART_OWNERSHIP_TABS = [
135
152
  ALL_TAB,
@@ -0,0 +1,15 @@
1
+ import type { UserDocument, UserSoftBan } from "../schemas/firestore";
2
+ import type { BannedAction } from "../permissions/constants";
3
+ /**
4
+ * Returns true when the user has an active (non-expired) soft ban for the
5
+ * given action. Admin and employee roles always bypass soft-ban checks.
6
+ */
7
+ export declare function isSoftBanned(user: Pick<UserDocument, "role" | "softBans">, action: BannedAction): boolean;
8
+ /**
9
+ * Splits a user's soft bans into active (non-expired) and expired buckets.
10
+ * Useful for display in admin views and user settings.
11
+ */
12
+ export declare function getBanSummary(user: Pick<UserDocument, "softBans">): {
13
+ activeBans: UserSoftBan[];
14
+ expiredBans: UserSoftBan[];
15
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Returns true when the user has an active (non-expired) soft ban for the
3
+ * given action. Admin and employee roles always bypass soft-ban checks.
4
+ */
5
+ export function isSoftBanned(user, action) {
6
+ if (user.role === "admin" || user.role === "employee")
7
+ return false;
8
+ if (!user.softBans || user.softBans.length === 0)
9
+ return false;
10
+ const now = new Date();
11
+ return user.softBans.some((ban) => {
12
+ if (ban.action !== action)
13
+ return false;
14
+ if (ban.expiresAt && new Date(ban.expiresAt) < now)
15
+ return false;
16
+ return true;
17
+ });
18
+ }
19
+ /**
20
+ * Splits a user's soft bans into active (non-expired) and expired buckets.
21
+ * Useful for display in admin views and user settings.
22
+ */
23
+ export function getBanSummary(user) {
24
+ const now = new Date();
25
+ const activeBans = [];
26
+ const expiredBans = [];
27
+ for (const ban of user.softBans ?? []) {
28
+ if (ban.expiresAt && new Date(ban.expiresAt) < now) {
29
+ expiredBans.push(ban);
30
+ }
31
+ else {
32
+ activeBans.push(ban);
33
+ }
34
+ }
35
+ return { activeBans, expiredBans };
36
+ }
@@ -18,3 +18,4 @@ export * from "./consent-otp";
18
18
  export * from "./repository";
19
19
  export * from "./actions";
20
20
  export { authMeGET } from "./api/route";
21
+ export { isSoftBanned, getBanSummary } from "./server/checkSoftBan";
@@ -18,3 +18,4 @@ export * from "./consent-otp";
18
18
  export * from "./repository";
19
19
  export * from "./actions";
20
20
  export { authMeGET } from "./api/route";
21
+ export { isSoftBanned, getBanSummary } from "./server/checkSoftBan";
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { resolveMediaUrl } from "../../../utils/media-url";
2
3
  export const mediaFieldSchema = z.object({
3
4
  url: z.string(),
4
5
  type: z.enum(["image", "video", "file"]),
@@ -28,7 +29,7 @@ export function coerceMediaFieldArray(values, fallbackType = "image") {
28
29
  .filter((value) => value !== null);
29
30
  }
30
31
  export function getMediaUrl(value) {
31
- return coerceMediaField(value)?.url;
32
+ return resolveMediaUrl(coerceMediaField(value)?.url);
32
33
  }
33
34
  export function inferMediaTypeFromMime(mimeType, url) {
34
35
  if (mimeType?.startsWith("image/"))
@@ -4,10 +4,14 @@
4
4
  * Data access layer for the `stores` Firestore collection.
5
5
  * One store per seller; identified by storeSlug (used as document ID).
6
6
  */
7
- import { BaseRepository, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
7
+ import { BaseRepository, type SieveModel, type FirebaseSieveResult, type DocumentSnapshot } from "../../../providers/db-firebase";
8
8
  import { StoreDocument } from "../schemas";
9
9
  export declare class StoreRepository extends BaseRepository<StoreDocument> {
10
10
  constructor();
11
+ private encryptSecrets;
12
+ private decryptSecrets;
13
+ protected mapDoc<D = StoreDocument>(snap: DocumentSnapshot): D;
14
+ update(id: string, data: Partial<StoreDocument>): Promise<StoreDocument>;
11
15
  /**
12
16
  * Create a new store.
13
17
  * The document ID is set to storeSlug for easy URL-based lookups.