@mohasinac/appkit 4.1.1 → 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.
Files changed (187) hide show
  1. package/dist/_internal/client/features/maintenance/index.d.ts +2 -0
  2. package/dist/_internal/client/features/maintenance/index.js +1 -0
  3. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.d.ts +9 -0
  4. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +55 -0
  5. package/dist/_internal/server/features/auctions/index.d.ts +1 -1
  6. package/dist/_internal/server/features/auctions/index.js +1 -1
  7. package/dist/_internal/server/features/auctions/service.js +2 -2
  8. package/dist/_internal/server/features/maintenance/cloud-logs-data.d.ts +50 -0
  9. package/dist/_internal/server/features/maintenance/cloud-logs-data.js +152 -0
  10. package/dist/_internal/server/jobs/core/onOrderCreate.d.ts +4 -12
  11. package/dist/_internal/server/jobs/core/onOrderCreate.js +3 -3
  12. package/dist/_internal/server/jobs/core/onProductWrite.js +14 -2
  13. package/dist/_internal/shared/features/auctions/config.d.ts +1 -1
  14. package/dist/_internal/shared/features/auctions/config.js +1 -1
  15. package/dist/_internal/shared/features/auctions/schema.js +1 -1
  16. package/dist/_internal/shared/features/cart/schema.js +2 -2
  17. package/dist/_internal/shared/features/events/schema.js +1 -1
  18. package/dist/_internal/shared/features/orders/schema.js +1 -1
  19. package/dist/_internal/shared/features/products/schema.js +3 -3
  20. package/dist/_internal/shared/features/promotions/schema.js +3 -3
  21. package/dist/_internal/shared/fees/calculator.d.ts +6 -7
  22. package/dist/client.d.ts +4 -4
  23. package/dist/client.js +4 -2
  24. package/dist/constants/api-endpoints.d.ts +9 -6
  25. package/dist/constants/api-endpoints.js +3 -2
  26. package/dist/contracts/client-payment-gateway.d.ts +1 -2
  27. package/dist/features/about/components/AboutView.d.ts +6 -16
  28. package/dist/features/about/components/AboutView.js +3 -3
  29. package/dist/features/about/index.d.ts +2 -1
  30. package/dist/features/about/schemas/firestore.d.ts +47 -0
  31. package/dist/features/about/schemas/firestore.js +6 -0
  32. package/dist/features/about/schemas/index.d.ts +210 -0
  33. package/dist/features/about/schemas/index.js +45 -0
  34. package/dist/features/account/hooks/useProfile.js +3 -0
  35. package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +6 -0
  36. package/dist/features/admin/components/AdminCarouselGroupEditorView.js +50 -0
  37. package/dist/features/admin/components/AdminCartsView.js +16 -3
  38. package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
  39. package/dist/features/admin/components/AdminCouponsView.js +14 -4
  40. package/dist/features/admin/components/AdminEmployeeEditorView.js +1 -42
  41. package/dist/features/admin/components/AdminMediaView.js +1 -1
  42. package/dist/features/admin/components/AdminNotificationsView.js +31 -19
  43. package/dist/features/admin/components/AdminOrdersView.js +7 -0
  44. package/dist/features/admin/components/AdminPayoutsView.js +18 -3
  45. package/dist/features/admin/components/AdminProductsView.js +17 -2
  46. package/dist/features/admin/components/AdminReviewsView.js +16 -2
  47. package/dist/features/admin/components/AdminScammersView.js +7 -0
  48. package/dist/features/admin/components/AdminSectionsView.js +4 -0
  49. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  50. package/dist/features/admin/components/AdminSiteSettingsView.js +44 -2
  51. package/dist/features/admin/components/AdminStoreEditorView.d.ts +2 -1
  52. package/dist/features/admin/components/AdminStoreEditorView.js +4 -4
  53. package/dist/features/admin/components/AdminStoresView.js +1 -1
  54. package/dist/features/admin/components/AdminSupportTicketDetailView.js +23 -5
  55. package/dist/features/admin/components/AdminSupportTicketsView.js +20 -2
  56. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +4 -1
  57. package/dist/features/admin/components/AdminTesterChecklistView.js +8 -0
  58. package/dist/features/admin/components/index.d.ts +2 -0
  59. package/dist/features/admin/components/index.js +1 -0
  60. package/dist/features/admin/constants/filter-tabs.d.ts +55 -41
  61. package/dist/features/admin/constants/filter-tabs.js +55 -27
  62. package/dist/features/admin/schemas/firestore.d.ts +5 -0
  63. package/dist/features/auctions/actions/bid-actions.js +1 -1
  64. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
  65. package/dist/features/auctions/components/AuctionsListView.js +8 -1
  66. package/dist/features/auth/components/LoginForm.js +2 -2
  67. package/dist/features/auth/components/RegisterForm.js +2 -2
  68. package/dist/features/auth/permissions/constants.d.ts +16 -0
  69. package/dist/features/auth/permissions/constants.js +53 -0
  70. package/dist/features/auth/schemas/firestore.js +1 -0
  71. package/dist/features/blog/components/BlogFilters.js +1 -1
  72. package/dist/features/cart/schemas/firestore.d.ts +1 -1
  73. package/dist/features/categories/schemas/firestore.d.ts +1 -1
  74. package/dist/features/consultation/components/ConsultationForm.js +1 -1
  75. package/dist/features/events/components/EventFilters.js +2 -0
  76. package/dist/features/homepage/components/WelcomeSection.js +2 -2
  77. package/dist/features/layout/TitleBarLayout.d.ts +2 -2
  78. package/dist/features/layout/TitleBarLayout.js +4 -4
  79. package/dist/features/media/AvatarUpload.js +7 -3
  80. package/dist/features/media/MediaVideo.js +1 -1
  81. package/dist/features/media/finalize.d.ts +9 -13
  82. package/dist/features/media/finalize.js +23 -74
  83. package/dist/features/media/server.d.ts +1 -1
  84. package/dist/features/media/server.js +1 -1
  85. package/dist/features/orders/hooks/useOrders.js +16 -4
  86. package/dist/features/orders/schemas/firestore.d.ts +5 -5
  87. package/dist/features/payments/schemas/firestore.d.ts +2 -2
  88. package/dist/features/pre-orders/components/PreOrdersListView.js +7 -0
  89. package/dist/features/products/components/ArtStickersListView.js +5 -0
  90. package/dist/features/products/components/ProductDetailPageView.d.ts +7 -1
  91. package/dist/features/products/components/ProductForm.js +14 -1
  92. package/dist/features/products/components/ProductsIndexPageView.js +7 -0
  93. package/dist/features/products/constants/action-defs.d.ts +1 -0
  94. package/dist/features/products/constants/action-defs.js +1 -0
  95. package/dist/features/products/schemas/firestore.d.ts +1 -1
  96. package/dist/features/products/schemas/product-templates.d.ts +1 -1
  97. package/dist/features/products/types/index.d.ts +4 -0
  98. package/dist/features/scams/schemas/index.js +2 -2
  99. package/dist/features/search/components/Search.js +3 -3
  100. package/dist/features/seller/components/SellerAuctionsView.js +27 -2
  101. package/dist/features/seller/components/SellerOrdersView.js +1 -1
  102. package/dist/features/seller/components/SellerPayoutsView.js +24 -10
  103. package/dist/features/seller/components/SellerPreOrdersView.js +12 -4
  104. package/dist/features/seller/components/SellerPrizeDrawsView.js +23 -5
  105. package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
  106. package/dist/features/seller/components/SellerProductShell.js +1 -1
  107. package/dist/features/seller/components/SellerProductsView.js +8 -6
  108. package/dist/features/seller/components/SellerReviewsView.js +27 -6
  109. package/dist/features/seller/components/index.d.ts +0 -2
  110. package/dist/features/seller/components/index.js +0 -1
  111. package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
  112. package/dist/features/stores/components/StoreProductsView.js +1 -1
  113. package/dist/features/stores/schemas/firestore.d.ts +11 -0
  114. package/dist/features/stores/schemas/firestore.js +5 -0
  115. package/dist/features/stores/schemas/index.d.ts +2 -2
  116. package/dist/features/tester/actions/checklist-item-actions.d.ts +6 -0
  117. package/dist/features/tester/actions/checklist-item-actions.js +2 -0
  118. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +13 -2
  119. package/dist/features/tester/components/AdminTesterFeedbackListView.js +2 -1
  120. package/dist/features/tester/components/AdminTesterFeedbackReportView.js +9 -5
  121. package/dist/features/tester/components/TesterHubView.js +34 -21
  122. package/dist/features/tester/repository/tester-checklist-item.repository.js +1 -0
  123. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +18 -4
  124. package/dist/features/tester/repository/tester-checklist-response.repository.js +43 -8
  125. package/dist/features/tester/schemas/firestore.d.ts +8 -3
  126. package/dist/features/tester/schemas/firestore.js +6 -0
  127. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +21 -9
  128. package/dist/features/tester/utils/phases.d.ts +31 -0
  129. package/dist/features/tester/utils/phases.js +46 -0
  130. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +1 -1
  131. package/dist/features/whatsapp-bot/types/index.d.ts +1 -1
  132. package/dist/features/wishlist/types/index.d.ts +3 -0
  133. package/dist/index.d.ts +11 -4
  134. package/dist/index.js +15 -7
  135. package/dist/next/components/NotFoundView.js +2 -1
  136. package/dist/next/routing/route-map.d.ts +4 -12
  137. package/dist/next/routing/route-map.js +2 -4
  138. package/dist/providers/db-firebase/admin.d.ts +1 -0
  139. package/dist/providers/db-firebase/admin.js +1 -1
  140. package/dist/schemas/registry.d.ts +2 -2
  141. package/dist/seed/addresses-seed-data.js +18 -18
  142. package/dist/seed/bids-seed-data.js +4 -4
  143. package/dist/seed/blog-posts-seed-data.js +82 -82
  144. package/dist/seed/carousel-slides-seed-data.js +14 -10
  145. package/dist/seed/carousels-seed-data.js +3 -3
  146. package/dist/seed/cart-seed-data.js +92 -92
  147. package/dist/seed/claimed-coupons-seed-data.js +10 -10
  148. package/dist/seed/conversations-seed-data.js +75 -75
  149. package/dist/seed/coupon-usage-seed-data.js +12 -12
  150. package/dist/seed/coupons-seed-data.js +41 -41
  151. package/dist/seed/events-seed-data.js +105 -105
  152. package/dist/seed/grouped-listings-seed-data.d.ts +2 -137
  153. package/dist/seed/grouped-listings-seed-data.js +79 -143
  154. package/dist/seed/history-seed-data.js +38 -38
  155. package/dist/seed/homepage-sections-seed-data.js +9 -9
  156. package/dist/seed/notifications-seed-data.js +56 -56
  157. package/dist/seed/offers-seed-data.js +91 -91
  158. package/dist/seed/orders-seed-data.js +121 -106
  159. package/dist/seed/payouts-seed-data.js +42 -42
  160. package/dist/seed/products-auctions-seed-data.js +10 -2
  161. package/dist/seed/products-standard-seed-data.js +40 -8
  162. package/dist/seed/reviews-seed-data.js +42 -23
  163. package/dist/seed/scammers-seed-data.js +22 -22
  164. package/dist/seed/sessions-seed-data.js +7 -7
  165. package/dist/seed/shipments-seed-data.js +10 -10
  166. package/dist/seed/site-settings-seed-data.js +82 -0
  167. package/dist/seed/store-addresses-seed-data.js +2 -2
  168. package/dist/seed/store-extensions-seed-data.js +93 -93
  169. package/dist/seed/stores-seed-data.js +2 -0
  170. package/dist/seed/support-tickets-seed-data.js +47 -47
  171. package/dist/seed/users-seed-data.js +55 -89
  172. package/dist/seed/wishlists-seed-data.js +22 -22
  173. package/dist/server.d.ts +2 -1
  174. package/dist/server.js +5 -5
  175. package/dist/styles.css +1 -1
  176. package/dist/tailwind-utilities.css +1 -1
  177. package/dist/ui/components/PageLoader.js +8 -3
  178. package/dist/ui/components/Select.d.ts +8 -1
  179. package/dist/ui/components/Select.js +2 -2
  180. package/dist/ui/components/SiteMark.d.ts +23 -0
  181. package/dist/ui/components/SiteMark.js +76 -0
  182. package/dist/ui/index.d.ts +2 -0
  183. package/dist/ui/index.js +1 -0
  184. package/dist/validation/schemas.d.ts +0 -2
  185. package/dist/validation/schemas.js +0 -2
  186. package/package.json +1 -1
  187. package/scripts/seed-cli.mjs +17 -15
