@mohasinac/appkit 3.6.4 → 3.7.0

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 (276) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +17 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
  3. package/dist/_internal/client/features/tour/TourProvider.d.ts +5 -2
  4. package/dist/_internal/client/features/tour/TourProvider.js +125 -12
  5. package/dist/_internal/client/theme/ThemeProvider.js +1 -1
  6. package/dist/_internal/server/features/faqs/og.js +5 -1
  7. package/dist/_internal/server/features/reviews/og.js +5 -1
  8. package/dist/_internal/server/features/seo/manifest.js +2 -1
  9. package/dist/_internal/server/features/seo/og.js +7 -1
  10. package/dist/_internal/server/features/tester/visibility.d.ts +13 -0
  11. package/dist/_internal/server/features/tester/visibility.js +28 -0
  12. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  13. package/dist/_internal/server/functions/scheduled.js +9 -1
  14. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  15. package/dist/_internal/server/jobs/core/resetOtpVerification.d.ts +17 -0
  16. package/dist/_internal/server/jobs/core/resetOtpVerification.js +88 -0
  17. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +6 -0
  18. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +35 -0
  19. package/dist/_internal/server/jobs/handlers/_helpers.d.ts +6 -0
  20. package/dist/_internal/server/jobs/handlers/_helpers.js +12 -0
  21. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  22. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  23. package/dist/_internal/server/jobs/handlers/messages.d.ts +2 -0
  24. package/dist/_internal/server/jobs/handlers/messages.js +2 -0
  25. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +2 -0
  26. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +2 -0
  27. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  28. package/dist/_internal/shared/actions/action-registry.js +48 -0
  29. package/dist/_internal/shared/tokens/index.d.ts +12 -12
  30. package/dist/client.d.ts +15 -3
  31. package/dist/client.js +10 -2
  32. package/dist/constants/api-endpoints.d.ts +36 -12
  33. package/dist/constants/api-endpoints.js +16 -8
  34. package/dist/constants/field-names.d.ts +9 -0
  35. package/dist/constants/field-names.js +12 -0
  36. package/dist/constants/index.d.ts +1 -1
  37. package/dist/constants/index.js +1 -1
  38. package/dist/features/account/components/LinkedAccountsSection.d.ts +11 -0
  39. package/dist/features/account/components/LinkedAccountsSection.js +11 -0
  40. package/dist/features/account/components/UserSidebar.d.ts +3 -1
  41. package/dist/features/account/components/UserSidebar.js +11 -15
  42. package/dist/features/account/hooks/useCollapsedSections.d.ts +20 -0
  43. package/dist/features/account/hooks/useCollapsedSections.js +63 -0
  44. package/dist/features/account/hooks/useProfile.d.ts +8 -0
  45. package/dist/features/account/index.d.ts +1 -0
  46. package/dist/features/account/index.js +1 -0
  47. package/dist/features/admin/components/AdminAddressEditorView.js +2 -2
  48. package/dist/features/admin/components/AdminCarouselView.js +2 -2
  49. package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
  50. package/dist/features/admin/components/AdminProductEditorView.js +5 -2
  51. package/dist/features/admin/components/AdminSectionsView.js +3 -18
  52. package/dist/features/admin/components/AdminSidebar.js +10 -8
  53. package/dist/features/admin/components/AdminSiteSettingsView.js +133 -127
  54. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.d.ts +9 -0
  55. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +129 -0
  56. package/dist/features/admin/components/AdminTesterChecklistView.d.ts +6 -0
  57. package/dist/features/admin/components/AdminTesterChecklistView.js +93 -0
  58. package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
  59. package/dist/features/admin/components/AdminUserEditorView.js +7 -1
  60. package/dist/features/admin/components/AdminUsersView.js +1 -1
  61. package/dist/features/admin/components/DataListingView.js +2 -2
  62. package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +8 -0
  63. package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +41 -0
  64. package/dist/features/admin/components/index.d.ts +6 -0
  65. package/dist/features/admin/components/index.js +3 -0
  66. package/dist/features/admin/schemas/firestore.d.ts +0 -7
  67. package/dist/features/admin/schemas/firestore.js +6 -6
  68. package/dist/features/analytics/components/PageViewTracker.d.ts +12 -0
  69. package/dist/features/analytics/components/PageViewTracker.js +22 -0
  70. package/dist/features/analytics/repository/page-views.repository.d.ts +32 -0
  71. package/dist/features/analytics/repository/page-views.repository.js +43 -0
  72. package/dist/features/analytics/types.d.ts +6 -0
  73. package/dist/features/analytics/types.js +13 -0
  74. package/dist/features/auth/components/SocialAuthButtons.js +1 -1
  75. package/dist/features/auth/hooks/useAuth.d.ts +12 -0
  76. package/dist/features/auth/hooks/useAuth.js +109 -0
  77. package/dist/features/auth/permissions/constants.d.ts +1 -1
  78. package/dist/features/auth/schemas/firestore.d.ts +17 -1
  79. package/dist/features/auth/schemas/firestore.js +2 -0
  80. package/dist/features/blog/components/BlogIndexListing.js +2 -2
  81. package/dist/features/blog/schemas/firestore.d.ts +4 -0
  82. package/dist/features/cart/actions/cart-actions.js +20 -2
  83. package/dist/features/categories/components/CategoriesIndexListing.js +2 -2
  84. package/dist/features/categories/components/CategoryBundlesListing.js +2 -2
  85. package/dist/features/categories/components/CategoryDetailPageView.js +3 -1
  86. package/dist/features/categories/components/CategoryProductsListing.js +2 -2
  87. package/dist/features/categories/schemas/firestore.d.ts +4 -0
  88. package/dist/features/classified/components/ClassifiedIndexListing.js +2 -2
  89. package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +2 -2
  90. package/dist/features/email/primitives.d.ts +2 -2
  91. package/dist/features/email/primitives.js +2 -2
  92. package/dist/features/events/components/EventsIndexListing.js +2 -2
  93. package/dist/features/events/schemas/firestore.d.ts +4 -0
  94. package/dist/features/layout/BackToTop.d.ts +6 -1
  95. package/dist/features/layout/BackToTop.js +31 -6
  96. package/dist/features/live/components/LiveItemsIndexListing.js +2 -2
  97. package/dist/features/media/MediaVideo.js +5 -1
  98. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -2
  99. package/dist/features/products/components/AuctionsIndexListing.js +2 -2
  100. package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -2
  101. package/dist/features/products/components/ProductsIndexListing.js +2 -2
  102. package/dist/features/products/constants/action-defs.d.ts +2 -0
  103. package/dist/features/products/constants/action-defs.js +2 -0
  104. package/dist/features/products/schemas/firestore.d.ts +4 -0
  105. package/dist/features/promotions/components/CouponsIndexListing.js +2 -2
  106. package/dist/features/reviews/components/ReviewsIndexListing.js +2 -2
  107. package/dist/features/seller/actions/seller-actions.js +11 -6
  108. package/dist/features/seller/components/SellerAddressesView.js +2 -2
  109. package/dist/features/seller/components/SellerBidsView.js +2 -2
  110. package/dist/features/seller/components/SellerOrdersView.js +2 -2
  111. package/dist/features/seller/components/SellerProductsView.js +2 -2
  112. package/dist/features/seller/components/SellerSidebar.d.ts +3 -1
  113. package/dist/features/seller/components/SellerSidebar.js +11 -10
  114. package/dist/features/seller/components/SellerStoreCategoriesView.js +2 -2
  115. package/dist/features/seller/components/SellerTemplatesView.js +2 -2
  116. package/dist/features/shell/StepForm.js +4 -4
  117. package/dist/features/stores/actions/store-query-actions.d.ts +5 -4
  118. package/dist/features/stores/actions/store-query-actions.js +11 -8
  119. package/dist/features/stores/components/StoreAuctionsListing.js +2 -2
  120. package/dist/features/stores/components/StoreClassifiedsListing.js +2 -2
  121. package/dist/features/stores/components/StoreDigitalCodesListing.js +2 -2
  122. package/dist/features/stores/components/StoreLiveItemsListing.js +2 -2
  123. package/dist/features/stores/components/StorePreOrdersListing.js +2 -2
  124. package/dist/features/stores/components/StoreProductsListing.js +2 -2
  125. package/dist/features/stores/components/StoreReviewsListing.js +2 -2
  126. package/dist/features/stores/components/StoresIndexListing.js +2 -2
  127. package/dist/features/stores/schemas/firestore.d.ts +4 -0
  128. package/dist/features/tester/actions/checklist-item-actions.d.ts +69 -0
  129. package/dist/features/tester/actions/checklist-item-actions.js +42 -0
  130. package/dist/features/tester/actions/index.d.ts +1 -0
  131. package/dist/features/tester/actions/index.js +1 -0
  132. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.d.ts +2 -0
  133. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +22 -0
  134. package/dist/features/tester/components/AdminTesterFeedbackListView.d.ts +3 -0
  135. package/dist/features/tester/components/AdminTesterFeedbackListView.js +100 -0
  136. package/dist/features/tester/components/AdminTesterFeedbackReportView.d.ts +2 -0
  137. package/dist/features/tester/components/AdminTesterFeedbackReportView.js +30 -0
  138. package/dist/features/tester/components/AdminTesterFeedbackView.d.ts +2 -0
  139. package/dist/features/tester/components/AdminTesterFeedbackView.js +9 -0
  140. package/dist/features/tester/components/TesterChecklistStepRow.d.ts +18 -0
  141. package/dist/features/tester/components/TesterChecklistStepRow.js +29 -0
  142. package/dist/features/tester/components/TesterFeedbackChart.d.ts +14 -0
  143. package/dist/features/tester/components/TesterFeedbackChart.js +18 -0
  144. package/dist/features/tester/components/TesterHubView.d.ts +5 -0
  145. package/dist/features/tester/components/TesterHubView.js +71 -0
  146. package/dist/features/tester/components/index.d.ts +11 -0
  147. package/dist/features/tester/components/index.js +7 -0
  148. package/dist/features/tester/index.d.ts +2 -0
  149. package/dist/features/tester/index.js +2 -0
  150. package/dist/features/tester/repository/index.d.ts +2 -0
  151. package/dist/features/tester/repository/index.js +2 -0
  152. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +13 -0
  153. package/dist/features/tester/repository/tester-checklist-item.repository.js +55 -0
  154. package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +42 -0
  155. package/dist/features/tester/repository/tester-checklist-response.repository.js +98 -0
  156. package/dist/features/tester/schemas/firestore.d.ts +85 -0
  157. package/dist/features/tester/schemas/firestore.js +93 -0
  158. package/dist/features/tester/schemas/index.d.ts +1 -0
  159. package/dist/features/tester/schemas/index.js +1 -0
  160. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +2 -0
  161. package/dist/features/tester/seed-data/blog-tester-seed-data.js +40 -0
  162. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +2 -0
  163. package/dist/features/tester/seed-data/categories-tester-seed-data.js +177 -0
  164. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +2 -0
  165. package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -0
  166. package/dist/features/tester/seed-data/index.d.ts +6 -0
  167. package/dist/features/tester/seed-data/index.js +6 -0
  168. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +2 -0
  169. package/dist/features/tester/seed-data/products-tester-seed-data.js +151 -0
  170. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +2 -0
  171. package/dist/features/tester/seed-data/stores-tester-seed-data.js +40 -0
  172. package/dist/features/tester/seed-data/tester-checklist-seed-data.d.ts +2 -0
  173. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +248 -0
  174. package/dist/features/tester/seed-data/tester-ttl.d.ts +4 -0
  175. package/dist/features/tester/seed-data/tester-ttl.js +6 -0
  176. package/dist/features/tester/server.d.ts +9 -0
  177. package/dist/features/tester/server.js +8 -0
  178. package/dist/index.d.ts +28 -2
  179. package/dist/index.js +51 -1
  180. package/dist/next/api/api-response.js +5 -1
  181. package/dist/next/api/routeHandler.js +10 -1
  182. package/dist/next/middleware/chain.js +1 -1
  183. package/dist/next/routing/route-map.d.ts +6 -7
  184. package/dist/next/routing/route-map.js +3 -7
  185. package/dist/providers/payment-manual/index.d.ts +2 -2
  186. package/dist/providers/payment-manual/index.js +2 -2
  187. package/dist/react/contexts/SessionContext.d.ts +4 -0
  188. package/dist/react/contexts/SessionContext.js +4 -0
  189. package/dist/repositories/index.d.ts +4 -0
  190. package/dist/repositories/index.js +3 -0
  191. package/dist/security/pii-encrypt.js +12 -2
  192. package/dist/seed/addresses-seed-data.js +1 -1
  193. package/dist/seed/bids-seed-data.js +1 -1
  194. package/dist/seed/blog-posts-seed-data.js +1 -1
  195. package/dist/seed/carousel-slides-seed-data.js +22 -16
  196. package/dist/seed/carousels-seed-data.js +1 -1
  197. package/dist/seed/cart-seed-data.js +1 -1
  198. package/dist/seed/categories-seed-data.js +1 -1
  199. package/dist/seed/claimed-coupons-seed-data.js +1 -1
  200. package/dist/seed/conversations-seed-data.js +1 -1
  201. package/dist/seed/coupon-usage-seed-data.js +1 -1
  202. package/dist/seed/coupons-seed-data.js +1 -1
  203. package/dist/seed/events-seed-data.js +1 -1
  204. package/dist/seed/faq-seed-data.js +119 -3
  205. package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
  206. package/dist/seed/grouped-listings-seed-data.js +1 -1
  207. package/dist/seed/history-seed-data.js +1 -1
  208. package/dist/seed/homepage-sections-seed-data.js +1 -1
  209. package/dist/seed/index.d.ts +2 -2
  210. package/dist/seed/index.js +7 -3
  211. package/dist/seed/lottery-entries-seed-data.js +1 -1
  212. package/dist/seed/manifest.d.ts +4 -3
  213. package/dist/seed/manifest.js +13 -6
  214. package/dist/seed/notifications-seed-data.js +1 -1
  215. package/dist/seed/offers-seed-data.js +1 -1
  216. package/dist/seed/orders-seed-data.js +1 -1
  217. package/dist/seed/payouts-seed-data.js +1 -1
  218. package/dist/seed/products-art-seed-data.js +1 -1
  219. package/dist/seed/products-auctions-seed-data.js +1 -1
  220. package/dist/seed/products-classifieds-seed-data.js +1 -1
  221. package/dist/seed/products-digital-codes-seed-data.js +1 -1
  222. package/dist/seed/products-live-items-seed-data.js +1 -1
  223. package/dist/seed/products-preorders-seed-data.d.ts +2 -0
  224. package/dist/seed/products-preorders-seed-data.js +1 -1
  225. package/dist/seed/products-prize-draws-seed-data.js +1 -1
  226. package/dist/seed/products-standard-seed-data.js +1 -1
  227. package/dist/seed/products-stickers-seed-data.js +1 -1
  228. package/dist/seed/reviews-seed-data.js +1 -1
  229. package/dist/seed/scammers-seed-data.js +1 -1
  230. package/dist/seed/sessions-seed-data.js +1 -1
  231. package/dist/seed/site-settings-seed-data.js +22 -16
  232. package/dist/seed/store-addresses-seed-data.js +1 -1
  233. package/dist/seed/store-extensions-seed-data.js +1 -1
  234. package/dist/seed/stores-seed-data.js +1 -1
  235. package/dist/seed/support-tickets-seed-data.js +1 -1
  236. package/dist/seed/tester-checklist-seed-data.d.ts +2 -0
  237. package/dist/seed/tester-checklist-seed-data.js +248 -0
  238. package/dist/seed/users-seed-data.js +1 -1
  239. package/dist/seed/wishlists-seed-data.js +1 -1
  240. package/dist/server.d.ts +7 -2
  241. package/dist/server.js +19 -12
  242. package/dist/styles.css +163 -147
  243. package/dist/tailwind-utilities.css +1 -1
  244. package/dist/tokens/color-pairs.d.ts +46 -0
  245. package/dist/tokens/color-pairs.js +93 -0
  246. package/dist/tokens/index.d.ts +60 -60
  247. package/dist/tokens/index.js +30 -30
  248. package/dist/tokens/themes/cobalt-night.d.ts +3 -2
  249. package/dist/tokens/themes/cobalt-night.js +3 -2
  250. package/dist/tokens/themes/default-dark.d.ts +4 -1
  251. package/dist/tokens/themes/default-dark.js +44 -41
  252. package/dist/tokens/themes/default-light.d.ts +5 -1
  253. package/dist/tokens/themes/default-light.js +39 -35
  254. package/dist/tokens/themes/sunset.js +4 -4
  255. package/dist/tokens/tokens.css +110 -104
  256. package/dist/ui/components/CollapsibleSection.d.ts +15 -0
  257. package/dist/ui/components/CollapsibleSection.js +13 -0
  258. package/dist/ui/components/ListingToolbar.js +2 -1
  259. package/dist/ui/components/Semantic.style.css +3 -3
  260. package/dist/ui/components/SideDrawer.style.css +19 -19
  261. package/dist/ui/components/Slider.js +1 -1
  262. package/dist/ui/components/StickyToolbar.d.ts +36 -7
  263. package/dist/ui/components/StickyToolbar.js +63 -5
  264. package/dist/ui/components/Tabs.js +17 -1
  265. package/dist/ui/components/Tabs.style.css +10 -0
  266. package/dist/ui/components/Typography.js +7 -0
  267. package/dist/ui/components/surface-tokens.d.ts +0 -7
  268. package/dist/ui/components/surface-tokens.js +7 -0
  269. package/dist/ui/index.d.ts +6 -0
  270. package/dist/ui/index.js +3 -0
  271. package/dist/ui/rich-text/RichText.style.css +20 -20
  272. package/dist/utils/id-generators.d.ts +13 -0
  273. package/dist/utils/id-generators.js +17 -0
  274. package/package.json +1 -1
  275. package/scripts/seed-cli-loader.mjs +15 -0
  276. package/scripts/seed-cli.mjs +71 -17
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { Stack, Text } from "../../../ui";
5
+ import { apiClient } from "../../../http";
6
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
7
+ import { TesterFeedbackChart } from "./TesterFeedbackChart";
8
+ export function AdminTesterFeedbackReportView() {
9
+ const query = useQuery({
10
+ queryKey: ["admin", "tester-feedback", "report"],
11
+ queryFn: async () => {
12
+ const res = await apiClient.get(ADMIN_ENDPOINTS.TESTER_FEEDBACK_REPORT);
13
+ return res?.data ?? res;
14
+ },
15
+ });
16
+ if (query.isLoading)
17
+ return _jsx(Text, { color: "muted", children: "Loading report\u2026" });
18
+ const itemCoverage = query.data?.itemCoverage ?? [];
19
+ const totals = query.data?.totals ?? { totalAnswered: 0, totalYes: 0, totalNo: 0 };
20
+ const byGroup = new Map();
21
+ for (const item of itemCoverage) {
22
+ if (!byGroup.has(item.groupKey)) {
23
+ byGroup.set(item.groupKey, { groupLabel: item.groupKey, yes: 0, no: 0 });
24
+ }
25
+ const entry = byGroup.get(item.groupKey);
26
+ entry.yes += item.yesCount;
27
+ entry.no += item.noCount;
28
+ }
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
+ }
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare function AdminTesterFeedbackView(): React.JSX.Element;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Heading, Stack, Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui";
4
+ import { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
5
+ import { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
6
+ import { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
7
+ export function AdminTesterFeedbackView() {
8
+ return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Feedback" }), _jsxs(Tabs, { defaultValue: "report", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "report", children: "Report" }), _jsx(TabsTrigger, { value: "issues", children: "Main Issues" }), _jsx(TabsTrigger, { value: "submissions", children: "All Submissions" })] }), _jsx(TabsContent, { value: "report", children: _jsx(AdminTesterFeedbackReportView, {}) }), _jsx(TabsContent, { value: "issues", children: _jsx(AdminTesterFeedbackIssuesView, {}) }), _jsx(TabsContent, { value: "submissions", children: _jsx(AdminTesterFeedbackListView, {}) })] })] }));
9
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import type { TesterAnswer } from "../schemas/firestore";
3
+ export interface TesterChecklistStepItem {
4
+ checklistItemId: string;
5
+ label: string;
6
+ description?: string;
7
+ href?: string;
8
+ answer: TesterAnswer | null;
9
+ comment?: string;
10
+ screenshotUrl?: string;
11
+ }
12
+ export interface TesterChecklistStepRowProps {
13
+ item: TesterChecklistStepItem;
14
+ testerDisplayName: string;
15
+ onAnswer: (checklistItemId: string, answer: TesterAnswer) => void;
16
+ onSaveNote: (checklistItemId: string, comment: string, screenshotUrl: string) => Promise<void>;
17
+ }
18
+ export declare function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSaveNote, }: TesterChecklistStepRowProps): React.JSX.Element;
@@ -0,0 +1,29 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { Anchor, Button, Div, FieldTextarea, Row, Stack, Text } from "../../../ui";
5
+ import { MediaUploadField } from "../../media/upload/MediaUploadField";
6
+ import { useMediaUpload } from "../../media/hooks/useMedia";
7
+ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
8
+ export function TesterChecklistStepRow({ item, testerDisplayName, onAnswer, onSaveNote, }) {
9
+ const [expanded, setExpanded] = React.useState(false);
10
+ const [comment, setComment] = React.useState(item.comment ?? "");
11
+ const [screenshotUrl, setScreenshotUrl] = React.useState(item.screenshotUrl ?? "");
12
+ const [saving, setSaving] = React.useState(false);
13
+ const { upload } = useMediaUpload();
14
+ const handleUpload = async (file) => upload(file, "tester-screenshots", false, {
15
+ type: "tester-screenshot",
16
+ testerName: testerDisplayName,
17
+ checklistItemId: item.checklistItemId,
18
+ });
19
+ const handleSave = async () => {
20
+ setSaving(true);
21
+ try {
22
+ await onSaveNote(item.checklistItemId, comment, screenshotUrl);
23
+ }
24
+ finally {
25
+ setSaving(false);
26
+ }
27
+ };
28
+ return (_jsxs(Stack, { gap: "xs", padding: "sm", border: "default", rounded: "md", children: [_jsxs(Row, { gap: "sm", justify: "between", align: "center", wrap: true, children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { weight: "medium", children: item.label }), item.description && (_jsx(Text, { size: "xs", color: "muted", children: item.description })), item.href && (_jsx(Anchor, { href: item.href, tone: "brand", underline: "hover", children: "Go test this \u2192" }))] }), _jsxs(Row, { gap: "xs", children: [_jsx(Button, { type: "button", size: "sm", variant: item.answer === "yes" ? "primary" : "outline", onClick: () => onAnswer(item.checklistItemId, "yes"), children: "Yes" }), _jsx(Button, { type: "button", size: "sm", variant: item.answer === "no" ? "danger" : "outline", onClick: () => onAnswer(item.checklistItemId, "no"), children: "No" }), _jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: () => setExpanded((v) => !v), children: expanded ? "Hide note" : "Add note" })] })] }), expanded && (_jsx(Div, { padding: "t-sm", border: "default", className: "border-t", children: _jsxs(Stack, { gap: "sm", children: [_jsx(FieldTextarea, { name: `comment-${item.checklistItemId}`, label: "Comment", value: comment, onChange: setComment, placeholder: "What did you notice? Colors, styles, readability, bugs..." }), _jsx(MediaUploadField, { label: "Screenshot (optional)", value: screenshotUrl, onChange: setScreenshotUrl, onUpload: handleUpload, kind: "image" }), _jsx(Row, { children: _jsx(Button, { type: "button", action: ACTIONS.TESTER["save-note"], isLoading: saving, onClick: handleSave }) })] }) }))] }));
29
+ }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ export interface GroupCoverageDatum {
3
+ groupLabel: string;
4
+ yes: number;
5
+ no: number;
6
+ }
7
+ export interface TesterFeedbackChartProps {
8
+ data: GroupCoverageDatum[];
9
+ totalCases: number;
10
+ totalAnswered: number;
11
+ totalYes: number;
12
+ totalNo: number;
13
+ }
14
+ export declare function TesterFeedbackChart({ data, totalCases, totalAnswered, totalYes, totalNo, }: TesterFeedbackChartProps): React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import dynamic from "next/dynamic";
4
+ import { Card, Heading, Row, Stack, Text } from "../../../ui";
5
+ // recharts exports generic components (<DataPointType>) that don't satisfy
6
+ // next/dynamic's ComponentType<P> constraint — cast each to ComponentType<any>.
7
+ const ResponsiveContainer = dynamic(() => import("recharts").then((m) => m.ResponsiveContainer), { ssr: false });
8
+ const BarChart = dynamic(() => import("recharts").then((m) => m.BarChart), { ssr: false });
9
+ const Bar = dynamic(() => import("recharts").then((m) => m.Bar), { ssr: false });
10
+ const XAxis = dynamic(() => import("recharts").then((m) => m.XAxis), { ssr: false });
11
+ const YAxis = dynamic(() => import("recharts").then((m) => m.YAxis), { ssr: false });
12
+ const CartesianGrid = dynamic(() => import("recharts").then((m) => m.CartesianGrid), { ssr: false });
13
+ const Tooltip = dynamic(() => import("recharts").then((m) => m.Tooltip), { ssr: false });
14
+ const Legend = dynamic(() => import("recharts").then((m) => m.Legend), { ssr: false });
15
+ export function TesterFeedbackChart({ data, totalCases, totalAnswered, totalYes, totalNo, }) {
16
+ const passRate = totalAnswered > 0 ? Math.round((totalYes / totalAnswered) * 100) : 0;
17
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Total test cases" }), _jsx(Heading, { level: 3, children: totalCases })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Answers recorded" }), _jsx(Heading, { level: 3, children: totalAnswered })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Pass rate" }), _jsxs(Heading, { level: 3, children: [passRate, "%"] })] }), _jsxs(Card, { padding: "md", variant: "default", children: [_jsx(Text, { size: "xs", color: "muted", children: "Issues (No)" }), _jsx(Heading, { level: 3, color: "error", children: totalNo })] })] }), _jsx(ResponsiveContainer, { width: "100%", height: 360, children: _jsxs(BarChart, { data: data, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "groupLabel", tick: { fontSize: 12 } }), _jsx(YAxis, { allowDecimals: false }), _jsx(Tooltip, {}), _jsx(Legend, {}), _jsx(Bar, { dataKey: "yes", name: "Yes", fill: "var(--appkit-color-success)" }), _jsx(Bar, { dataKey: "no", name: "No", fill: "var(--appkit-color-error)" })] }) })] }));
18
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ export interface TesterHubViewProps {
3
+ sandboxExpiresAt?: string | null;
4
+ }
5
+ export declare function TesterHubView({ sandboxExpiresAt }: TesterHubViewProps): React.JSX.Element;
@@ -0,0 +1,71 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
5
+ import { Alert, Details, Div, FieldInput, Heading, Stack, Summary, Text } from "../../../ui";
6
+ import { apiClient } from "../../../http";
7
+ import { useApiMutation } from "../../../client";
8
+ import { useSession } from "../../../react";
9
+ import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
10
+ import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
11
+ function matchesQuery(item, query) {
12
+ if (!query)
13
+ return true;
14
+ const haystack = `${item.label} ${item.description ?? ""} ${item.groupLabel} ${item.pageLabel}`.toLowerCase();
15
+ return haystack.includes(query.toLowerCase());
16
+ }
17
+ export function TesterHubView({ sandboxExpiresAt }) {
18
+ const { user } = useSession();
19
+ const queryClient = useQueryClient();
20
+ const [search, setSearch] = React.useState("");
21
+ const query = useQuery({
22
+ queryKey: ["user", "tester-checklist"],
23
+ queryFn: async () => {
24
+ const res = await apiClient.get(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST);
25
+ return res?.data ?? res;
26
+ },
27
+ });
28
+ const upsertMutation = useApiMutation({
29
+ mutationFn: async (vars) => apiClient.put(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST_ITEM_BY_ID(vars.checklistItemId), vars.patch),
30
+ onSuccess: () => {
31
+ void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
32
+ },
33
+ });
34
+ if (!user?.isTester) {
35
+ return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers. Contact an admin if you believe this is a mistake." }));
36
+ }
37
+ const items = query.data?.items ?? [];
38
+ const filtered = items.filter((item) => matchesQuery(item, search));
39
+ const groups = new Map();
40
+ for (const item of filtered) {
41
+ if (!groups.has(item.groupKey)) {
42
+ groups.set(item.groupKey, { groupLabel: item.groupLabel, pages: new Map() });
43
+ }
44
+ const group = groups.get(item.groupKey);
45
+ if (!group.pages.has(item.pageKey)) {
46
+ group.pages.set(item.pageKey, { pageLabel: item.pageLabel, items: [] });
47
+ }
48
+ group.pages.get(item.pageKey).items.push(item);
49
+ }
50
+ const handleAnswer = (checklistItemId, answer) => {
51
+ upsertMutation.mutate({ checklistItemId, patch: { answer } });
52
+ };
53
+ const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
54
+ await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
55
+ };
56
+ 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: "e.g. checkout, google, bidding..." }), 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]) => {
57
+ const answeredCount = page.items.filter((i) => i.answer).length;
58
+ 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
59
+ .slice()
60
+ .sort((a, b) => a.order - b.order)
61
+ .map((item) => (_jsx(TesterChecklistStepRow, { testerDisplayName: user?.displayName ?? user?.email ?? "tester", item: {
62
+ checklistItemId: item.id,
63
+ label: item.label,
64
+ description: item.description,
65
+ href: item.href,
66
+ answer: item.answer,
67
+ comment: item.comment,
68
+ screenshotUrl: item.screenshotUrl,
69
+ }, onAnswer: handleAnswer, onSaveNote: handleSaveNote }, item.id))) }) })] }, pageKey));
70
+ })] }, groupKey)))] }));
71
+ }
@@ -0,0 +1,11 @@
1
+ export { TesterHubView } from "./TesterHubView";
2
+ export type { TesterHubViewProps } from "./TesterHubView";
3
+ export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
4
+ export type { TesterChecklistStepRowProps, TesterChecklistStepItem } from "./TesterChecklistStepRow";
5
+ export { TesterFeedbackChart } from "./TesterFeedbackChart";
6
+ export type { TesterFeedbackChartProps, GroupCoverageDatum } from "./TesterFeedbackChart";
7
+ export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
8
+ export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
9
+ export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
10
+ export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
11
+ export type { AdminTesterFeedbackListViewProps } from "./AdminTesterFeedbackListView";
@@ -0,0 +1,7 @@
1
+ export { TesterHubView } from "./TesterHubView";
2
+ export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
3
+ export { TesterFeedbackChart } from "./TesterFeedbackChart";
4
+ export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
5
+ export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
6
+ export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
7
+ export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
@@ -0,0 +1,2 @@
1
+ export * from "./schemas";
2
+ export * from "./components";
@@ -0,0 +1,2 @@
1
+ export * from "./schemas";
2
+ export * from "./components";
@@ -0,0 +1,2 @@
1
+ export * from "./tester-checklist-item.repository";
2
+ export * from "./tester-checklist-response.repository";
@@ -0,0 +1,2 @@
1
+ export * from "./tester-checklist-item.repository";
2
+ export * from "./tester-checklist-response.repository";
@@ -0,0 +1,13 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
+ import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput } from "../schemas/firestore";
4
+ export declare class TesterChecklistItemRepository extends BaseRepository<TesterChecklistItemDocument> {
5
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
6
+ constructor();
7
+ private buildSearchTokens;
8
+ createItem(input: TesterChecklistItemCreateInput): Promise<TesterChecklistItemDocument>;
9
+ update(id: string, data: TesterChecklistItemUpdateInput): Promise<TesterChecklistItemDocument>;
10
+ listActive(): Promise<TesterChecklistItemDocument[]>;
11
+ list(model: SieveModel): Promise<FirebaseSieveResult<TesterChecklistItemDocument>>;
12
+ }
13
+ export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
@@ -0,0 +1,55 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ import { DatabaseError } from "../../../errors";
3
+ import { TESTER_CHECKLIST_ITEM_COLLECTION, TESTER_CHECKLIST_ITEM_FIELDS, createChecklistItemId, } from "../schemas/firestore";
4
+ export class TesterChecklistItemRepository extends BaseRepository {
5
+ constructor() {
6
+ super(TESTER_CHECKLIST_ITEM_COLLECTION);
7
+ }
8
+ buildSearchTokens(input) {
9
+ const rawText = [input.label, input.description, input.groupLabel, input.pageLabel]
10
+ .filter(Boolean)
11
+ .join(" ")
12
+ .toLowerCase();
13
+ return Array.from(new Set(rawText
14
+ .split(/[^a-z0-9]+/i)
15
+ .map((token) => token.trim())
16
+ .filter((token) => token.length >= 2))).slice(0, 50);
17
+ }
18
+ async createItem(input) {
19
+ const id = createChecklistItemId(input.groupKey, input.pageKey, input.label);
20
+ const searchTokens = this.buildSearchTokens(input);
21
+ return this.createWithId(id, { ...input, searchTokens });
22
+ }
23
+ async update(id, data) {
24
+ const current = await this.findById(id);
25
+ if (!current) {
26
+ throw new DatabaseError(`Failed to update checklist item: missing document ${id}`);
27
+ }
28
+ const merged = { ...current, ...data };
29
+ return super.update(id, {
30
+ ...data,
31
+ searchTokens: this.buildSearchTokens(merged),
32
+ });
33
+ }
34
+ async listActive() {
35
+ const snapshot = await this.db
36
+ .collection(this.collection)
37
+ .where(TESTER_CHECKLIST_ITEM_FIELDS.IS_ACTIVE, "==", true)
38
+ .orderBy(TESTER_CHECKLIST_ITEM_FIELDS.ORDER, "asc")
39
+ .get();
40
+ return snapshot.docs.map((doc) => this.mapDoc(doc));
41
+ }
42
+ async list(model) {
43
+ return this.sieveQuery(model, TesterChecklistItemRepository.SIEVE_FIELDS);
44
+ }
45
+ }
46
+ TesterChecklistItemRepository.SIEVE_FIELDS = {
47
+ groupKey: { canFilter: true, canSort: true },
48
+ pageKey: { canFilter: true, canSort: true },
49
+ label: { canFilter: true, canSort: false },
50
+ order: { canFilter: true, canSort: true },
51
+ isActive: { canFilter: true, canSort: false },
52
+ searchTokens: { canFilter: true, canSort: false },
53
+ createdAt: { canFilter: true, canSort: true },
54
+ };
55
+ export const testerChecklistItemRepository = new TesterChecklistItemRepository();
@@ -0,0 +1,42 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
+ import { type TesterAnswer, type TesterChecklistResponseDocument } from "../schemas/firestore";
4
+ export interface UpsertResponseInput {
5
+ testerId: string;
6
+ testerDisplayName: string;
7
+ checklistItemId: string;
8
+ groupKey: string;
9
+ pageKey: string;
10
+ answer?: TesterAnswer | null;
11
+ comment?: string;
12
+ screenshotUrl?: string;
13
+ }
14
+ export interface ChecklistItemCoverage {
15
+ checklistItemId: string;
16
+ groupKey: string;
17
+ pageKey: string;
18
+ yesCount: number;
19
+ noCount: number;
20
+ totalAnswered: number;
21
+ }
22
+ export interface CoverageReport {
23
+ itemCoverage: ChecklistItemCoverage[];
24
+ issues: TesterChecklistResponseDocument[];
25
+ totals: {
26
+ totalAnswered: number;
27
+ totalYes: number;
28
+ totalNo: number;
29
+ };
30
+ }
31
+ export declare class TesterChecklistResponseRepository extends BaseRepository<TesterChecklistResponseDocument> {
32
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
33
+ constructor();
34
+ /** Deterministic-ID upsert — the only mutation path for tester responses. */
35
+ upsertResponse(input: UpsertResponseInput): Promise<TesterChecklistResponseDocument>;
36
+ listForTester(testerId: string): Promise<TesterChecklistResponseDocument[]>;
37
+ list(model: SieveModel): Promise<FirebaseSieveResult<TesterChecklistResponseDocument>>;
38
+ markReviewed(id: string): Promise<TesterChecklistResponseDocument>;
39
+ /** Single collection scan (small table: testers x cases) — no join needed. */
40
+ getCoverageReport(): Promise<CoverageReport>;
41
+ }
42
+ export declare const testerChecklistResponseRepository: TesterChecklistResponseRepository;
@@ -0,0 +1,98 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase";
2
+ import { TESTER_CHECKLIST_RESPONSE_COLLECTION, TESTER_CHECKLIST_RESPONSE_FIELDS, createChecklistResponseId, } from "../schemas/firestore";
3
+ export class TesterChecklistResponseRepository extends BaseRepository {
4
+ constructor() {
5
+ super(TESTER_CHECKLIST_RESPONSE_COLLECTION);
6
+ }
7
+ /** Deterministic-ID upsert — the only mutation path for tester responses. */
8
+ async upsertResponse(input) {
9
+ const id = createChecklistResponseId(input.testerId, input.checklistItemId);
10
+ const existing = await this.findById(id);
11
+ const patch = {
12
+ testerId: input.testerId,
13
+ testerDisplayName: input.testerDisplayName,
14
+ checklistItemId: input.checklistItemId,
15
+ groupKey: input.groupKey,
16
+ pageKey: input.pageKey,
17
+ status: "new",
18
+ };
19
+ if (input.answer !== undefined)
20
+ patch.answer = input.answer;
21
+ if (input.comment !== undefined)
22
+ patch.comment = input.comment;
23
+ if (input.screenshotUrl !== undefined)
24
+ patch.screenshotUrl = input.screenshotUrl;
25
+ if (existing) {
26
+ return this.update(id, patch);
27
+ }
28
+ return this.createWithId(id, {
29
+ answer: null,
30
+ ...patch,
31
+ });
32
+ }
33
+ async listForTester(testerId) {
34
+ const snapshot = await this.db
35
+ .collection(this.collection)
36
+ .where(TESTER_CHECKLIST_RESPONSE_FIELDS.TESTER_ID, "==", testerId)
37
+ .get();
38
+ return snapshot.docs.map((doc) => this.mapDoc(doc));
39
+ }
40
+ async list(model) {
41
+ return this.sieveQuery(model, TesterChecklistResponseRepository.SIEVE_FIELDS);
42
+ }
43
+ async markReviewed(id) {
44
+ return this.update(id, {
45
+ status: "reviewed",
46
+ });
47
+ }
48
+ /** Single collection scan (small table: testers x cases) — no join needed. */
49
+ async getCoverageReport() {
50
+ const snapshot = await this.db.collection(this.collection).get();
51
+ const docs = snapshot.docs.map((doc) => this.mapDoc(doc));
52
+ const byItem = new Map();
53
+ const issues = [];
54
+ let totalYes = 0;
55
+ let totalNo = 0;
56
+ for (const doc of docs) {
57
+ if (!doc.answer)
58
+ continue;
59
+ let entry = byItem.get(doc.checklistItemId);
60
+ if (!entry) {
61
+ entry = {
62
+ checklistItemId: doc.checklistItemId,
63
+ groupKey: doc.groupKey,
64
+ pageKey: doc.pageKey,
65
+ yesCount: 0,
66
+ noCount: 0,
67
+ totalAnswered: 0,
68
+ };
69
+ byItem.set(doc.checklistItemId, entry);
70
+ }
71
+ entry.totalAnswered += 1;
72
+ if (doc.answer === "yes") {
73
+ entry.yesCount += 1;
74
+ totalYes += 1;
75
+ }
76
+ else {
77
+ entry.noCount += 1;
78
+ totalNo += 1;
79
+ issues.push(doc);
80
+ }
81
+ }
82
+ return {
83
+ itemCoverage: Array.from(byItem.values()),
84
+ issues,
85
+ totals: { totalAnswered: totalYes + totalNo, totalYes, totalNo },
86
+ };
87
+ }
88
+ }
89
+ TesterChecklistResponseRepository.SIEVE_FIELDS = {
90
+ testerId: { canFilter: true, canSort: false },
91
+ checklistItemId: { canFilter: true, canSort: false },
92
+ groupKey: { canFilter: true, canSort: true },
93
+ pageKey: { canFilter: true, canSort: true },
94
+ answer: { canFilter: true, canSort: false },
95
+ status: { canFilter: true, canSort: false },
96
+ createdAt: { canFilter: true, canSort: true },
97
+ };
98
+ export const testerChecklistResponseRepository = new TesterChecklistResponseRepository();
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Tester Feature Firestore Document Types & Constants
3
+ *
4
+ * Two distinct collections, deliberately separate:
5
+ * - TesterChecklistItemDocument — the admin-authored catalog of test cases (mirrors FAQs).
6
+ * - TesterChecklistResponseDocument — one doc per (tester, case) answer, upserted by testerId+checklistItemId.
7
+ */
8
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
9
+ export interface TesterChecklistItemDocument extends BaseDocument {
10
+ groupKey: string;
11
+ groupLabel: string;
12
+ pageKey: string;
13
+ pageLabel: string;
14
+ label: string;
15
+ description?: string;
16
+ href?: string;
17
+ order: number;
18
+ isActive: boolean;
19
+ searchTokens: string[];
20
+ }
21
+ export declare const TESTER_CHECKLIST_ITEM_COLLECTION: "testerChecklistItems";
22
+ export declare const TESTER_CHECKLIST_ITEM_ID_PREFIX: "checklist-";
23
+ export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "order", "isActive", "searchTokens", "createdAt"];
24
+ export declare const DEFAULT_TESTER_CHECKLIST_ITEM_DATA: Partial<TesterChecklistItemDocument>;
25
+ export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "searchTokens">;
26
+ export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "isActive">>;
27
+ export declare const testerChecklistItemQueryHelpers: {
28
+ readonly byGroup: (groupKey: string) => readonly ["groupKey", "==", string];
29
+ readonly byPage: (pageKey: string) => readonly ["pageKey", "==", string];
30
+ readonly active: () => readonly ["isActive", "==", true];
31
+ };
32
+ export declare function slugifyChecklistLabel(label: string): string;
33
+ export declare function createChecklistItemId(groupKey: string, pageKey: string, label: string): string;
34
+ export type TesterAnswer = "yes" | "no";
35
+ export type TesterFeedbackStatus = "new" | "reviewed";
36
+ export interface TesterChecklistResponseDocument extends BaseDocument {
37
+ testerId: string;
38
+ testerDisplayName: string;
39
+ checklistItemId: string;
40
+ groupKey: string;
41
+ pageKey: string;
42
+ answer: TesterAnswer | null;
43
+ comment?: string;
44
+ screenshotUrl?: string;
45
+ status: TesterFeedbackStatus;
46
+ }
47
+ export declare const TESTER_CHECKLIST_RESPONSE_COLLECTION: "testerChecklistResponses";
48
+ export declare const TESTER_CHECKLIST_RESPONSE_INDEXED_FIELDS: readonly ["testerId", "checklistItemId", "groupKey", "pageKey", "answer", "status", "createdAt"];
49
+ export declare const DEFAULT_TESTER_CHECKLIST_RESPONSE_DATA: Partial<TesterChecklistResponseDocument>;
50
+ export declare function createChecklistResponseId(testerId: string, checklistItemId: string): string;
51
+ export declare const testerChecklistResponseQueryHelpers: {
52
+ readonly byTester: (testerId: string) => readonly ["testerId", "==", string];
53
+ readonly byItem: (checklistItemId: string) => readonly ["checklistItemId", "==", string];
54
+ readonly byAnswer: (answer: TesterAnswer) => readonly ["answer", "==", TesterAnswer];
55
+ readonly byStatus: (status: TesterFeedbackStatus) => readonly ["status", "==", TesterFeedbackStatus];
56
+ };
57
+ export declare const TESTER_CHECKLIST_ITEM_FIELDS: {
58
+ readonly ID: "id";
59
+ readonly GROUP_KEY: "groupKey";
60
+ readonly GROUP_LABEL: "groupLabel";
61
+ readonly PAGE_KEY: "pageKey";
62
+ readonly PAGE_LABEL: "pageLabel";
63
+ readonly LABEL: "label";
64
+ readonly DESCRIPTION: "description";
65
+ readonly HREF: "href";
66
+ readonly ORDER: "order";
67
+ readonly IS_ACTIVE: "isActive";
68
+ readonly SEARCH_TOKENS: "searchTokens";
69
+ readonly CREATED_AT: "createdAt";
70
+ readonly UPDATED_AT: "updatedAt";
71
+ };
72
+ export declare const TESTER_CHECKLIST_RESPONSE_FIELDS: {
73
+ readonly ID: "id";
74
+ readonly TESTER_ID: "testerId";
75
+ readonly TESTER_DISPLAY_NAME: "testerDisplayName";
76
+ readonly CHECKLIST_ITEM_ID: "checklistItemId";
77
+ readonly GROUP_KEY: "groupKey";
78
+ readonly PAGE_KEY: "pageKey";
79
+ readonly ANSWER: "answer";
80
+ readonly COMMENT: "comment";
81
+ readonly SCREENSHOT_URL: "screenshotUrl";
82
+ readonly STATUS: "status";
83
+ readonly CREATED_AT: "createdAt";
84
+ readonly UPDATED_AT: "updatedAt";
85
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Tester Feature Firestore Document Types & Constants
3
+ *
4
+ * Two distinct collections, deliberately separate:
5
+ * - TesterChecklistItemDocument — the admin-authored catalog of test cases (mirrors FAQs).
6
+ * - TesterChecklistResponseDocument — one doc per (tester, case) answer, upserted by testerId+checklistItemId.
7
+ */
8
+ import { generateChecklistItemId } from "../../../utils/id-generators";
9
+ export const TESTER_CHECKLIST_ITEM_COLLECTION = "testerChecklistItems";
10
+ export const TESTER_CHECKLIST_ITEM_ID_PREFIX = "checklist-";
11
+ export const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS = [
12
+ "groupKey",
13
+ "pageKey",
14
+ "order",
15
+ "isActive",
16
+ "searchTokens",
17
+ "createdAt",
18
+ ];
19
+ export const DEFAULT_TESTER_CHECKLIST_ITEM_DATA = {
20
+ order: 0,
21
+ isActive: true,
22
+ searchTokens: [],
23
+ };
24
+ export const testerChecklistItemQueryHelpers = {
25
+ byGroup: (groupKey) => ["groupKey", "==", groupKey],
26
+ byPage: (pageKey) => ["pageKey", "==", pageKey],
27
+ active: () => ["isActive", "==", true],
28
+ };
29
+ export function slugifyChecklistLabel(label) {
30
+ return label
31
+ .toLowerCase()
32
+ .trim()
33
+ .replace(/[^\w\s-]/g, "")
34
+ .replace(/[\s_-]+/g, "-")
35
+ .replace(/^-+|-+$/g, "")
36
+ .slice(0, 40);
37
+ }
38
+ export function createChecklistItemId(groupKey, pageKey, label) {
39
+ return generateChecklistItemId({ groupKey, pageKey, label });
40
+ }
41
+ export const TESTER_CHECKLIST_RESPONSE_COLLECTION = "testerChecklistResponses";
42
+ export const TESTER_CHECKLIST_RESPONSE_INDEXED_FIELDS = [
43
+ "testerId",
44
+ "checklistItemId",
45
+ "groupKey",
46
+ "pageKey",
47
+ "answer",
48
+ "status",
49
+ "createdAt",
50
+ ];
51
+ export const DEFAULT_TESTER_CHECKLIST_RESPONSE_DATA = {
52
+ answer: null,
53
+ status: "new",
54
+ };
55
+ export function createChecklistResponseId(testerId, checklistItemId) {
56
+ return `${testerId}__${checklistItemId}`;
57
+ }
58
+ export const testerChecklistResponseQueryHelpers = {
59
+ byTester: (testerId) => ["testerId", "==", testerId],
60
+ byItem: (checklistItemId) => ["checklistItemId", "==", checklistItemId],
61
+ byAnswer: (answer) => ["answer", "==", answer],
62
+ byStatus: (status) => ["status", "==", status],
63
+ };
64
+ // --- Field name constants -----------------------------------------------------
65
+ export const TESTER_CHECKLIST_ITEM_FIELDS = {
66
+ ID: "id",
67
+ GROUP_KEY: "groupKey",
68
+ GROUP_LABEL: "groupLabel",
69
+ PAGE_KEY: "pageKey",
70
+ PAGE_LABEL: "pageLabel",
71
+ LABEL: "label",
72
+ DESCRIPTION: "description",
73
+ HREF: "href",
74
+ ORDER: "order",
75
+ IS_ACTIVE: "isActive",
76
+ SEARCH_TOKENS: "searchTokens",
77
+ CREATED_AT: "createdAt",
78
+ UPDATED_AT: "updatedAt",
79
+ };
80
+ export const TESTER_CHECKLIST_RESPONSE_FIELDS = {
81
+ ID: "id",
82
+ TESTER_ID: "testerId",
83
+ TESTER_DISPLAY_NAME: "testerDisplayName",
84
+ CHECKLIST_ITEM_ID: "checklistItemId",
85
+ GROUP_KEY: "groupKey",
86
+ PAGE_KEY: "pageKey",
87
+ ANSWER: "answer",
88
+ COMMENT: "comment",
89
+ SCREENSHOT_URL: "screenshotUrl",
90
+ STATUS: "status",
91
+ CREATED_AT: "createdAt",
92
+ UPDATED_AT: "updatedAt",
93
+ };
@@ -0,0 +1 @@
1
+ export * from "./firestore";