@mohasinac/appkit 4.1.0 → 4.1.3
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/maintenance/index.d.ts +2 -0
- package/dist/_internal/client/features/maintenance/index.js +1 -0
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.d.ts +9 -0
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +55 -0
- package/dist/_internal/server/features/auctions/index.d.ts +1 -1
- package/dist/_internal/server/features/auctions/index.js +1 -1
- package/dist/_internal/server/features/auctions/service.js +2 -2
- package/dist/_internal/server/features/maintenance/cloud-logs-data.d.ts +50 -0
- package/dist/_internal/server/features/maintenance/cloud-logs-data.js +152 -0
- package/dist/_internal/server/jobs/core/onOrderCreate.d.ts +4 -12
- package/dist/_internal/server/jobs/core/onOrderCreate.js +3 -3
- package/dist/_internal/server/jobs/core/onProductWrite.js +14 -2
- package/dist/_internal/shared/features/auctions/config.d.ts +1 -1
- package/dist/_internal/shared/features/auctions/config.js +1 -1
- package/dist/_internal/shared/features/auctions/schema.js +1 -1
- package/dist/_internal/shared/features/cart/schema.js +2 -2
- package/dist/_internal/shared/features/events/schema.js +1 -1
- package/dist/_internal/shared/features/orders/schema.js +1 -1
- package/dist/_internal/shared/features/products/schema.js +3 -3
- package/dist/_internal/shared/features/promotions/schema.js +3 -3
- package/dist/_internal/shared/fees/calculator.d.ts +6 -7
- package/dist/client.d.ts +4 -4
- package/dist/client.js +4 -2
- package/dist/constants/api-endpoints.d.ts +9 -6
- package/dist/constants/api-endpoints.js +3 -2
- package/dist/contracts/client-payment-gateway.d.ts +1 -2
- package/dist/features/about/components/AboutView.d.ts +6 -16
- package/dist/features/about/components/AboutView.js +3 -3
- package/dist/features/about/index.d.ts +2 -1
- package/dist/features/about/schemas/firestore.d.ts +47 -0
- package/dist/features/about/schemas/firestore.js +6 -0
- package/dist/features/about/schemas/index.d.ts +210 -0
- package/dist/features/about/schemas/index.js +45 -0
- package/dist/features/account/hooks/useProfile.js +3 -0
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +6 -0
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +50 -0
- package/dist/features/admin/components/AdminCartsView.js +16 -3
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
- package/dist/features/admin/components/AdminCouponsView.js +14 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +1 -42
- package/dist/features/admin/components/AdminMediaView.js +1 -1
- package/dist/features/admin/components/AdminNotificationsView.js +31 -19
- package/dist/features/admin/components/AdminOrdersView.js +7 -0
- package/dist/features/admin/components/AdminPayoutsView.js +18 -3
- package/dist/features/admin/components/AdminProductsView.js +17 -2
- package/dist/features/admin/components/AdminReviewsView.js +16 -2
- package/dist/features/admin/components/AdminScammersView.js +7 -0
- package/dist/features/admin/components/AdminSectionsView.js +4 -0
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +44 -2
- package/dist/features/admin/components/AdminStoreEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +4 -4
- package/dist/features/admin/components/AdminStoresView.js +1 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +23 -5
- package/dist/features/admin/components/AdminSupportTicketsView.js +20 -2
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +4 -1
- package/dist/features/admin/components/AdminTesterChecklistView.js +8 -0
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +55 -41
- package/dist/features/admin/constants/filter-tabs.js +55 -27
- package/dist/features/admin/schemas/firestore.d.ts +5 -0
- package/dist/features/auctions/actions/bid-actions.js +1 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/AuctionsListView.js +8 -1
- package/dist/features/auth/components/LoginForm.js +2 -2
- package/dist/features/auth/components/RegisterForm.js +2 -2
- package/dist/features/auth/permissions/constants.d.ts +16 -0
- package/dist/features/auth/permissions/constants.js +53 -0
- package/dist/features/auth/schemas/firestore.js +1 -0
- package/dist/features/blog/components/BlogFilters.js +1 -1
- package/dist/features/cart/schemas/firestore.d.ts +1 -1
- package/dist/features/categories/schemas/firestore.d.ts +1 -1
- package/dist/features/consultation/components/ConsultationForm.js +1 -1
- package/dist/features/events/components/EventFilters.js +2 -0
- package/dist/features/homepage/components/WelcomeSection.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +2 -2
- package/dist/features/layout/TitleBarLayout.js +4 -4
- package/dist/features/media/AvatarUpload.js +7 -3
- package/dist/features/media/MediaVideo.js +1 -1
- package/dist/features/media/finalize.d.ts +9 -13
- package/dist/features/media/finalize.js +23 -74
- package/dist/features/media/server.d.ts +1 -1
- package/dist/features/media/server.js +1 -1
- package/dist/features/orders/hooks/useOrders.js +16 -4
- package/dist/features/orders/schemas/firestore.d.ts +5 -5
- package/dist/features/payments/schemas/firestore.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrdersListView.js +7 -0
- package/dist/features/products/components/ArtStickersListView.js +5 -0
- package/dist/features/products/components/ProductDetailPageView.d.ts +7 -1
- package/dist/features/products/components/ProductForm.js +14 -1
- package/dist/features/products/components/ProductsIndexPageView.js +7 -0
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/schemas/firestore.d.ts +1 -1
- package/dist/features/products/schemas/product-templates.d.ts +1 -1
- package/dist/features/products/types/index.d.ts +4 -0
- package/dist/features/scams/schemas/index.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/seller/components/SellerAuctionsView.js +27 -2
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPayoutsView.js +24 -10
- package/dist/features/seller/components/SellerPreOrdersView.js +12 -4
- package/dist/features/seller/components/SellerPrizeDrawsView.js +23 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +1 -1
- package/dist/features/seller/components/SellerProductsView.js +8 -6
- package/dist/features/seller/components/SellerReviewsView.js +27 -6
- package/dist/features/seller/components/index.d.ts +0 -2
- package/dist/features/seller/components/index.js +0 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
- package/dist/features/stores/components/StoreProductsView.js +1 -1
- package/dist/features/stores/schemas/firestore.d.ts +11 -0
- package/dist/features/stores/schemas/firestore.js +5 -0
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/tester/actions/checklist-item-actions.d.ts +6 -0
- package/dist/features/tester/actions/checklist-item-actions.js +2 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +13 -2
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +2 -1
- package/dist/features/tester/components/AdminTesterFeedbackReportView.js +9 -5
- package/dist/features/tester/components/TesterHubView.js +34 -21
- package/dist/features/tester/repository/tester-checklist-item.repository.js +1 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +18 -4
- package/dist/features/tester/repository/tester-checklist-response.repository.js +43 -8
- package/dist/features/tester/schemas/firestore.d.ts +8 -3
- package/dist/features/tester/schemas/firestore.js +6 -0
- package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/bids-tester-seed-data.js +37 -0
- package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +76 -0
- package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -2
- package/dist/features/tester/seed-data/index.d.ts +3 -0
- package/dist/features/tester/seed-data/index.js +3 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +2 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +265 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +213 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +294 -12
- package/dist/features/tester/utils/phases.d.ts +31 -0
- package/dist/features/tester/utils/phases.js +46 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +1 -1
- package/dist/features/whatsapp-bot/types/index.d.ts +1 -1
- package/dist/features/wishlist/types/index.d.ts +3 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +15 -7
- package/dist/next/api/routeHandler.js +6 -10
- package/dist/next/components/NotFoundView.js +2 -1
- package/dist/next/routing/route-map.d.ts +4 -12
- package/dist/next/routing/route-map.js +2 -4
- package/dist/providers/db-firebase/admin.d.ts +1 -0
- package/dist/providers/db-firebase/admin.js +1 -1
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seed/addresses-seed-data.js +18 -18
- package/dist/seed/bids-seed-data.js +4 -4
- package/dist/seed/blog-posts-seed-data.js +82 -82
- package/dist/seed/carousel-slides-seed-data.js +14 -10
- package/dist/seed/carousels-seed-data.js +3 -3
- package/dist/seed/cart-seed-data.js +92 -92
- package/dist/seed/claimed-coupons-seed-data.js +10 -10
- package/dist/seed/conversations-seed-data.js +75 -75
- package/dist/seed/coupon-usage-seed-data.js +12 -12
- package/dist/seed/coupons-seed-data.js +41 -41
- package/dist/seed/events-seed-data.js +105 -105
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -137
- package/dist/seed/grouped-listings-seed-data.js +79 -143
- package/dist/seed/history-seed-data.js +38 -38
- package/dist/seed/homepage-sections-seed-data.js +9 -9
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +1 -1
- package/dist/seed/manifest.js +4 -4
- package/dist/seed/notifications-seed-data.js +56 -56
- package/dist/seed/offers-seed-data.js +91 -91
- package/dist/seed/orders-seed-data.js +125 -106
- package/dist/seed/payouts-seed-data.js +42 -42
- package/dist/seed/products-auctions-seed-data.js +10 -2
- package/dist/seed/products-classifieds-seed-data.js +48 -5
- package/dist/seed/products-digital-codes-seed-data.js +48 -5
- package/dist/seed/products-live-items-seed-data.js +6 -1
- package/dist/seed/products-prize-draws-seed-data.js +58 -5
- package/dist/seed/products-standard-seed-data.js +40 -8
- package/dist/seed/reviews-seed-data.js +42 -23
- package/dist/seed/scammers-seed-data.js +22 -22
- package/dist/seed/sessions-seed-data.js +7 -7
- package/dist/seed/shipments-seed-data.js +10 -10
- package/dist/seed/site-settings-seed-data.js +82 -0
- package/dist/seed/store-addresses-seed-data.js +2 -2
- package/dist/seed/store-extensions-seed-data.js +93 -93
- package/dist/seed/stores-seed-data.js +2 -0
- package/dist/seed/support-tickets-seed-data.js +47 -47
- package/dist/seed/users-seed-data.js +55 -89
- package/dist/seed/wishlists-seed-data.js +22 -22
- package/dist/server.d.ts +2 -1
- package/dist/server.js +5 -5
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PageLoader.js +8 -3
- package/dist/ui/components/Select.d.ts +8 -1
- package/dist/ui/components/Select.js +2 -2
- package/dist/ui/components/SiteMark.d.ts +23 -0
- package/dist/ui/components/SiteMark.js +76 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/validation/schemas.d.ts +0 -2
- package/dist/validation/schemas.js +0 -2
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +27 -19
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
-
import { Alert, Badge, BulkActionBar, Button, Checkbox, Div, Modal, Row, Select, SideDrawer, Span, Stack, Text, Textarea } from "../../../ui";
|
|
5
|
+
import { Alert, Badge, BulkActionBar, Button, Checkbox, Div, Input, Modal, Row, Select, SideDrawer, Span, Stack, Text, Textarea } from "../../../ui";
|
|
6
6
|
import { StackedViewShell } from "../../../ui";
|
|
7
7
|
import { useBottomActions } from "../../layout";
|
|
8
8
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
9
9
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
+
import { sortBy } from "../../../constants/sort";
|
|
11
|
+
const SORT_OPTIONS = [
|
|
12
|
+
{ value: sortBy("createdAt", "DESC"), label: "Newest" },
|
|
13
|
+
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
14
|
+
{ value: sortBy("rating", "DESC"), label: "Highest rating" },
|
|
15
|
+
{ value: sortBy("rating", "ASC"), label: "Lowest rating" },
|
|
16
|
+
];
|
|
10
17
|
const __P = {
|
|
11
18
|
p3: "p-[var(--appkit-space-3)]",
|
|
12
19
|
p4: "p-[var(--appkit-space-4)]",
|
|
@@ -32,6 +39,8 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
32
39
|
// Filters
|
|
33
40
|
const [rating, setRating] = useState("");
|
|
34
41
|
const [replied, setReplied] = useState("");
|
|
42
|
+
const [sort, setSort] = useState(SORT_OPTIONS[0].value);
|
|
43
|
+
const [search, setSearch] = useState("");
|
|
35
44
|
const [page, setPage] = useState(1);
|
|
36
45
|
// Reply drawer
|
|
37
46
|
const [replyTarget, setReplyTarget] = useState(null);
|
|
@@ -101,7 +110,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
101
110
|
setLoading(true);
|
|
102
111
|
setError(null);
|
|
103
112
|
try {
|
|
104
|
-
const params = new URLSearchParams({ page: String(page), pageSize: "20" });
|
|
113
|
+
const params = new URLSearchParams({ page: String(page), pageSize: "20", sorts: sort });
|
|
105
114
|
if (rating)
|
|
106
115
|
params.set("rating", rating);
|
|
107
116
|
if (replied)
|
|
@@ -120,7 +129,19 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
120
129
|
finally {
|
|
121
130
|
setLoading(false);
|
|
122
131
|
}
|
|
123
|
-
}, [reviewsApiBase, rating, replied, page]);
|
|
132
|
+
}, [reviewsApiBase, rating, replied, sort, page]);
|
|
133
|
+
// The API route has no ?q= support (only rating/replied/sorts) — filter
|
|
134
|
+
// the fetched page client-side, same category of post-fetch narrowing the
|
|
135
|
+
// route itself already does for `replied`.
|
|
136
|
+
const visibleReviews = useMemo(() => {
|
|
137
|
+
const q = search.trim().toLowerCase();
|
|
138
|
+
if (!q)
|
|
139
|
+
return reviews;
|
|
140
|
+
return reviews.filter((r) => r.productTitle?.toLowerCase().includes(q) ||
|
|
141
|
+
r.userName?.toLowerCase().includes(q) ||
|
|
142
|
+
r.comment?.toLowerCase().includes(q) ||
|
|
143
|
+
r.title?.toLowerCase().includes(q));
|
|
144
|
+
}, [reviews, search]);
|
|
124
145
|
useEffect(() => { fetchReviews(); }, [fetchReviews]);
|
|
125
146
|
useEffect(() => { fetchReviewsRef.current = fetchReviews; }, [fetchReviews, fetchReviewsRef]);
|
|
126
147
|
const openReply = (review) => {
|
|
@@ -161,7 +182,7 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
161
182
|
},
|
|
162
183
|
] } } : {});
|
|
163
184
|
return (_jsxs(_Fragment, { children: [_jsx(StackedViewShell, { portal: "seller", title: "Reviews", sections: [
|
|
164
|
-
_jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { align: "center", gap: "3", wrap: true, children: [_jsx(Select, { value: rating, onChange: (e) => { setRating(e.target.value); setPage(1); }, "aria-label": "Filter by rating", options: [
|
|
185
|
+
_jsxs(Stack, { gap: "lg", children: [_jsxs(Row, { align: "center", gap: "3", wrap: true, children: [_jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: "Search product, reviewer, or comment\u2026", "aria-label": "Search reviews", className: "min-w-[220px]" }), _jsx(Select, { value: rating, onChange: (e) => { setRating(e.target.value); setPage(1); }, "aria-label": "Filter by rating", options: [
|
|
165
186
|
{ value: "", label: "All ratings" },
|
|
166
187
|
{ value: "5", label: "5 stars" },
|
|
167
188
|
{ value: "4", label: "4 stars" },
|
|
@@ -172,12 +193,12 @@ export function SellerReviewsView({ reviewsApiBase = SELLER_ENDPOINTS.REVIEWS, r
|
|
|
172
193
|
{ value: "", label: "All reply statuses" },
|
|
173
194
|
{ value: "true", label: "Store replied" },
|
|
174
195
|
{ value: "false", label: "Awaiting reply" },
|
|
175
|
-
] }), meta && (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] ml-auto", size: "sm", children: [meta.total, " review", meta.total !== 1 ? "s" : ""] }))] }), error && _jsx(Alert, { variant: "error", children: error }), selectedIds.size > 0 && (_jsx(BulkActionBar, { selectedCount: selectedIds.size, onClearSelection: clearSelection, actions: [
|
|
196
|
+
] }), _jsx(Select, { value: sort, onChange: (e) => { setSort(e.target.value); setPage(1); }, "aria-label": "Sort reviews", options: SORT_OPTIONS }), meta && (_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] ml-auto", size: "sm", children: [meta.total, " review", meta.total !== 1 ? "s" : ""] }))] }), error && _jsx(Alert, { variant: "error", children: error }), selectedIds.size > 0 && (_jsx(BulkActionBar, { selectedCount: selectedIds.size, onClearSelection: clearSelection, actions: [
|
|
176
197
|
{
|
|
177
198
|
id: "bulk-reply",
|
|
178
199
|
label: "Reply to selected",
|
|
179
200
|
onClick: () => setBulkReplyOpen(true),
|
|
180
201
|
},
|
|
181
|
-
] })), loading ? (_jsx(Div, { className: "text-center", padding: "y-xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Loading reviews\u2026" }) })) :
|
|
202
|
+
] })), loading ? (_jsx(Div, { className: "text-center", padding: "y-xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Loading reviews\u2026" }) })) : visibleReviews.length === 0 ? (_jsx(Div, { className: "text-center", padding: "y-3xl", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "No reviews found." }) })) : (_jsx(Stack, { gap: "md", children: visibleReviews.map((review) => (_jsx(Div, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "lg", padding: "md", children: _jsxs(Row, { align: "start", justify: "between", gap: "3", wrap: true, children: [_jsxs(Row, { align: "start", className: "flex-1 min-w-0", gap: "3", children: [_jsx(Checkbox, { checked: selectedIds.has(review.id), onChange: () => toggleSelected(review.id), "aria-label": "Select review" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsx(Text, { className: "truncate", weight: "medium", children: review.productTitle }), _jsxs(Row, { className: "mt-1", align: "center", gap: "sm", wrap: true, children: [_jsx(Stars, { rating: review.rating }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: ["by ", review.userName] }), review.verified && _jsx(Badge, { variant: "success", children: "Verified" }), statusBadge(review.status), _jsx(Badge, { variant: review.sellerReply ? "success" : "warning", children: review.sellerReply ? "Store replied" : "Awaiting store reply" })] }), review.title && _jsx(Text, { className: "mt-2", weight: "medium", children: review.title }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-secondary)] line-clamp-3", size: "sm", children: review.comment }), review.sellerReply && (_jsxs(Div, { className: "mt-2 pl-[0.75rem] border-l-2 border-[var(--appkit-color-primary)]", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "Store reply:" }), _jsx(Text, { size: "sm", children: review.sellerReply })] }))] })] }), _jsxs(Row, { className: "flex-shrink-0", gap: "sm", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => openReply(review), children: review.sellerReply ? "Edit Reply" : ACTIONS.STORE["reply-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setContestTarget(review), children: ACTIONS.STORE["contest-review"].label }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setFeedbackTarget(review), children: ACTIONS.STORE["buyer-feedback"].label })] })] }) }, review.id))) })), meta && meta.totalPages > 1 && (_jsxs(Row, { align: "center", justify: "center", gap: "sm", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: page <= 1 || loading, children: "Previous" }), _jsxs(Text, { size: "sm", children: ["Page ", meta.page, " of ", meta.totalPages] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setPage((p) => p + 1), disabled: !meta.hasMore || loading, children: "Next" })] }))] }, "reviews"),
|
|
182
203
|
] }), _jsx(SideDrawer, { isOpen: !!replyTarget, onClose: () => setReplyTarget(null), title: replyTarget?.sellerReply ? "Edit Reply" : "Reply to Review", mode: "create", children: _jsxs(Stack, { gap: "md", className: `${__P.p4}`, children: [replyTarget && (_jsxs(Div, { className: `${__P.p3} bg-[var(--appkit-color-surface-muted)]`, rounded: "default", children: [_jsx(Stars, { rating: replyTarget.rating }), _jsx(Text, { className: "mt-1", size: "sm", children: replyTarget.comment })] })), replyError && _jsx(Alert, { variant: "error", children: replyError }), _jsxs(Div, { children: [_jsx(Text, { className: "mb-1.5", size: "sm", weight: "medium", children: "Store reply" }), _jsx(Textarea, { rows: 5, value: replyText, onChange: (e) => setReplyText(e.target.value), maxLength: 1000, placeholder: "Write your response to this review\u2026" }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mt-1", size: "xs", align: "end", children: [replyText.length, "/1000"] })] }), _jsxs(Row, { justify: "end", gap: "sm", children: [_jsx(Button, { variant: "outline", onClick: () => setReplyTarget(null), disabled: replySaving, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: handleReplySave, disabled: replySaving || !replyText.trim(), isLoading: replySaving, children: replyTarget?.sellerReply ? "Update Reply" : "Post Reply" })] })] }) }), _jsx(Modal, { isOpen: bulkReplyOpen, onClose: () => setBulkReplyOpen(false), title: `Bulk reply to ${selectedIds.size} review${selectedIds.size === 1 ? "" : "s"}`, actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setBulkReplyOpen(false), disabled: bulkSaving, children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitBulkReply(), disabled: !bulkReplyText.trim() || bulkSaving, isLoading: bulkSaving, children: "Send reply" })] }), children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "The same reply will be posted on all selected reviews." }), _jsx(Textarea, { value: bulkReplyText, onChange: (e) => setBulkReplyText(e.target.value), rows: 5, placeholder: "Thanks for your review\u2026", label: "Reply" })] }) }), _jsx(Modal, { isOpen: !!contestTarget, onClose: () => setContestTarget(null), title: "Contest this review", actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setContestTarget(null), children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitContest(), disabled: !contestReason.trim(), children: "Submit" })] }), children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Flag this review for admin investigation. Provide a clear reason \u2014 fake, abusive, off-topic, etc." }), _jsx(Textarea, { value: contestReason, onChange: (e) => setContestReason(e.target.value), rows: 4, label: "Reason" })] }) }), _jsx(Modal, { isOpen: !!feedbackTarget, onClose: () => setFeedbackTarget(null), title: "Send feedback to buyer", actions: _jsxs(Row, { gap: "sm", children: [_jsx(Button, { variant: "ghost", onClick: () => setFeedbackTarget(null), children: "Cancel" }), _jsx(Button, { variant: "primary", onClick: () => void submitFeedback(), disabled: !feedbackText.trim(), children: "Send" })] }), children: _jsxs(Stack, { gap: "md", padding: "2xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: "Private message sent to the buyer's notification inbox. Does not appear on the public review." }), _jsx(Textarea, { value: feedbackText, onChange: (e) => setFeedbackText(e.target.value), rows: 4, label: "Feedback" })] }) })] }));
|
|
183
204
|
}
|
|
@@ -112,8 +112,6 @@ export { SellerAnalyticsAlertsView as StoreAnalyticsAlertsView } from "./SellerA
|
|
|
112
112
|
export type { SellerAnalyticsAlertsViewProps as StoreAnalyticsAlertsViewProps } from "./SellerAnalyticsAlertsView";
|
|
113
113
|
export { SellerStoreCategoriesView } from "./SellerStoreCategoriesView";
|
|
114
114
|
export type { SellerStoreCategoriesViewProps } from "./SellerStoreCategoriesView";
|
|
115
|
-
export { SellerTemplatesView } from "./SellerTemplatesView";
|
|
116
|
-
export type { SellerTemplatesViewProps } from "./SellerTemplatesView";
|
|
117
115
|
export { SellerPayoutMethodsView } from "./SellerPayoutMethodsView";
|
|
118
116
|
export type { SellerPayoutMethodsViewProps } from "./SellerPayoutMethodsView";
|
|
119
117
|
export { SellerShippingConfigsView } from "./SellerShippingConfigsView";
|
|
@@ -59,7 +59,6 @@ export { SellerGroupedListingsView as StoreGroupedListingsView } from "./SellerG
|
|
|
59
59
|
export { SellerAnalyticsAlertsView } from "./SellerAnalyticsAlertsView";
|
|
60
60
|
export { SellerAnalyticsAlertsView as StoreAnalyticsAlertsView } from "./SellerAnalyticsAlertsView";
|
|
61
61
|
export { SellerStoreCategoriesView } from "./SellerStoreCategoriesView";
|
|
62
|
-
export { SellerTemplatesView } from "./SellerTemplatesView";
|
|
63
62
|
export { SellerPayoutMethodsView } from "./SellerPayoutMethodsView";
|
|
64
63
|
export { SellerShippingConfigsView } from "./SellerShippingConfigsView";
|
|
65
64
|
export { SellerGoogleReviewsView } from "./SellerGoogleReviewsView";
|
|
@@ -65,7 +65,8 @@ const STANDARD_STEPS = [
|
|
|
65
65
|
{
|
|
66
66
|
step: "Pricing",
|
|
67
67
|
items: [
|
|
68
|
-
|
|
68
|
+
// audit-money-units-ok: instructs sellers there is NO paise conversion, not a stale reference
|
|
69
|
+
"Price — enter in Indian Rupees (₹). Stored as decimal rupees — no paise conversion.",
|
|
69
70
|
"isOnSale + salePrice — enable the sale badge and show a strikethrough price on the listing.",
|
|
70
71
|
"customFields — add any extra key/value pairs buyers find useful (e.g. PSA cert number, card number, print run).",
|
|
71
72
|
],
|
|
@@ -13,5 +13,5 @@ export function StoreProductsView({ storeSlug, labels = {}, renderProducts, rend
|
|
|
13
13
|
{ value: sortBy("createdAt", "ASC"), label: "Oldest" },
|
|
14
14
|
{ value: sortBy("price", "ASC"), label: "Price: Low to High" },
|
|
15
15
|
{ value: sortBy("price", "DESC"), label: "Price: High to Low" },
|
|
16
|
-
],
|
|
16
|
+
], wrapperClassName: "min-w-44" })), renderFilters: renderFilters, renderActiveFilters: renderActiveFilters, renderBulkActions: () => renderViewToggle?.(viewMode, setViewMode) ?? null, renderTable: () => renderProducts(items, isLoading), renderPagination: () => renderPagination?.(total) ?? null, total: total, isLoading: isLoading }) }));
|
|
17
17
|
}
|
|
@@ -75,6 +75,12 @@ export interface StoreDocument extends BaseDocument {
|
|
|
75
75
|
isFeatured?: boolean;
|
|
76
76
|
isVacationMode?: boolean;
|
|
77
77
|
vacationMessage?: string;
|
|
78
|
+
/** Internal admin note, never shown to the store owner or the public. */
|
|
79
|
+
adminNotes?: string;
|
|
80
|
+
/** Admin "verified store" badge — distinct from `status`/`isPublic`. */
|
|
81
|
+
isVerified?: boolean;
|
|
82
|
+
/** Reason recorded when `status` is set to "suspended". */
|
|
83
|
+
suspensionReason?: string;
|
|
78
84
|
stats?: {
|
|
79
85
|
totalProducts: number;
|
|
80
86
|
itemsSold: number;
|
|
@@ -158,6 +164,11 @@ export declare const STORE_FIELDS: {
|
|
|
158
164
|
readonly STATS: "stats";
|
|
159
165
|
readonly CREATED_AT: "createdAt";
|
|
160
166
|
readonly UPDATED_AT: "updatedAt";
|
|
167
|
+
readonly ADMIN_NOTES: "adminNotes";
|
|
168
|
+
readonly IS_FEATURED: "isFeatured";
|
|
169
|
+
readonly IS_VERIFIED: "isVerified";
|
|
170
|
+
readonly SUSPENSION_REASON: "suspensionReason";
|
|
171
|
+
readonly CAPABILITIES: "capabilities";
|
|
161
172
|
readonly STATUS_VALUES: {
|
|
162
173
|
readonly PENDING: "pending";
|
|
163
174
|
readonly ACTIVE: "active";
|
|
@@ -35,6 +35,11 @@ export const STORE_FIELDS = {
|
|
|
35
35
|
STATS: "stats",
|
|
36
36
|
CREATED_AT: "createdAt",
|
|
37
37
|
UPDATED_AT: "updatedAt",
|
|
38
|
+
ADMIN_NOTES: "adminNotes",
|
|
39
|
+
IS_FEATURED: "isFeatured",
|
|
40
|
+
IS_VERIFIED: "isVerified",
|
|
41
|
+
SUSPENSION_REASON: "suspensionReason",
|
|
42
|
+
CAPABILITIES: "capabilities",
|
|
38
43
|
STATUS_VALUES: {
|
|
39
44
|
PENDING: "pending",
|
|
40
45
|
ACTIVE: "active",
|
|
@@ -458,10 +458,10 @@ export declare const storeFirestoreSchema: z.ZodObject<{
|
|
|
458
458
|
shippingPolicy?: string | undefined;
|
|
459
459
|
isVacationMode?: boolean | undefined;
|
|
460
460
|
vacationMessage?: string | undefined;
|
|
461
|
+
capabilities?: string[] | undefined;
|
|
461
462
|
vacationReturnDate?: string | Date | z.objectOutputType<{
|
|
462
463
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
463
464
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
464
|
-
capabilities?: string[] | undefined;
|
|
465
465
|
customCommissionRate?: number | undefined;
|
|
466
466
|
googleReviews?: {
|
|
467
467
|
enabled: boolean;
|
|
@@ -543,10 +543,10 @@ export declare const storeFirestoreSchema: z.ZodObject<{
|
|
|
543
543
|
shippingPolicy?: string | undefined;
|
|
544
544
|
isVacationMode?: boolean | undefined;
|
|
545
545
|
vacationMessage?: string | undefined;
|
|
546
|
+
capabilities?: string[] | undefined;
|
|
546
547
|
vacationReturnDate?: string | Date | z.objectInputType<{
|
|
547
548
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
548
549
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
549
|
-
capabilities?: string[] | undefined;
|
|
550
550
|
customCommissionRate?: number | undefined;
|
|
551
551
|
googleReviews?: {
|
|
552
552
|
enabled: boolean;
|
|
@@ -9,6 +9,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
|
|
|
9
9
|
description: z.ZodOptional<z.ZodString>;
|
|
10
10
|
href: z.ZodOptional<z.ZodString>;
|
|
11
11
|
order: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
phase: z.ZodDefault<z.ZodNumber>;
|
|
12
13
|
isActive: z.ZodDefault<z.ZodBoolean>;
|
|
13
14
|
adminOnly: z.ZodDefault<z.ZodBoolean>;
|
|
14
15
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -17,6 +18,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
|
|
|
17
18
|
isActive: boolean;
|
|
18
19
|
groupKey: string;
|
|
19
20
|
pageKey: string;
|
|
21
|
+
phase: number;
|
|
20
22
|
groupLabel: string;
|
|
21
23
|
pageLabel: string;
|
|
22
24
|
adminOnly: boolean;
|
|
@@ -32,6 +34,7 @@ export declare const checklistItemCreateSchema: z.ZodObject<{
|
|
|
32
34
|
description?: string | undefined;
|
|
33
35
|
order?: number | undefined;
|
|
34
36
|
isActive?: boolean | undefined;
|
|
37
|
+
phase?: number | undefined;
|
|
35
38
|
adminOnly?: boolean | undefined;
|
|
36
39
|
}>;
|
|
37
40
|
export declare const checklistItemUpdateSchema: z.ZodObject<{
|
|
@@ -43,6 +46,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
|
|
|
43
46
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
44
47
|
href: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
45
48
|
order: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
49
|
+
phase: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
46
50
|
isActive: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
47
51
|
adminOnly: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
48
52
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -53,6 +57,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
|
|
|
53
57
|
isActive?: boolean | undefined;
|
|
54
58
|
groupKey?: string | undefined;
|
|
55
59
|
pageKey?: string | undefined;
|
|
60
|
+
phase?: number | undefined;
|
|
56
61
|
groupLabel?: string | undefined;
|
|
57
62
|
pageLabel?: string | undefined;
|
|
58
63
|
adminOnly?: boolean | undefined;
|
|
@@ -64,6 +69,7 @@ export declare const checklistItemUpdateSchema: z.ZodObject<{
|
|
|
64
69
|
isActive?: boolean | undefined;
|
|
65
70
|
groupKey?: string | undefined;
|
|
66
71
|
pageKey?: string | undefined;
|
|
72
|
+
phase?: number | undefined;
|
|
67
73
|
groupLabel?: string | undefined;
|
|
68
74
|
pageLabel?: string | undefined;
|
|
69
75
|
adminOnly?: boolean | undefined;
|
|
@@ -11,6 +11,7 @@ const checklistItemBaseSchema = z.object({
|
|
|
11
11
|
description: z.string().max(2000).optional(),
|
|
12
12
|
href: z.string().max(300).optional(),
|
|
13
13
|
order: z.number().int().min(0).default(0),
|
|
14
|
+
phase: z.number().int().min(1).default(1),
|
|
14
15
|
isActive: z.boolean().default(true),
|
|
15
16
|
adminOnly: z.boolean().default(false),
|
|
16
17
|
});
|
|
@@ -27,6 +28,7 @@ export async function createChecklistItem(input) {
|
|
|
27
28
|
description: input.description,
|
|
28
29
|
href: input.href,
|
|
29
30
|
order: input.order ?? 0,
|
|
31
|
+
phase: input.phase ?? 1,
|
|
30
32
|
isActive: input.isActive ?? true,
|
|
31
33
|
adminOnly: input.adminOnly ?? false,
|
|
32
34
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { Card, Stack, Text } from "../../../ui";
|
|
4
|
+
import { Card, Details, Div, Stack, Summary, Text } from "../../../ui";
|
|
5
5
|
import { MediaImage } from "../../media/MediaImage";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
@@ -18,5 +18,16 @@ export function AdminTesterFeedbackIssuesView() {
|
|
|
18
18
|
return _jsx(Text, { color: "muted", children: "Loading issues\u2026" });
|
|
19
19
|
if (issues.length === 0)
|
|
20
20
|
return _jsx(Text, { color: "muted", children: "No \"No\" answers recorded yet." });
|
|
21
|
-
|
|
21
|
+
const byPhase = new Map();
|
|
22
|
+
for (const issue of issues) {
|
|
23
|
+
const phase = issue.phase ?? 0;
|
|
24
|
+
if (!byPhase.has(phase))
|
|
25
|
+
byPhase.set(phase, []);
|
|
26
|
+
byPhase.get(phase).push(issue);
|
|
27
|
+
}
|
|
28
|
+
const sortedPhases = Array.from(byPhase.keys()).sort((a, b) => a - b);
|
|
29
|
+
return (_jsx(Stack, { gap: "md", children: sortedPhases.map((phase) => {
|
|
30
|
+
const phaseIssues = byPhase.get(phase);
|
|
31
|
+
return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" }))] }) }, issue.id))) }) })] }, phase));
|
|
32
|
+
}) }));
|
|
22
33
|
}
|
|
@@ -59,12 +59,13 @@ export function AdminTesterFeedbackListView(_props) {
|
|
|
59
59
|
sortOptions: [
|
|
60
60
|
{ value: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.CREATED_AT, "DESC"), label: "Newest" },
|
|
61
61
|
{ value: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.CREATED_AT, "ASC"), label: "Oldest" },
|
|
62
|
+
{ value: sortBy(TESTER_CHECKLIST_RESPONSE_FIELDS.PHASE, "ASC"), label: "Phase" },
|
|
62
63
|
],
|
|
63
64
|
columns: COLUMNS,
|
|
64
65
|
mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
|
|
65
66
|
id: toStringValue(item.id, `feedback-${index}`),
|
|
66
67
|
primary: toStringValue(item.testerDisplayName, "Unknown tester"),
|
|
67
|
-
secondary:
|
|
68
|
+
secondary: `Phase ${toStringValue(item.phase, "?")} — ${toStringValue(item.groupKey, "")} / ${toStringValue(item.pageKey, "")}${item.comment ? " — " + toStringValue(item.comment, "") : ""}`,
|
|
68
69
|
answer: toStringValue(item.answer, ""),
|
|
69
70
|
status: toStringValue(item.status, "new"),
|
|
70
71
|
updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
|
|
@@ -17,14 +17,18 @@ export function AdminTesterFeedbackReportView() {
|
|
|
17
17
|
return _jsx(Text, { color: "muted", children: "Loading report\u2026" });
|
|
18
18
|
const itemCoverage = query.data?.itemCoverage ?? [];
|
|
19
19
|
const totals = query.data?.totals ?? { totalAnswered: 0, totalYes: 0, totalNo: 0 };
|
|
20
|
-
const
|
|
20
|
+
const byPhase = new Map();
|
|
21
21
|
for (const item of itemCoverage) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const phase = item.phase ?? 0;
|
|
23
|
+
if (!byPhase.has(phase)) {
|
|
24
|
+
byPhase.set(phase, { groupLabel: `Phase ${phase}`, yes: 0, no: 0 });
|
|
24
25
|
}
|
|
25
|
-
const entry =
|
|
26
|
+
const entry = byPhase.get(phase);
|
|
26
27
|
entry.yes += item.yesCount;
|
|
27
28
|
entry.no += item.noCount;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
const sortedPhaseData = Array.from(byPhase.entries())
|
|
31
|
+
.sort(([a], [b]) => a - b)
|
|
32
|
+
.map(([, datum]) => datum);
|
|
33
|
+
return (_jsx(Stack, { gap: "lg", children: _jsx(TesterFeedbackChart, { data: sortedPhaseData, totalCases: itemCoverage.length, totalAnswered: totals.totalAnswered, totalYes: totals.totalYes, totalNo: totals.totalNo }) }));
|
|
30
34
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
5
|
import { Alert, Details, Div, FieldInput, Heading, Stack, Summary, Text } from "../../../ui";
|
|
@@ -15,6 +15,21 @@ function matchesQuery(item, query) {
|
|
|
15
15
|
const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel} ${item.href ?? ""}`.toLowerCase();
|
|
16
16
|
return haystack.includes(query.toLowerCase());
|
|
17
17
|
}
|
|
18
|
+
function TesterChecklistPageSection({ page, testerDisplayName, onAnswer, onSaveNote }) {
|
|
19
|
+
const answeredCount = page.items.filter((i) => i.answer).length;
|
|
20
|
+
return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { children: [page.pageLabel, " (", answeredCount, "/", page.items.length, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: page.items
|
|
21
|
+
.slice()
|
|
22
|
+
.sort((a, b) => a.order - b.order)
|
|
23
|
+
.map((item) => (_jsx(TesterChecklistStepRow, { testerDisplayName: testerDisplayName, item: {
|
|
24
|
+
checklistItemId: item.id,
|
|
25
|
+
label: item.label,
|
|
26
|
+
description: item.description,
|
|
27
|
+
href: item.href,
|
|
28
|
+
answer: item.answer,
|
|
29
|
+
comment: item.comment,
|
|
30
|
+
screenshotUrl: item.screenshotUrl,
|
|
31
|
+
}, onAnswer: onAnswer, onSaveNote: onSaveNote }, item.id))) }) })] }));
|
|
32
|
+
}
|
|
18
33
|
export function TesterHubView({ sandboxExpiresAt }) {
|
|
19
34
|
const { user } = useSession();
|
|
20
35
|
const queryClient = useQueryClient();
|
|
@@ -35,38 +50,36 @@ export function TesterHubView({ sandboxExpiresAt }) {
|
|
|
35
50
|
if (!user?.isTester && !isAdminUser(user)) {
|
|
36
51
|
return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers (and admins). Contact an admin if you believe this is a mistake." }));
|
|
37
52
|
}
|
|
53
|
+
// Items arrive pre-sorted by catalog `order` (listActive() orders by it).
|
|
38
54
|
const items = query.data?.items ?? [];
|
|
39
55
|
const filtered = items.filter((item) => matchesQuery(item, search));
|
|
40
|
-
const
|
|
56
|
+
const phases = new Map();
|
|
41
57
|
for (const item of filtered) {
|
|
42
|
-
if (!
|
|
58
|
+
if (!phases.has(item.phase))
|
|
59
|
+
phases.set(item.phase, new Map());
|
|
60
|
+
const groups = phases.get(item.phase);
|
|
61
|
+
if (!groups.has(item.groupKey))
|
|
43
62
|
groups.set(item.groupKey, { groupLabel: item.groupLabel, pages: new Map() });
|
|
44
|
-
}
|
|
45
63
|
const group = groups.get(item.groupKey);
|
|
46
|
-
if (!group.pages.has(item.pageKey))
|
|
64
|
+
if (!group.pages.has(item.pageKey))
|
|
47
65
|
group.pages.set(item.pageKey, { pageLabel: item.pageLabel, items: [] });
|
|
48
|
-
}
|
|
49
66
|
group.pages.get(item.pageKey).items.push(item);
|
|
50
67
|
}
|
|
68
|
+
const sortedPhaseNumbers = Array.from(phases.keys()).sort((a, b) => a - b);
|
|
51
69
|
const handleAnswer = (checklistItemId, answer) => {
|
|
52
70
|
upsertMutation.mutate({ checklistItemId, patch: { answer } });
|
|
53
71
|
};
|
|
54
72
|
const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
|
|
55
73
|
await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
|
|
56
74
|
};
|
|
57
|
-
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
answer: item.answer,
|
|
68
|
-
comment: item.comment,
|
|
69
|
-
screenshotUrl: item.screenshotUrl,
|
|
70
|
-
}, onAnswer: handleAnswer, onSaveNote: handleSaveNote }, item.id))) }) })] }, pageKey));
|
|
71
|
-
})] }, groupKey)))] }));
|
|
75
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), query.isLoading && _jsx(Text, { color: "muted", children: "Loading checklist\u2026" }), sortedPhaseNumbers.map((phaseNumber) => {
|
|
76
|
+
const groups = phases.get(phaseNumber);
|
|
77
|
+
const phaseItemCount = Array.from(groups.values())
|
|
78
|
+
.flatMap((g) => Array.from(g.pages.values()))
|
|
79
|
+
.reduce((sum, page) => sum + page.items.length, 0);
|
|
80
|
+
const phaseAnswered = Array.from(groups.values())
|
|
81
|
+
.flatMap((g) => Array.from(g.pages.values()))
|
|
82
|
+
.reduce((sum, page) => sum + page.items.filter((i) => i.answer).length, 0);
|
|
83
|
+
return (_jsxs(Details, { tone: "card", defaultOpen: false, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phaseNumber, " (", phaseAnswered, "/", phaseItemCount, " answered)"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "md", children: Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 4, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => (_jsx(TesterChecklistPageSection, { page: page, testerDisplayName: user?.displayName ?? user?.email ?? "tester", onAnswer: handleAnswer, onSaveNote: handleSaveNote }, pageKey)))] }, groupKey))) }) })] }, phaseNumber));
|
|
84
|
+
})] }));
|
|
72
85
|
}
|
|
@@ -48,6 +48,7 @@ TesterChecklistItemRepository.SIEVE_FIELDS = {
|
|
|
48
48
|
pageKey: { canFilter: true, canSort: true },
|
|
49
49
|
label: { canFilter: true, canSort: false },
|
|
50
50
|
order: { canFilter: true, canSort: true },
|
|
51
|
+
phase: { canFilter: true, canSort: true },
|
|
51
52
|
isActive: { canFilter: true, canSort: false },
|
|
52
53
|
searchTokens: { canFilter: true, canSort: false },
|
|
53
54
|
createdAt: { canFilter: true, canSort: true },
|
|
@@ -7,6 +7,7 @@ export interface UpsertResponseInput {
|
|
|
7
7
|
checklistItemId: string;
|
|
8
8
|
groupKey: string;
|
|
9
9
|
pageKey: string;
|
|
10
|
+
phase: number;
|
|
10
11
|
answer?: TesterAnswer | null;
|
|
11
12
|
comment?: string;
|
|
12
13
|
screenshotUrl?: string;
|
|
@@ -14,14 +15,22 @@ export interface UpsertResponseInput {
|
|
|
14
15
|
export interface ChecklistItemCoverage {
|
|
15
16
|
checklistItemId: string;
|
|
16
17
|
groupKey: string;
|
|
18
|
+
groupLabel: string;
|
|
17
19
|
pageKey: string;
|
|
20
|
+
pageLabel: string;
|
|
21
|
+
phase: number;
|
|
18
22
|
yesCount: number;
|
|
19
23
|
noCount: number;
|
|
20
24
|
totalAnswered: number;
|
|
21
25
|
}
|
|
26
|
+
export type CoverageIssue = TesterChecklistResponseDocument & {
|
|
27
|
+
groupLabel: string;
|
|
28
|
+
pageLabel: string;
|
|
29
|
+
label: string;
|
|
30
|
+
};
|
|
22
31
|
export interface CoverageReport {
|
|
23
32
|
itemCoverage: ChecklistItemCoverage[];
|
|
24
|
-
issues:
|
|
33
|
+
issues: CoverageIssue[];
|
|
25
34
|
totals: {
|
|
26
35
|
totalAnswered: number;
|
|
27
36
|
totalYes: number;
|
|
@@ -36,14 +45,19 @@ export declare class TesterChecklistResponseRepository extends BaseRepository<Te
|
|
|
36
45
|
listForTester(testerId: string): Promise<TesterChecklistResponseDocument[]>;
|
|
37
46
|
list(model: SieveModel): Promise<FirebaseSieveResult<TesterChecklistResponseDocument>>;
|
|
38
47
|
markReviewed(id: string): Promise<TesterChecklistResponseDocument>;
|
|
39
|
-
/**
|
|
48
|
+
/** Joins responses against the checklist catalog for human labels
|
|
49
|
+
* (groupLabel/pageLabel) — small tables (testers x cases, and the
|
|
50
|
+
* ~300-item catalog), so the extra read is cheap. `phase` itself is
|
|
51
|
+
* already denormalized onto the response doc, no join needed for it. */
|
|
40
52
|
getCoverageReport(): Promise<CoverageReport>;
|
|
41
53
|
/**
|
|
42
54
|
* Markdown dump of every answered case, joined against the checklist item
|
|
43
55
|
* catalog for the human-readable label/href — optimized for a future dev
|
|
44
56
|
* (or Claude session) to read directly and go fix the reported issues.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
57
|
+
* Grouped by phase (ascending), then group, then page — mirrors how the
|
|
58
|
+
* Tester Hub itself is organized. Mirrors
|
|
59
|
+
* appkit/scripts/export-tester-feedback.mjs's CLI output exactly; keep
|
|
60
|
+
* the two in sync.
|
|
47
61
|
*/
|
|
48
62
|
getMarkdownReport(siteOrigin: string): Promise<string>;
|
|
49
63
|
}
|