@@ -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" }) })) : reviews.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: reviews.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"),
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
- "Price enter in Indian Rupees (₹). Stored in paise internally.",
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
- ], className: "min-w-44" })), renderFilters: renderFilters, renderActiveFilters: renderActiveFilters, renderBulkActions: () => renderViewToggle?.(viewMode, setViewMode) ?? null, renderTable: () => renderProducts(items, isLoading), renderPagination: () => renderPagination?.(total) ?? null, total: total, isLoading: isLoading }) }));
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
- return (_jsx(Stack, { gap: "sm", children: issues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { weight: "semibold", children: [issue.groupKey, " / ", issue.pageKey] }), _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))) }));
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: `${toStringValue(item.groupKey, "")} / ${toStringValue(item.pageKey, "")}${item.comment ? " — " + toStringValue(item.comment, "") : ""}`,
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 byGroup = new Map();
20
+ const byPhase = new Map();
21
21
  for (const item of itemCoverage) {
22
- if (!byGroup.has(item.groupKey)) {
23
- byGroup.set(item.groupKey, { groupLabel: item.groupKey, yes: 0, no: 0 });
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 = byGroup.get(item.groupKey);
26
+ const entry = byPhase.get(phase);
26
27
  entry.yes += item.yesCount;
27
28
  entry.no += item.noCount;
28
29
  }
29
- return (_jsx(Stack, { gap: "lg", children: _jsx(TesterFeedbackChart, { data: Array.from(byGroup.values()), totalCases: itemCoverage.length, totalAnswered: totals.totalAnswered, totalYes: totals.totalYes, totalNo: totals.totalNo }) }));
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 groups = new Map();
56
+ const phases = new Map();
41
57
  for (const item of filtered) {
42
- if (!groups.has(item.groupKey)) {
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" }), Array.from(groups.entries()).map(([groupKey, group]) => (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 3, children: group.groupLabel }), Array.from(group.pages.entries()).map(([pageKey, page]) => {
58
- const answeredCount = page.items.filter((i) => i.answer).length;
59
- 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
60
- .slice()
61
- .sort((a, b) => a.order - b.order)
62
- .map((item) => (_jsx(TesterChecklistStepRow, { testerDisplayName: user?.displayName ?? user?.email ?? "tester", item: {
63
- checklistItemId: item.id,
64
- label: item.label,
65
- description: item.description,
66
- href: item.href,
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: TesterChecklistResponseDocument[];
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
- /** Single collection scan (small table: testers x cases) — no join needed. */
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
- * Mirrors appkit/scripts/export-tester-feedback.mjs's CLI output exactly;
46
- * keep the two in sync.
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
  }