@mohasinac/appkit 2.6.4 → 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.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +8 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +18 -2
- package/dist/_internal/server/features/auth/capabilities.d.ts +17 -0
- package/dist/_internal/server/features/auth/capabilities.js +31 -0
- package/dist/_internal/server/features/auth/index.d.ts +2 -0
- package/dist/_internal/server/features/auth/index.js +2 -0
- package/dist/_internal/server/features/auth/permissions.d.ts +49 -0
- package/dist/_internal/server/features/auth/permissions.js +76 -0
- package/dist/_internal/shared/features/events/schema.d.ts +4 -4
- package/dist/client.d.ts +4 -4
- package/dist/client.js +2 -2
- package/dist/configs/next.d.ts +6 -1
- package/dist/configs/next.js +45 -18
- package/dist/constants/api-endpoints.d.ts +69 -0
- package/dist/constants/api-endpoints.js +34 -0
- package/dist/contracts/registry.d.ts +7 -0
- package/dist/features/admin/components/AdminEmployeeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminEmployeeEditorView.js +168 -0
- package/dist/features/admin/components/AdminPrizeDrawsView.d.ts +4 -0
- package/dist/features/admin/components/AdminPrizeDrawsView.js +135 -0
- package/dist/features/admin/components/AdminSidebar.d.ts +2 -0
- package/dist/features/admin/components/AdminStoreEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +55 -3
- package/dist/features/admin/components/AdminStoresView.js +3 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.d.ts +23 -0
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +83 -0
- package/dist/features/admin/components/AdminSupportTicketsView.d.ts +4 -0
- package/dist/features/admin/components/AdminSupportTicketsView.js +151 -0
- package/dist/features/admin/components/AdminTeamView.d.ts +4 -0
- package/dist/features/admin/components/AdminTeamView.js +139 -0
- package/dist/features/admin/components/AdminUserEditorView.d.ts +14 -1
- package/dist/features/admin/components/AdminUserEditorView.js +116 -14
- package/dist/features/admin/components/AdminUsersView.js +39 -12
- package/dist/features/admin/components/index.d.ts +10 -0
- package/dist/features/admin/components/index.js +6 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +71 -0
- package/dist/features/admin/constants/filter-tabs.js +33 -0
- package/dist/features/auth/server/checkSoftBan.d.ts +15 -0
- package/dist/features/auth/server/checkSoftBan.js +36 -0
- package/dist/features/auth/server.d.ts +1 -0
- package/dist/features/auth/server.js +1 -0
- package/dist/features/media/types/index.js +2 -1
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +4 -0
- package/dist/features/seller/components/SellerPreOrdersView.js +141 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +4 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +138 -0
- package/dist/features/seller/components/index.d.ts +4 -0
- package/dist/features/seller/components/index.js +2 -0
- package/dist/features/stores/repository/store.repository.d.ts +5 -1
- package/dist/features/stores/repository/store.repository.js +27 -2
- package/dist/features/support/repository/support.repository.d.ts +18 -0
- package/dist/features/support/repository/support.repository.js +117 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/next/api/routeHandler.d.ts +8 -0
- package/dist/next/api/routeHandler.js +24 -3
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +1 -0
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +2 -0
- package/dist/seed/stores-seed-data.js +8 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +6 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/media-url.d.ts +9 -0
- package/dist/utils/media-url.js +29 -0
- package/package.json +1 -1
|
@@ -15,6 +15,8 @@ export { AdminCategoriesView } from "./AdminCategoriesView";
|
|
|
15
15
|
export { AdminCategoryEditorView } from "./AdminCategoryEditorView";
|
|
16
16
|
export { AdminBrandsView } from "./AdminBrandsView";
|
|
17
17
|
export { AdminBrandEditorView } from "./AdminBrandEditorView";
|
|
18
|
+
// SB4-E 2026-05-14 — admin prize draws listing view.
|
|
19
|
+
export { AdminPrizeDrawsView } from "./AdminPrizeDrawsView";
|
|
18
20
|
// S-SBUNI-4 2026-05-13 — admin bundle CRUD views.
|
|
19
21
|
export { AdminBundlesView } from "./AdminBundlesView";
|
|
20
22
|
export { AdminBundleEditorView } from "./AdminBundleEditorView";
|
|
@@ -67,3 +69,7 @@ export { AdminStoreAddressesView } from "./AdminStoreAddressesView";
|
|
|
67
69
|
export { AdminSidebar } from "./AdminSidebar";
|
|
68
70
|
export { AdminTopBar } from "./AdminTopBar";
|
|
69
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";
|
|
@@ -336,6 +373,40 @@ export declare const SELLER_AUCTION_STATUS_TABS: readonly [{
|
|
|
336
373
|
readonly id: "cancelled";
|
|
337
374
|
readonly label: "Cancelled";
|
|
338
375
|
}];
|
|
376
|
+
/** Seller > Pre-orders — pre-order-state filter chip set. */
|
|
377
|
+
export declare const SELLER_PRE_ORDER_STATUS_TABS: readonly [{
|
|
378
|
+
readonly id: "All";
|
|
379
|
+
readonly label: "All";
|
|
380
|
+
}, {
|
|
381
|
+
readonly id: "active";
|
|
382
|
+
readonly label: "Active";
|
|
383
|
+
}, {
|
|
384
|
+
readonly id: "draft";
|
|
385
|
+
readonly label: "Draft";
|
|
386
|
+
}, {
|
|
387
|
+
readonly id: "archived";
|
|
388
|
+
readonly label: "Archived";
|
|
389
|
+
}, {
|
|
390
|
+
readonly id: "cancelled";
|
|
391
|
+
readonly label: "Cancelled";
|
|
392
|
+
}];
|
|
393
|
+
/** Seller > Prize Draws — draw-state filter chip set. */
|
|
394
|
+
export declare const SELLER_PRIZE_DRAW_STATUS_TABS: readonly [{
|
|
395
|
+
readonly id: "All";
|
|
396
|
+
readonly label: "All";
|
|
397
|
+
}, {
|
|
398
|
+
readonly id: "active";
|
|
399
|
+
readonly label: "Active";
|
|
400
|
+
}, {
|
|
401
|
+
readonly id: "draft";
|
|
402
|
+
readonly label: "Draft";
|
|
403
|
+
}, {
|
|
404
|
+
readonly id: "ended";
|
|
405
|
+
readonly label: "Ended";
|
|
406
|
+
}, {
|
|
407
|
+
readonly id: "cancelled";
|
|
408
|
+
readonly label: "Cancelled";
|
|
409
|
+
}];
|
|
339
410
|
/** Seller > Orders — order-state filter chip set. Subset of
|
|
340
411
|
* `ADMIN_ORDER_STATUS_TABS` — sellers don't see `RETURN_REQUESTED` until
|
|
341
412
|
* the buyer initiates one through support. */
|
|
@@ -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,
|
|
@@ -168,6 +185,22 @@ export const SELLER_AUCTION_STATUS_TABS = [
|
|
|
168
185
|
{ id: "ended", label: "Ended" },
|
|
169
186
|
{ id: "cancelled", label: "Cancelled" },
|
|
170
187
|
];
|
|
188
|
+
/** Seller > Pre-orders — pre-order-state filter chip set. */
|
|
189
|
+
export const SELLER_PRE_ORDER_STATUS_TABS = [
|
|
190
|
+
ALL_TAB,
|
|
191
|
+
{ id: "active", label: "Active" },
|
|
192
|
+
{ id: "draft", label: "Draft" },
|
|
193
|
+
{ id: "archived", label: "Archived" },
|
|
194
|
+
{ id: "cancelled", label: "Cancelled" },
|
|
195
|
+
];
|
|
196
|
+
/** Seller > Prize Draws — draw-state filter chip set. */
|
|
197
|
+
export const SELLER_PRIZE_DRAW_STATUS_TABS = [
|
|
198
|
+
ALL_TAB,
|
|
199
|
+
{ id: "active", label: "Active" },
|
|
200
|
+
{ id: "draft", label: "Draft" },
|
|
201
|
+
{ id: "ended", label: "Ended" },
|
|
202
|
+
{ id: "cancelled", label: "Cancelled" },
|
|
203
|
+
];
|
|
171
204
|
/** Seller > Orders — order-state filter chip set. Subset of
|
|
172
205
|
* `ADMIN_ORDER_STATUS_TABS` — sellers don't see `RETURN_REQUESTED` until
|
|
173
206
|
* the buyer initiates one through support. */
|
|
@@ -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
|
+
}
|
|
@@ -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/"))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ListingViewShellProps } from "../../../ui";
|
|
2
|
+
export interface SellerPreOrdersViewProps extends ListingViewShellProps {
|
|
3
|
+
}
|
|
4
|
+
export declare function SellerPreOrdersView({ children, ...props }: SellerPreOrdersViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,141 @@
|
|
|
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 { Pencil, X } from "lucide-react";
|
|
5
|
+
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
|
+
import { Alert, Badge, Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell } from "../../../ui";
|
|
7
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
import { SELLER_PRE_ORDER_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
9
|
+
import { ROUTES } from "../../../constants";
|
|
10
|
+
import { toRecordArray, toRelativeDate, toRupees, toStringValue, useSellerListingData, } from "../hooks/useSellerListingData";
|
|
11
|
+
import { DataTable } from "../../admin/components/DataTable";
|
|
12
|
+
import { useActionDispatch } from "../../../react/hooks/use-action-dispatch";
|
|
13
|
+
const PAGE_SIZE = 25;
|
|
14
|
+
const FILTER_KEYS = ["status"];
|
|
15
|
+
const DEFAULT_SORT = "-createdAt";
|
|
16
|
+
// Hardcoded sieve clause — this view only ever shows pre-orders.
|
|
17
|
+
const LISTING_TYPE_FILTER = "listingType==pre-order";
|
|
18
|
+
const SORT_OPTIONS = [
|
|
19
|
+
{ value: "-createdAt", label: "Newest" },
|
|
20
|
+
{ value: "createdAt", label: "Oldest" },
|
|
21
|
+
{ value: "title", label: "Title A–Z" },
|
|
22
|
+
{ value: "preorderAvailableDate", label: "Delivery Soon" },
|
|
23
|
+
];
|
|
24
|
+
const STATUS_OPTIONS = SELLER_PRE_ORDER_STATUS_TABS;
|
|
25
|
+
const PRE_ORDER_COLUMNS = [
|
|
26
|
+
{
|
|
27
|
+
key: "thumbnail",
|
|
28
|
+
header: "",
|
|
29
|
+
className: "w-12",
|
|
30
|
+
render: (row) => row.imageUrl ? (_jsx("img", { src: row.imageUrl, alt: "", className: "w-10 h-10 rounded-lg object-cover border border-[var(--appkit-color-border)]" })) : (_jsx("div", { className: "w-10 h-10 rounded-lg bg-[var(--appkit-color-surface-raised)] border border-[var(--appkit-color-border)] flex items-center justify-center", children: _jsx("span", { className: "text-xs text-[var(--appkit-color-text-faint)]", children: "\u2013" }) })),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "primary",
|
|
34
|
+
header: "Pre-order",
|
|
35
|
+
render: (row) => (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "font-medium text-[var(--appkit-color-text)] line-clamp-1", children: row.primary }), _jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.secondary })] })),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "price",
|
|
39
|
+
header: "Price",
|
|
40
|
+
className: "w-28 text-right",
|
|
41
|
+
render: (row) => (_jsx("span", { className: "text-sm font-medium text-[var(--appkit-color-text)]", children: row.price })),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "status",
|
|
45
|
+
header: "Status",
|
|
46
|
+
className: "w-28",
|
|
47
|
+
render: (row) => {
|
|
48
|
+
const variant = row.status === "active"
|
|
49
|
+
? "success"
|
|
50
|
+
: row.status === "draft"
|
|
51
|
+
? "default"
|
|
52
|
+
: row.status === "cancelled"
|
|
53
|
+
? "danger"
|
|
54
|
+
: "warning";
|
|
55
|
+
return _jsx(Badge, { variant: variant, children: row.status });
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "deliveryDate",
|
|
60
|
+
header: "Est. Delivery",
|
|
61
|
+
className: "w-36",
|
|
62
|
+
render: (row) => (_jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.deliveryDate })),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "updatedAt",
|
|
66
|
+
header: "Updated",
|
|
67
|
+
className: "w-28",
|
|
68
|
+
render: (row) => (_jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.updatedAt })),
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
export function SellerPreOrdersView({ children, ...props }) {
|
|
72
|
+
const hasChildren = React.Children.count(children) > 0;
|
|
73
|
+
const dispatch = useActionDispatch();
|
|
74
|
+
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
75
|
+
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
76
|
+
const [filterOpen, setFilterOpen] = useState(false);
|
|
77
|
+
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
78
|
+
const openFilters = useCallback(() => {
|
|
79
|
+
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
80
|
+
setFilterOpen(true);
|
|
81
|
+
}, [table]);
|
|
82
|
+
const applyFilters = useCallback(() => {
|
|
83
|
+
const updates = { page: "1" };
|
|
84
|
+
for (const k of FILTER_KEYS)
|
|
85
|
+
updates[k] = pendingFilters[k] ?? "";
|
|
86
|
+
table.setMany(updates);
|
|
87
|
+
setFilterOpen(false);
|
|
88
|
+
}, [pendingFilters, table]);
|
|
89
|
+
const clearFilters = useCallback(() => {
|
|
90
|
+
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
|
|
91
|
+
}, []);
|
|
92
|
+
const resetAll = useCallback(() => {
|
|
93
|
+
const updates = { q: "", sort: "" };
|
|
94
|
+
for (const k of FILTER_KEYS)
|
|
95
|
+
updates[k] = "";
|
|
96
|
+
table.setMany(updates);
|
|
97
|
+
setSearchInput("");
|
|
98
|
+
}, [table]);
|
|
99
|
+
const commitSearch = useCallback(() => {
|
|
100
|
+
table.set("q", searchInput.trim());
|
|
101
|
+
}, [searchInput, table]);
|
|
102
|
+
const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
|
|
103
|
+
const hasActiveState = !!table.get("q") || table.get("sort") !== DEFAULT_SORT || activeFilterCount > 0;
|
|
104
|
+
const statusRaw = table.get("status");
|
|
105
|
+
const statusFilter = statusRaw && statusRaw !== "All" ? `status==${statusRaw}` : undefined;
|
|
106
|
+
const filters = [LISTING_TYPE_FILTER, statusFilter].filter(Boolean).join(",");
|
|
107
|
+
const { rows, total, isLoading, errorMessage } = useSellerListingData({
|
|
108
|
+
queryKey: ["seller", "pre-orders", "listing"],
|
|
109
|
+
endpoint: SELLER_ENDPOINTS.PRODUCTS,
|
|
110
|
+
page: table.getNumber("page", 1),
|
|
111
|
+
pageSize: PAGE_SIZE,
|
|
112
|
+
sorts: table.get("sort") || DEFAULT_SORT,
|
|
113
|
+
filters,
|
|
114
|
+
q: table.get("q") || undefined,
|
|
115
|
+
mapRows: (response) => toRecordArray(response.products).map((item, index) => {
|
|
116
|
+
const priceRaw = typeof item.price === "number" ? item.price : 0;
|
|
117
|
+
return {
|
|
118
|
+
id: toStringValue(item.id, `preorder-${index}`),
|
|
119
|
+
primary: toStringValue(item.title ?? item.name, "Untitled pre-order"),
|
|
120
|
+
secondary: toStringValue(item.condition, ""),
|
|
121
|
+
status: toStringValue(item.status, "draft"),
|
|
122
|
+
price: priceRaw ? toRupees(priceRaw) : "—",
|
|
123
|
+
deliveryDate: item.preorderAvailableDate
|
|
124
|
+
? toRelativeDate(item.preorderAvailableDate)
|
|
125
|
+
: "TBA",
|
|
126
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
127
|
+
imageUrl: toStringValue(item.mainImage ?? item.images?.[0], undefined),
|
|
128
|
+
};
|
|
129
|
+
}),
|
|
130
|
+
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
131
|
+
});
|
|
132
|
+
const currentPage = table.getNumber("page", 1);
|
|
133
|
+
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
134
|
+
if (hasChildren) {
|
|
135
|
+
return (_jsx(ListingViewShell, { portal: "seller", ...props, children: children }));
|
|
136
|
+
}
|
|
137
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search pre-orders by name\u2026", 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 z-10 flex justify-center bg-[var(--appkit-color-surface)]/95 backdrop-blur-sm border-b border-[var(--appkit-color-border)] px-3 py-1.5", style: { top: "calc(var(--header-height, 0px) + 44px)" }, 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(Alert, { variant: "error", className: "mb-4", children: errorMessage })), _jsx(DataTable, { columns: PRE_ORDER_COLUMNS, rows: rows, isLoading: isLoading, emptyLabel: "No pre-orders listed yet", getRowHref: (row) => String(ROUTES.STORE.PRE_ORDERS_EDIT(row.id)), renderRowActions: (row) => (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRE_ORDERS_EDIT(row.id)) });
|
|
140
|
+
}, "aria-label": "Edit", children: _jsx(Pencil, { className: "w-4 h-4" }) })) })] }), 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-[var(--appkit-color-surface)] shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-[var(--appkit-color-border)] px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-[var(--appkit-color-text)]", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-error)] transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-border-subtle)] transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("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("div", { className: "border-t border-[var(--appkit-color-border)] px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-[var(--appkit-color-primary)] py-2.5 text-sm font-semibold text-white hover:opacity-90 transition-opacity active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
141
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ListingViewShellProps } from "../../../ui";
|
|
2
|
+
export interface SellerPrizeDrawsViewProps extends ListingViewShellProps {
|
|
3
|
+
}
|
|
4
|
+
export declare function SellerPrizeDrawsView({ children, ...props }: SellerPrizeDrawsViewProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,138 @@
|
|
|
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 { Pencil, X } from "lucide-react";
|
|
5
|
+
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
|
+
import { Alert, Badge, Button, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell } from "../../../ui";
|
|
7
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
import { SELLER_PRIZE_DRAW_STATUS_TABS } from "../../admin/constants/filter-tabs";
|
|
9
|
+
import { ROUTES } from "../../../constants";
|
|
10
|
+
import { toRecordArray, toRelativeDate, toRupees, toStringValue, useSellerListingData, } from "../hooks/useSellerListingData";
|
|
11
|
+
import { DataTable } from "../../admin/components/DataTable";
|
|
12
|
+
import { useActionDispatch } from "../../../react/hooks/use-action-dispatch";
|
|
13
|
+
const PAGE_SIZE = 25;
|
|
14
|
+
const FILTER_KEYS = ["status"];
|
|
15
|
+
const DEFAULT_SORT = "-createdAt";
|
|
16
|
+
// Hardcoded sieve clause — this view only ever shows prize draws.
|
|
17
|
+
const LISTING_TYPE_FILTER = "listingType==prize-draw";
|
|
18
|
+
const SORT_OPTIONS = [
|
|
19
|
+
{ value: "-createdAt", label: "Newest" },
|
|
20
|
+
{ value: "createdAt", label: "Oldest" },
|
|
21
|
+
{ value: "title", label: "Title A–Z" },
|
|
22
|
+
{ value: "prizeDrawEndDate", label: "Draw Date Soon" },
|
|
23
|
+
];
|
|
24
|
+
const STATUS_OPTIONS = SELLER_PRIZE_DRAW_STATUS_TABS;
|
|
25
|
+
const STATUS_VARIANT = {
|
|
26
|
+
active: "success",
|
|
27
|
+
draft: "default",
|
|
28
|
+
ended: "secondary",
|
|
29
|
+
cancelled: "danger",
|
|
30
|
+
};
|
|
31
|
+
const PRIZE_DRAW_COLUMNS = [
|
|
32
|
+
{
|
|
33
|
+
key: "thumbnail",
|
|
34
|
+
header: "",
|
|
35
|
+
className: "w-12",
|
|
36
|
+
render: (row) => row.imageUrl ? (_jsx("img", { src: row.imageUrl, alt: "", className: "w-10 h-10 rounded-lg object-cover border border-[var(--appkit-color-border)]" })) : (_jsx("div", { className: "w-10 h-10 rounded-lg bg-[var(--appkit-color-surface-raised)] border border-[var(--appkit-color-border)] flex items-center justify-center", children: _jsx("span", { className: "text-xs text-[var(--appkit-color-text-faint)]", children: "\u2013" }) })),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "primary",
|
|
40
|
+
header: "Prize Draw",
|
|
41
|
+
render: (row) => (_jsxs("div", { className: "space-y-1", children: [_jsx("p", { className: "font-medium text-[var(--appkit-color-text)] line-clamp-1", children: row.primary }), _jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.secondary })] })),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "entryFee",
|
|
45
|
+
header: "Entry Fee",
|
|
46
|
+
className: "w-28 text-right",
|
|
47
|
+
render: (row) => (_jsx("span", { className: "text-sm font-medium text-[var(--appkit-color-text)]", children: row.entryFee })),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "status",
|
|
51
|
+
header: "Status",
|
|
52
|
+
className: "w-28",
|
|
53
|
+
render: (row) => (_jsx(Badge, { variant: STATUS_VARIANT[row.status] ?? "default", children: row.status })),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "drawDate",
|
|
57
|
+
header: "Draw Date",
|
|
58
|
+
className: "w-32",
|
|
59
|
+
render: (row) => (_jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.drawDate })),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "updatedAt",
|
|
63
|
+
header: "Updated",
|
|
64
|
+
className: "w-28",
|
|
65
|
+
render: (row) => (_jsx("span", { className: "text-xs text-[var(--appkit-color-text-muted)]", children: row.updatedAt })),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
export function SellerPrizeDrawsView({ children, ...props }) {
|
|
69
|
+
const hasChildren = React.Children.count(children) > 0;
|
|
70
|
+
const dispatch = useActionDispatch();
|
|
71
|
+
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
72
|
+
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
73
|
+
const [filterOpen, setFilterOpen] = useState(false);
|
|
74
|
+
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
75
|
+
const openFilters = useCallback(() => {
|
|
76
|
+
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
77
|
+
setFilterOpen(true);
|
|
78
|
+
}, [table]);
|
|
79
|
+
const applyFilters = useCallback(() => {
|
|
80
|
+
const updates = { page: "1" };
|
|
81
|
+
for (const k of FILTER_KEYS)
|
|
82
|
+
updates[k] = pendingFilters[k] ?? "";
|
|
83
|
+
table.setMany(updates);
|
|
84
|
+
setFilterOpen(false);
|
|
85
|
+
}, [pendingFilters, table]);
|
|
86
|
+
const clearFilters = useCallback(() => {
|
|
87
|
+
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, ""])));
|
|
88
|
+
}, []);
|
|
89
|
+
const resetAll = useCallback(() => {
|
|
90
|
+
const updates = { q: "", sort: "" };
|
|
91
|
+
for (const k of FILTER_KEYS)
|
|
92
|
+
updates[k] = "";
|
|
93
|
+
table.setMany(updates);
|
|
94
|
+
setSearchInput("");
|
|
95
|
+
}, [table]);
|
|
96
|
+
const commitSearch = useCallback(() => {
|
|
97
|
+
table.set("q", searchInput.trim());
|
|
98
|
+
}, [searchInput, table]);
|
|
99
|
+
const activeFilterCount = FILTER_KEYS.filter((k) => !!table.get(k)).length;
|
|
100
|
+
const hasActiveState = !!table.get("q") || table.get("sort") !== DEFAULT_SORT || activeFilterCount > 0;
|
|
101
|
+
const statusRaw = table.get("status");
|
|
102
|
+
const statusFilter = statusRaw && statusRaw !== "All" ? `status==${statusRaw}` : undefined;
|
|
103
|
+
const filters = [LISTING_TYPE_FILTER, statusFilter].filter(Boolean).join(",");
|
|
104
|
+
const { rows, total, isLoading, errorMessage } = useSellerListingData({
|
|
105
|
+
queryKey: ["seller", "prize-draws", "listing"],
|
|
106
|
+
endpoint: SELLER_ENDPOINTS.PRODUCTS,
|
|
107
|
+
page: table.getNumber("page", 1),
|
|
108
|
+
pageSize: PAGE_SIZE,
|
|
109
|
+
sorts: table.get("sort") || DEFAULT_SORT,
|
|
110
|
+
filters,
|
|
111
|
+
q: table.get("q") || undefined,
|
|
112
|
+
mapRows: (response) => toRecordArray(response.products).map((item, index) => {
|
|
113
|
+
const priceRaw = typeof item.price === "number" ? item.price : 0;
|
|
114
|
+
return {
|
|
115
|
+
id: toStringValue(item.id, `prize-draw-${index}`),
|
|
116
|
+
primary: toStringValue(item.title ?? item.name, "Untitled prize draw"),
|
|
117
|
+
secondary: toStringValue(item.condition, ""),
|
|
118
|
+
status: toStringValue(item.status, "draft"),
|
|
119
|
+
entryFee: priceRaw ? toRupees(priceRaw) : "Free",
|
|
120
|
+
drawDate: item.prizeDrawEndDate
|
|
121
|
+
? toRelativeDate(item.prizeDrawEndDate)
|
|
122
|
+
: "TBA",
|
|
123
|
+
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
124
|
+
imageUrl: toStringValue(item.mainImage ?? item.images?.[0], undefined),
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
|
|
128
|
+
});
|
|
129
|
+
const currentPage = table.getNumber("page", 1);
|
|
130
|
+
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
131
|
+
if (hasChildren) {
|
|
132
|
+
return (_jsx(ListingViewShell, { portal: "seller", ...props, children: children }));
|
|
133
|
+
}
|
|
134
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search prize draws by name\u2026", 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 z-10 flex justify-center bg-[var(--appkit-color-surface)]/95 backdrop-blur-sm border-b border-[var(--appkit-color-border)] px-3 py-1.5", style: { top: "calc(var(--header-height, 0px) + 44px)" }, 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(Alert, { variant: "error", className: "mb-4", children: errorMessage })), _jsx(DataTable, { columns: PRIZE_DRAW_COLUMNS, rows: rows, isLoading: isLoading, emptyLabel: "No prize draws listed yet", getRowHref: (row) => String(ROUTES.STORE.PRIZE_DRAWS_EDIT(row.id)), renderRowActions: (row) => (_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => {
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRIZE_DRAWS_EDIT(row.id)) });
|
|
137
|
+
}, "aria-label": "Edit", children: _jsx(Pencil, { className: "w-4 h-4" }) })) })] }), 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-[var(--appkit-color-surface)] shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-[var(--appkit-color-border)] px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-[var(--appkit-color-text)]", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-error)] transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-border-subtle)] transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("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("div", { className: "border-t border-[var(--appkit-color-border)] px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-[var(--appkit-color-primary)] py-2.5 text-sm font-semibold text-white hover:opacity-90 transition-opacity active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }))] }));
|
|
138
|
+
}
|
|
@@ -7,6 +7,10 @@ export { SellerProductsView } from "./SellerProductsView";
|
|
|
7
7
|
export type { SellerProductsViewProps } from "./SellerProductsView";
|
|
8
8
|
export { SellerAuctionsView } from "./SellerAuctionsView";
|
|
9
9
|
export type { SellerAuctionsViewProps } from "./SellerAuctionsView";
|
|
10
|
+
export { SellerPreOrdersView } from "./SellerPreOrdersView";
|
|
11
|
+
export type { SellerPreOrdersViewProps } from "./SellerPreOrdersView";
|
|
12
|
+
export { SellerPrizeDrawsView } from "./SellerPrizeDrawsView";
|
|
13
|
+
export type { SellerPrizeDrawsViewProps } from "./SellerPrizeDrawsView";
|
|
10
14
|
export { SellerOrdersView } from "./SellerOrdersView";
|
|
11
15
|
export type { SellerOrdersViewProps } from "./SellerOrdersView";
|
|
12
16
|
export { SellerOffersView } from "./SellerOffersView";
|
|
@@ -3,6 +3,8 @@ export { SellerSidebar } from "./SellerSidebar";
|
|
|
3
3
|
export { SellerDashboardView } from "./SellerDashboardView";
|
|
4
4
|
export { SellerProductsView } from "./SellerProductsView";
|
|
5
5
|
export { SellerAuctionsView } from "./SellerAuctionsView";
|
|
6
|
+
export { SellerPreOrdersView } from "./SellerPreOrdersView";
|
|
7
|
+
export { SellerPrizeDrawsView } from "./SellerPrizeDrawsView";
|
|
6
8
|
export { SellerOrdersView } from "./SellerOrdersView";
|
|
7
9
|
export { SellerOffersView } from "./SellerOffersView";
|
|
8
10
|
export { SellerOffersPanel } from "./SellerOffersPanel";
|
|
@@ -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.
|
|
@@ -8,10 +8,35 @@ import { BaseRepository, prepareForFirestore, applySieveToFirestore, } from "../
|
|
|
8
8
|
import { increment } from "../../../contracts/field-ops";
|
|
9
9
|
import { STORE_COLLECTION, STORE_FIELDS, DEFAULT_STORE_DATA, StoreStatusValues, } from "../schemas";
|
|
10
10
|
import { DatabaseError } from "../../../errors";
|
|
11
|
+
import { encryptSecret, decryptSecret, } from "../../../security/settings-encryption";
|
|
11
12
|
export class StoreRepository extends BaseRepository {
|
|
12
13
|
constructor() {
|
|
13
14
|
super(STORE_COLLECTION);
|
|
14
15
|
}
|
|
16
|
+
encryptSecrets(data) {
|
|
17
|
+
const token = data.whatsappConfig?.accessToken;
|
|
18
|
+
if (!token)
|
|
19
|
+
return data;
|
|
20
|
+
return {
|
|
21
|
+
...data,
|
|
22
|
+
whatsappConfig: { ...data.whatsappConfig, accessToken: encryptSecret(token) },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
decryptSecrets(data) {
|
|
26
|
+
const token = data.whatsappConfig?.accessToken;
|
|
27
|
+
if (!token)
|
|
28
|
+
return data;
|
|
29
|
+
return {
|
|
30
|
+
...data,
|
|
31
|
+
whatsappConfig: { ...data.whatsappConfig, accessToken: decryptSecret(token) },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
mapDoc(snap) {
|
|
35
|
+
return this.decryptSecrets(super.mapDoc(snap));
|
|
36
|
+
}
|
|
37
|
+
async update(id, data) {
|
|
38
|
+
return super.update(id, this.encryptSecrets(data));
|
|
39
|
+
}
|
|
15
40
|
/**
|
|
16
41
|
* Create a new store.
|
|
17
42
|
* The document ID is set to storeSlug for easy URL-based lookups.
|
|
@@ -37,7 +62,7 @@ export class StoreRepository extends BaseRepository {
|
|
|
37
62
|
await this.db
|
|
38
63
|
.collection(this.collection)
|
|
39
64
|
.doc(input.storeSlug)
|
|
40
|
-
.create(prepareForFirestore(storeData));
|
|
65
|
+
.create(prepareForFirestore(this.encryptSecrets(storeData)));
|
|
41
66
|
}
|
|
42
67
|
catch (err) {
|
|
43
68
|
// gRPC ALREADY_EXISTS = code 6
|
|
@@ -210,7 +235,7 @@ export class StoreRepository extends BaseRepository {
|
|
|
210
235
|
const batch = this.db.batch();
|
|
211
236
|
const newRef = this.db.collection(this.collection).doc(newSlug);
|
|
212
237
|
const oldRef = this.db.collection(this.collection).doc(currentSlug);
|
|
213
|
-
batch.create(newRef, prepareForFirestore(newDoc));
|
|
238
|
+
batch.create(newRef, prepareForFirestore(this.encryptSecrets(newDoc)));
|
|
214
239
|
batch.delete(oldRef);
|
|
215
240
|
await batch.commit();
|
|
216
241
|
return { id: newSlug, ...newDoc };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
+
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
|
+
import { type SupportTicketDocument, type SupportTicketCreateInput, type SupportTicketUpdateInput, type TicketMessage, type TicketStatus } from "../schemas/firestore";
|
|
4
|
+
export declare class SupportRepository extends BaseRepository<SupportTicketDocument> {
|
|
5
|
+
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
|
+
constructor();
|
|
7
|
+
createTicket(input: SupportTicketCreateInput): Promise<SupportTicketDocument>;
|
|
8
|
+
getUserTickets(userId: string, page?: number, pageSize?: number): Promise<FirebaseSieveResult<SupportTicketDocument>>;
|
|
9
|
+
getTicketById(ticketId: string): Promise<SupportTicketDocument | null>;
|
|
10
|
+
countActiveTickets(userId: string): Promise<number>;
|
|
11
|
+
getActiveOrderTicket(userId: string, orderId: string): Promise<SupportTicketDocument | null>;
|
|
12
|
+
getActiveCategoryTicket(userId: string, category: string): Promise<SupportTicketDocument | null>;
|
|
13
|
+
updateTicketStatus(ticketId: string, update: SupportTicketUpdateInput): Promise<SupportTicketDocument>;
|
|
14
|
+
addMessage(ticketId: string, message: TicketMessage, newStatus?: TicketStatus): Promise<void>;
|
|
15
|
+
assignTicket(ticketId: string, assignedTo: string, assignedToName: string): Promise<SupportTicketDocument>;
|
|
16
|
+
listAll(model: SieveModel): Promise<FirebaseSieveResult<SupportTicketDocument>>;
|
|
17
|
+
}
|
|
18
|
+
export declare const supportRepository: SupportRepository;
|