amliq-dashboard 2.0.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 (492) hide show
  1. package/.env.example +3 -0
  2. package/.env.production +1 -0
  3. package/Dockerfile +26 -0
  4. package/QUICK_START.md +267 -0
  5. package/README.md +161 -0
  6. package/TESTING_GUIDE.md +322 -0
  7. package/dist/.well-known/ai-plugin.json +15 -0
  8. package/dist/_headers +8 -0
  9. package/dist/_redirects +1 -0
  10. package/dist/assets/APIKeys-BvwFauIs.js +6 -0
  11. package/dist/assets/APIKeys-DaMSBZQL.js +1 -0
  12. package/dist/assets/AdverseMedia-DckXfMzS.js +4 -0
  13. package/dist/assets/AlertDetailPage-Bc2_zwu_.js +1 -0
  14. package/dist/assets/AlertQueue-DFtBiRoM.js +8 -0
  15. package/dist/assets/Analytics-DtruB5lQ.js +1 -0
  16. package/dist/assets/AuditTrail-BK68ePu0.js +1 -0
  17. package/dist/assets/AuthDivider-gRHEt7gx.js +1 -0
  18. package/dist/assets/Badge-B9VFkofy.js +1 -0
  19. package/dist/assets/BatchJobs-Bz6KFnXD.js +6 -0
  20. package/dist/assets/BillingPage-R9nQ5nFb.js +11 -0
  21. package/dist/assets/Card-Cw-T_-oU.js +1 -0
  22. package/dist/assets/CaseDetail-DYs7Zg_y.js +7 -0
  23. package/dist/assets/CaseManagement-B1Q4Dp1Y.js +1 -0
  24. package/dist/assets/ComplianceReport-Dhssqc9T.js +1 -0
  25. package/dist/assets/Configuration-B92kl9T0.js +1 -0
  26. package/dist/assets/CryptoScreening-C7yFlskC.js +1 -0
  27. package/dist/assets/Dashboard-w4-Nhv9q.js +17 -0
  28. package/dist/assets/DataSources-u3-Ri_5_.js +3 -0
  29. package/dist/assets/EDDWorkflow-CFzlEO0e.js +1 -0
  30. package/dist/assets/EmptyState-Do0xJAT9.js +6 -0
  31. package/dist/assets/ForgotPassword-BPYyQFQp.js +1 -0
  32. package/dist/assets/LandingPage-CjaP0zw4.js +41 -0
  33. package/dist/assets/ListsMarketplace-CG6KkT9B.js +6 -0
  34. package/dist/assets/Login-677LEGP1.js +1 -0
  35. package/dist/assets/MFASetup-BxqCPvui.js +1 -0
  36. package/dist/assets/Monitoring-9TN9MK4y.js +1 -0
  37. package/dist/assets/Onboarding-Cf0Y83lX.js +1 -0
  38. package/dist/assets/Operations-CcXwc2xw.js +10 -0
  39. package/dist/assets/Overview-B_Ky0VWV.js +1 -0
  40. package/dist/assets/PEPScreening-pUQjTH9c.js +1 -0
  41. package/dist/assets/PageHeader-BObHFn0i.js +1 -0
  42. package/dist/assets/PrivacyPolicy-DZ2xrKir.js +1 -0
  43. package/dist/assets/ResetPassword-AtxtpIG1.js +1 -0
  44. package/dist/assets/RiskAssessment-Dzuh1Usv.js +1 -0
  45. package/dist/assets/SARForm-D8w2ZGe-.js +1 -0
  46. package/dist/assets/SanctionsLists-CPA3UH2v.js +1 -0
  47. package/dist/assets/ScheduledTasks-Dsjk-6UR.js +1 -0
  48. package/dist/assets/ScreenEntity-D1U0YL7v.js +3 -0
  49. package/dist/assets/ScreeningProgress-BW49PzoB.js +66 -0
  50. package/dist/assets/SearchField-CulFKdiI.js +1 -0
  51. package/dist/assets/Signup-C0FmFOo_.js +1 -0
  52. package/dist/assets/SystemHealth-B_8u4eva.js +1 -0
  53. package/dist/assets/TaskHistory-BCmEM89q.js +3 -0
  54. package/dist/assets/Team-CUo_FlU7.js +1 -0
  55. package/dist/assets/TenantDetail-CgZQHUY8.js +1 -0
  56. package/dist/assets/Tenants-RB9E9ick.js +2 -0
  57. package/dist/assets/TermsOfService-BfL-kndX.js +1 -0
  58. package/dist/assets/TransactionMonitoring-DGh_T22s.js +14 -0
  59. package/dist/assets/TxnScreening-B7cm2-eh.js +1 -0
  60. package/dist/assets/UBOChain-falXAsCo.js +1 -0
  61. package/dist/assets/Users-Doakpg9c.js +1 -0
  62. package/dist/assets/Webhooks-BZpAfaxv.js +1 -0
  63. package/dist/assets/alert-triangle-YCFVm7B_.js +6 -0
  64. package/dist/assets/alerts-COUnTwWY.js +1 -0
  65. package/dist/assets/arrow-left-9ApLqP95.js +6 -0
  66. package/dist/assets/check-VTcvVtIU.js +6 -0
  67. package/dist/assets/check-circle-2-Dn-lG5YQ.js +6 -0
  68. package/dist/assets/code-DIJRWFVv.js +6 -0
  69. package/dist/assets/fingerprint-C0MGVtax.js +6 -0
  70. package/dist/assets/flag-B8_Gwxh6.js +6 -0
  71. package/dist/assets/index-CfdYcqRM.js +289 -0
  72. package/dist/assets/index-HPU_Rhdu.css +1 -0
  73. package/dist/assets/layers-zHz2o4vo.js +6 -0
  74. package/dist/assets/loader-B9_dNihg.js +6 -0
  75. package/dist/assets/mail-BijL2qwp.js +6 -0
  76. package/dist/assets/plus-i0oBIIPS.js +6 -0
  77. package/dist/assets/refresh-cw-CSuAwutt.js +6 -0
  78. package/dist/assets/useAnalytics-Bj8IONcw.js +72 -0
  79. package/dist/assets/x-circle-DLGKvijE.js +6 -0
  80. package/dist/favicon.svg +15 -0
  81. package/dist/index.html +51 -0
  82. package/dist/llms.txt +28 -0
  83. package/dist/logo.png +0 -0
  84. package/dist/logo.svg +17 -0
  85. package/dist/manifest.json +44 -0
  86. package/dist/robots.txt +15 -0
  87. package/dist/schema.json +13 -0
  88. package/dist/sitemap.xml +28 -0
  89. package/dist/sw.js +67 -0
  90. package/e2e/auth-setup.ts +20 -0
  91. package/e2e/billing.spec.ts +50 -0
  92. package/e2e/cases.spec.ts +53 -0
  93. package/e2e/compliance.spec.ts +65 -0
  94. package/e2e/config.spec.ts +56 -0
  95. package/e2e/dashboard.spec.ts +47 -0
  96. package/e2e/fixtures.ts +33 -0
  97. package/e2e/lists.spec.ts +43 -0
  98. package/e2e/login.spec.ts +64 -0
  99. package/e2e/media.spec.ts +48 -0
  100. package/e2e/mocks.ts +51 -0
  101. package/e2e/monitoring.spec.ts +44 -0
  102. package/e2e/navigation.spec.ts +56 -0
  103. package/e2e/onboarding.spec.ts +49 -0
  104. package/e2e/responsive.spec.ts +40 -0
  105. package/e2e/risk.spec.ts +61 -0
  106. package/e2e/screening.spec.ts +76 -0
  107. package/e2e/team.spec.ts +53 -0
  108. package/index.html +50 -0
  109. package/package.json +47 -0
  110. package/playwright.config.ts +35 -0
  111. package/postcss.config.js +6 -0
  112. package/public/.well-known/ai-plugin.json +15 -0
  113. package/public/_headers +8 -0
  114. package/public/_redirects +1 -0
  115. package/public/favicon.svg +15 -0
  116. package/public/llms.txt +28 -0
  117. package/public/logo.png +0 -0
  118. package/public/logo.svg +17 -0
  119. package/public/manifest.json +44 -0
  120. package/public/robots.txt +15 -0
  121. package/public/schema.json +13 -0
  122. package/public/sitemap.xml +28 -0
  123. package/public/sw.js +67 -0
  124. package/scripts/test-runner.sh +152 -0
  125. package/src/App.tsx +48 -0
  126. package/src/api/alerts.ts +19 -0
  127. package/src/api/analytics.ts +6 -0
  128. package/src/api/audit.ts +11 -0
  129. package/src/api/auth.ts +26 -0
  130. package/src/api/billing.ts +54 -0
  131. package/src/api/cases.ts +48 -0
  132. package/src/api/client.ts +50 -0
  133. package/src/api/config.ts +30 -0
  134. package/src/api/edd.ts +25 -0
  135. package/src/api/enforcement.ts +33 -0
  136. package/src/api/lists.ts +24 -0
  137. package/src/api/monitoring.ts +82 -0
  138. package/src/api/pep.ts +30 -0
  139. package/src/api/risk.ts +26 -0
  140. package/src/api/screening.ts +37 -0
  141. package/src/api/team.ts +27 -0
  142. package/src/api/transactions.ts +37 -0
  143. package/src/components/admin/TenantCards.tsx +51 -0
  144. package/src/components/alerts/AlertActions.test.tsx +80 -0
  145. package/src/components/alerts/AlertActions.tsx +68 -0
  146. package/src/components/alerts/AlertCard.test.tsx +86 -0
  147. package/src/components/alerts/AlertCard.tsx +60 -0
  148. package/src/components/alerts/AlertDetailSidebar.tsx +63 -0
  149. package/src/components/alerts/AlertFilters.test.tsx +73 -0
  150. package/src/components/alerts/AlertFilters.tsx +88 -0
  151. package/src/components/alerts/EntityDetailsCard.test.tsx +61 -0
  152. package/src/components/alerts/EntityDetailsCard.tsx +37 -0
  153. package/src/components/alerts/NotesCard.test.tsx +39 -0
  154. package/src/components/alerts/NotesCard.tsx +28 -0
  155. package/src/components/auth/AuthDivider.tsx +18 -0
  156. package/src/components/auth/LoginForm.tsx +62 -0
  157. package/src/components/auth/LoginLeftPanel.tsx +43 -0
  158. package/src/components/auth/PasswordStrength.tsx +26 -0
  159. package/src/components/auth/SignInButtons.tsx +46 -0
  160. package/src/components/auth/SignupLeftPanel.tsx +35 -0
  161. package/src/components/auth/countries.ts +17 -0
  162. package/src/components/charts/AreaChart.tsx +45 -0
  163. package/src/components/charts/BarChart.tsx +48 -0
  164. package/src/components/charts/DonutChart.tsx +47 -0
  165. package/src/components/compliance/CaseActions.tsx +74 -0
  166. package/src/components/compliance/CaseTimeline.tsx +68 -0
  167. package/src/components/compliance/MediaResultCard.tsx +87 -0
  168. package/src/components/compliance/PEPResultCard.tsx +78 -0
  169. package/src/components/config/MatchingModesCard.test.tsx +75 -0
  170. package/src/components/config/MatchingModesCard.tsx +40 -0
  171. package/src/components/config/ScreeningLayersCard.test.tsx +57 -0
  172. package/src/components/config/ScreeningLayersCard.tsx +36 -0
  173. package/src/components/config/ThresholdsCard.test.tsx +79 -0
  174. package/src/components/config/ThresholdsCard.tsx +51 -0
  175. package/src/components/dashboard/ActivityFeed.tsx +93 -0
  176. package/src/components/dashboard/DashboardGreeting.tsx +23 -0
  177. package/src/components/dashboard/DashboardSkeleton.tsx +35 -0
  178. package/src/components/dashboard/QuickActions.tsx +52 -0
  179. package/src/components/dashboard/TopEntitiesTable.tsx +63 -0
  180. package/src/components/data/ComplianceMetrics.test.tsx +32 -0
  181. package/src/components/data/ComplianceMetrics.tsx +40 -0
  182. package/src/components/data/ConfidenceScore.test.tsx +62 -0
  183. package/src/components/data/ConfidenceScore.tsx +27 -0
  184. package/src/components/data/StatCard.test.tsx +72 -0
  185. package/src/components/data/StatCard.tsx +63 -0
  186. package/src/components/data/StatusBadge.test.tsx +63 -0
  187. package/src/components/data/StatusBadge.tsx +39 -0
  188. package/src/components/layout/AppShell.tsx +48 -0
  189. package/src/components/layout/Breadcrumbs.tsx +48 -0
  190. package/src/components/layout/CommandPalette.tsx +81 -0
  191. package/src/components/layout/DashboardLayout.tsx +19 -0
  192. package/src/components/layout/MobileHeader.tsx +30 -0
  193. package/src/components/layout/NavGroup.test.tsx +63 -0
  194. package/src/components/layout/NavGroup.tsx +64 -0
  195. package/src/components/layout/NotificationBell.tsx +89 -0
  196. package/src/components/layout/PageHeader.test.tsx +61 -0
  197. package/src/components/layout/PageHeader.tsx +19 -0
  198. package/src/components/layout/ProtectedRoute.tsx +31 -0
  199. package/src/components/layout/PublicLayout.tsx +17 -0
  200. package/src/components/layout/Sidebar.test.tsx +67 -0
  201. package/src/components/layout/Sidebar.tsx +92 -0
  202. package/src/components/layout/Toolbar.test.tsx +47 -0
  203. package/src/components/layout/Toolbar.tsx +68 -0
  204. package/src/components/layout/navItems.ts +94 -0
  205. package/src/components/lists/ListCard.tsx +78 -0
  206. package/src/components/lists/ListMarketplaceCard.tsx +77 -0
  207. package/src/components/screening/CircularConfidence.tsx +38 -0
  208. package/src/components/screening/LimitReachedBanner.tsx +31 -0
  209. package/src/components/screening/ListSelector.tsx +64 -0
  210. package/src/components/screening/MatchDetailHeader.tsx +64 -0
  211. package/src/components/screening/MatchEntityInfo.tsx +56 -0
  212. package/src/components/screening/MatchEvidenceBars.tsx +65 -0
  213. package/src/components/screening/MatchMetadata.tsx +95 -0
  214. package/src/components/screening/ScreenResults.tsx +49 -0
  215. package/src/components/screening/ScreeningForm.test.tsx +83 -0
  216. package/src/components/screening/ScreeningForm.tsx +100 -0
  217. package/src/components/screening/ScreeningLayersList.test.tsx +33 -0
  218. package/src/components/screening/ScreeningLayersList.tsx +46 -0
  219. package/src/components/screening/ScreeningProgress.tsx +91 -0
  220. package/src/components/screening/ScreeningQuotaBanner.tsx +64 -0
  221. package/src/components/screening/ScreeningResultCard.tsx +47 -0
  222. package/src/components/screening/ScreeningResultRow.tsx +45 -0
  223. package/src/components/screening/ScreeningResults.test.tsx +37 -0
  224. package/src/components/screening/ScreeningResults.tsx +33 -0
  225. package/src/components/screening/ShareResults.tsx +103 -0
  226. package/src/components/screening/ThresholdSlider.tsx +23 -0
  227. package/src/components/transactions/WebhookCTA.tsx +63 -0
  228. package/src/components/ui/Avatar.test.tsx +47 -0
  229. package/src/components/ui/Avatar.tsx +35 -0
  230. package/src/components/ui/Badge.test.tsx +49 -0
  231. package/src/components/ui/Badge.tsx +33 -0
  232. package/src/components/ui/Button.test.tsx +56 -0
  233. package/src/components/ui/Button.tsx +46 -0
  234. package/src/components/ui/Card.test.tsx +61 -0
  235. package/src/components/ui/Card.tsx +29 -0
  236. package/src/components/ui/ConfirmModal.tsx +67 -0
  237. package/src/components/ui/Divider.test.tsx +24 -0
  238. package/src/components/ui/Divider.tsx +5 -0
  239. package/src/components/ui/EmptyState.test.tsx +49 -0
  240. package/src/components/ui/EmptyState.tsx +22 -0
  241. package/src/components/ui/ErrorBoundary.tsx +44 -0
  242. package/src/components/ui/ExportMenu.tsx +71 -0
  243. package/src/components/ui/LanguageSwitcher.tsx +37 -0
  244. package/src/components/ui/LoadingSpinner.test.tsx +41 -0
  245. package/src/components/ui/LoadingSpinner.tsx +21 -0
  246. package/src/components/ui/MetricCard.tsx +63 -0
  247. package/src/components/ui/ScoreRing.tsx +51 -0
  248. package/src/components/ui/SearchField.test.tsx +55 -0
  249. package/src/components/ui/SearchField.tsx +31 -0
  250. package/src/components/ui/SeverityBadge.tsx +57 -0
  251. package/src/components/ui/ThemeToggle.tsx +37 -0
  252. package/src/components/ui/Toast.test.tsx +79 -0
  253. package/src/components/ui/Toast.tsx +75 -0
  254. package/src/components/ui/Toggle.test.tsx +85 -0
  255. package/src/components/ui/Toggle.tsx +46 -0
  256. package/src/context/AuthContext.tsx +71 -0
  257. package/src/data/pepProfiles.ts +76 -0
  258. package/src/data/pepProfilesExtra.ts +58 -0
  259. package/src/hooks/useAlerts.ts +36 -0
  260. package/src/hooks/useAnalytics.ts +23 -0
  261. package/src/hooks/useApi.test.ts +79 -0
  262. package/src/hooks/useApi.ts +33 -0
  263. package/src/hooks/useAudit.ts +28 -0
  264. package/src/hooks/useBilling.ts +38 -0
  265. package/src/hooks/useConfig.ts +35 -0
  266. package/src/hooks/useDebounce.test.ts +84 -0
  267. package/src/hooks/useDebounce.ts +15 -0
  268. package/src/hooks/useDirection.ts +15 -0
  269. package/src/hooks/useLists.ts +34 -0
  270. package/src/hooks/useMediaQuery.test.ts +97 -0
  271. package/src/hooks/useMediaQuery.ts +28 -0
  272. package/src/hooks/useScreening.ts +33 -0
  273. package/src/hooks/useSidebar.ts +18 -0
  274. package/src/hooks/useUsage.ts +27 -0
  275. package/src/i18n/config.ts +33 -0
  276. package/src/i18n/locales/ar/admin.json +19 -0
  277. package/src/i18n/locales/ar/alerts.json +52 -0
  278. package/src/i18n/locales/ar/analytics.json +9 -0
  279. package/src/i18n/locales/ar/audit.json +12 -0
  280. package/src/i18n/locales/ar/auth.json +60 -0
  281. package/src/i18n/locales/ar/batch.json +5 -0
  282. package/src/i18n/locales/ar/billing.json +41 -0
  283. package/src/i18n/locales/ar/common.json +65 -0
  284. package/src/i18n/locales/ar/compliance.json +83 -0
  285. package/src/i18n/locales/ar/config.json +13 -0
  286. package/src/i18n/locales/ar/dashboard.json +19 -0
  287. package/src/i18n/locales/ar/errors.json +9 -0
  288. package/src/i18n/locales/ar/index.ts +29 -0
  289. package/src/i18n/locales/ar/legal.json +10 -0
  290. package/src/i18n/locales/ar/lists.json +6 -0
  291. package/src/i18n/locales/ar/marketing.json +110 -0
  292. package/src/i18n/locales/ar/monitoring.json +15 -0
  293. package/src/i18n/locales/ar/nav.json +35 -0
  294. package/src/i18n/locales/ar/onboarding.json +25 -0
  295. package/src/i18n/locales/ar/platform.json +23 -0
  296. package/src/i18n/locales/ar/screening.json +26 -0
  297. package/src/i18n/locales/ar/team.json +11 -0
  298. package/src/i18n/locales/en/admin.json +21 -0
  299. package/src/i18n/locales/en/alerts.json +52 -0
  300. package/src/i18n/locales/en/analytics.json +9 -0
  301. package/src/i18n/locales/en/audit.json +12 -0
  302. package/src/i18n/locales/en/auth.json +60 -0
  303. package/src/i18n/locales/en/batch.json +5 -0
  304. package/src/i18n/locales/en/billing.json +87 -0
  305. package/src/i18n/locales/en/common.json +65 -0
  306. package/src/i18n/locales/en/compliance.json +83 -0
  307. package/src/i18n/locales/en/config.json +29 -0
  308. package/src/i18n/locales/en/dashboard.json +23 -0
  309. package/src/i18n/locales/en/errors.json +9 -0
  310. package/src/i18n/locales/en/index.ts +29 -0
  311. package/src/i18n/locales/en/legal.json +114 -0
  312. package/src/i18n/locales/en/lists.json +6 -0
  313. package/src/i18n/locales/en/marketing.json +174 -0
  314. package/src/i18n/locales/en/monitoring.json +15 -0
  315. package/src/i18n/locales/en/nav.json +35 -0
  316. package/src/i18n/locales/en/onboarding.json +31 -0
  317. package/src/i18n/locales/en/platform.json +25 -0
  318. package/src/i18n/locales/en/screening.json +26 -0
  319. package/src/i18n/locales/en/team.json +12 -0
  320. package/src/i18n/locales/he/admin.json +19 -0
  321. package/src/i18n/locales/he/alerts.json +52 -0
  322. package/src/i18n/locales/he/analytics.json +9 -0
  323. package/src/i18n/locales/he/audit.json +12 -0
  324. package/src/i18n/locales/he/auth.json +60 -0
  325. package/src/i18n/locales/he/batch.json +5 -0
  326. package/src/i18n/locales/he/billing.json +41 -0
  327. package/src/i18n/locales/he/common.json +65 -0
  328. package/src/i18n/locales/he/compliance.json +83 -0
  329. package/src/i18n/locales/he/config.json +13 -0
  330. package/src/i18n/locales/he/dashboard.json +19 -0
  331. package/src/i18n/locales/he/errors.json +9 -0
  332. package/src/i18n/locales/he/index.ts +29 -0
  333. package/src/i18n/locales/he/legal.json +10 -0
  334. package/src/i18n/locales/he/lists.json +6 -0
  335. package/src/i18n/locales/he/marketing.json +110 -0
  336. package/src/i18n/locales/he/monitoring.json +15 -0
  337. package/src/i18n/locales/he/nav.json +35 -0
  338. package/src/i18n/locales/he/onboarding.json +25 -0
  339. package/src/i18n/locales/he/platform.json +23 -0
  340. package/src/i18n/locales/he/screening.json +26 -0
  341. package/src/i18n/locales/he/team.json +11 -0
  342. package/src/index.css +112 -0
  343. package/src/main.tsx +15 -0
  344. package/src/pages/APIKeys.tsx +120 -0
  345. package/src/pages/AddMonitorModal.tsx +30 -0
  346. package/src/pages/AdverseMedia.test.tsx +18 -0
  347. package/src/pages/AdverseMedia.tsx +89 -0
  348. package/src/pages/AlertDetailPage.tsx +64 -0
  349. package/src/pages/AlertQueue.test.tsx +48 -0
  350. package/src/pages/AlertQueue.tsx +63 -0
  351. package/src/pages/Analytics.tsx +50 -0
  352. package/src/pages/AuditTrail.tsx +64 -0
  353. package/src/pages/BatchJobs.tsx +79 -0
  354. package/src/pages/CaseDetail.tsx +72 -0
  355. package/src/pages/CaseManagement.test.tsx +31 -0
  356. package/src/pages/CaseManagement.tsx +92 -0
  357. package/src/pages/Configuration.test.tsx +96 -0
  358. package/src/pages/Configuration.tsx +123 -0
  359. package/src/pages/CryptoScreening.tsx +109 -0
  360. package/src/pages/Dashboard.test.tsx +51 -0
  361. package/src/pages/Dashboard.tsx +66 -0
  362. package/src/pages/EDDWorkflow.test.tsx +29 -0
  363. package/src/pages/EDDWorkflow.tsx +73 -0
  364. package/src/pages/ForgotPassword.test.tsx +49 -0
  365. package/src/pages/ForgotPassword.tsx +67 -0
  366. package/src/pages/ListsMarketplace.tsx +102 -0
  367. package/src/pages/Login.test.tsx +100 -0
  368. package/src/pages/Login.tsx +57 -0
  369. package/src/pages/MFASetup.tsx +114 -0
  370. package/src/pages/MonitorProfileCard.tsx +27 -0
  371. package/src/pages/Monitoring.tsx +68 -0
  372. package/src/pages/Onboarding.test.tsx +36 -0
  373. package/src/pages/Onboarding.tsx +60 -0
  374. package/src/pages/PEPScreening.test.tsx +15 -0
  375. package/src/pages/PEPScreening.tsx +100 -0
  376. package/src/pages/ResetPassword.tsx +81 -0
  377. package/src/pages/RiskAssessment.test.tsx +15 -0
  378. package/src/pages/RiskAssessment.tsx +108 -0
  379. package/src/pages/SanctionsLists.tsx +74 -0
  380. package/src/pages/ScreenEntity.test.tsx +82 -0
  381. package/src/pages/ScreenEntity.tsx +76 -0
  382. package/src/pages/Signup.test.tsx +98 -0
  383. package/src/pages/Signup.tsx +92 -0
  384. package/src/pages/TaskHistory.tsx +183 -0
  385. package/src/pages/Team.test.tsx +15 -0
  386. package/src/pages/Team.tsx +140 -0
  387. package/src/pages/TransactionMonitoring.test.tsx +18 -0
  388. package/src/pages/TransactionMonitoring.tsx +118 -0
  389. package/src/pages/TxnScreening.tsx +125 -0
  390. package/src/pages/UBOChain.test.tsx +35 -0
  391. package/src/pages/UBOChain.tsx +65 -0
  392. package/src/pages/Webhooks.tsx +137 -0
  393. package/src/pages/admin/DataSources.tsx +230 -0
  394. package/src/pages/admin/Operations.tsx +103 -0
  395. package/src/pages/admin/ScheduledTasks.tsx +155 -0
  396. package/src/pages/admin/SystemHealth.tsx +58 -0
  397. package/src/pages/admin/TenantDetail.tsx +62 -0
  398. package/src/pages/admin/Tenants.test.tsx +20 -0
  399. package/src/pages/admin/Tenants.tsx +63 -0
  400. package/src/pages/admin/opsRunners.ts +81 -0
  401. package/src/pages/admin/opsTerminal.tsx +63 -0
  402. package/src/pages/billing/ActiveSubscriptions.tsx +41 -0
  403. package/src/pages/billing/AddProductModal.tsx +99 -0
  404. package/src/pages/billing/BillingPage.test.tsx +30 -0
  405. package/src/pages/billing/BillingPage.tsx +67 -0
  406. package/src/pages/billing/CurrentPlan.tsx +50 -0
  407. package/src/pages/billing/InvoiceList.tsx +104 -0
  408. package/src/pages/billing/InvoiceRow.tsx +37 -0
  409. package/src/pages/billing/LemonSqueezySetup.tsx +51 -0
  410. package/src/pages/billing/PaymentAlert.tsx +33 -0
  411. package/src/pages/billing/PlanComparison.tsx +53 -0
  412. package/src/pages/billing/ProductUsage.tsx +43 -0
  413. package/src/pages/billing/PromoCodeInput.tsx +58 -0
  414. package/src/pages/billing/SeatManager.tsx +80 -0
  415. package/src/pages/billing/SeatRow.tsx +32 -0
  416. package/src/pages/billing/SubscriptionCard.tsx +73 -0
  417. package/src/pages/billing/UpgradeModal.tsx +53 -0
  418. package/src/pages/billing/UsageHistory.tsx +37 -0
  419. package/src/pages/billing/UsageMeter.tsx +26 -0
  420. package/src/pages/billing/UsageOverview.tsx +38 -0
  421. package/src/pages/legal/PrivacyPolicy.tsx +25 -0
  422. package/src/pages/legal/TermsOfService.tsx +25 -0
  423. package/src/pages/marketing/BundleCallout.tsx +24 -0
  424. package/src/pages/marketing/CTASection.tsx +48 -0
  425. package/src/pages/marketing/CaseStudy.tsx +37 -0
  426. package/src/pages/marketing/ComparisonTable.tsx +66 -0
  427. package/src/pages/marketing/CompetitiveEdge.tsx +55 -0
  428. package/src/pages/marketing/DataCoverage.tsx +54 -0
  429. package/src/pages/marketing/DataRain.tsx +30 -0
  430. package/src/pages/marketing/EnterpriseCTA.tsx +26 -0
  431. package/src/pages/marketing/FAQItem.tsx +27 -0
  432. package/src/pages/marketing/FAQSchema.tsx +43 -0
  433. package/src/pages/marketing/FAQSection.tsx +19 -0
  434. package/src/pages/marketing/FeatureDetail.tsx +19 -0
  435. package/src/pages/marketing/FeaturesGrid.tsx +60 -0
  436. package/src/pages/marketing/FeaturesSpotlight.tsx +68 -0
  437. package/src/pages/marketing/FooterSection.tsx +92 -0
  438. package/src/pages/marketing/GradientOrbs.tsx +26 -0
  439. package/src/pages/marketing/HeroSearch.tsx +113 -0
  440. package/src/pages/marketing/HeroSection.tsx +72 -0
  441. package/src/pages/marketing/LandingPage.tsx +45 -0
  442. package/src/pages/marketing/LogoCloud.tsx +31 -0
  443. package/src/pages/marketing/LogoMarquee.tsx +45 -0
  444. package/src/pages/marketing/MarketingNav.tsx +80 -0
  445. package/src/pages/marketing/MatchingLayers.tsx +78 -0
  446. package/src/pages/marketing/MobileMenu.tsx +45 -0
  447. package/src/pages/marketing/PricingCard.tsx +57 -0
  448. package/src/pages/marketing/PricingFeatureRow.tsx +16 -0
  449. package/src/pages/marketing/PricingSection.tsx +103 -0
  450. package/src/pages/marketing/PricingToggle.tsx +40 -0
  451. package/src/pages/marketing/ProductPricingCards.tsx +23 -0
  452. package/src/pages/marketing/ProductShowcase.tsx +81 -0
  453. package/src/pages/marketing/ProductTabs.tsx +45 -0
  454. package/src/pages/marketing/QuoteRotator.tsx +56 -0
  455. package/src/pages/marketing/StatsBar.tsx +81 -0
  456. package/src/pages/marketing/StatsSection.tsx +44 -0
  457. package/src/pages/marketing/TestimonialCard.tsx +38 -0
  458. package/src/pages/marketing/TestimonialsSection.tsx +32 -0
  459. package/src/pages/marketing/animations.tsx +56 -0
  460. package/src/pages/onboarding/CountryStep.tsx +38 -0
  461. package/src/pages/onboarding/ListsStep.tsx +44 -0
  462. package/src/pages/onboarding/StepIndicator.tsx +34 -0
  463. package/src/pages/onboarding/ThresholdStep.tsx +49 -0
  464. package/src/pages/platform/APIKeys.tsx +102 -0
  465. package/src/pages/platform/Overview.tsx +58 -0
  466. package/src/pages/platform/Users.tsx +110 -0
  467. package/src/pages/reporting/ComplianceReport.tsx +99 -0
  468. package/src/pages/reporting/SARForm.tsx +99 -0
  469. package/src/routes/appRoutes.tsx +60 -0
  470. package/src/routes/compliance.tsx +28 -0
  471. package/src/routes/lazyCompliance.ts +34 -0
  472. package/src/routes/lazyPages.ts +35 -0
  473. package/src/routes/lazyPlatform.ts +5 -0
  474. package/src/routes/platform.tsx +15 -0
  475. package/src/styles/effects.css +76 -0
  476. package/src/test/setup.ts +25 -0
  477. package/src/test/utils.tsx +49 -0
  478. package/src/types/alert.ts +31 -0
  479. package/src/types/analytics.ts +16 -0
  480. package/src/types/audit.ts +11 -0
  481. package/src/types/billing.ts +60 -0
  482. package/src/types/common.ts +15 -0
  483. package/src/types/config.ts +19 -0
  484. package/src/types/entity.ts +34 -0
  485. package/src/types/index.ts +8 -0
  486. package/src/types/list.ts +15 -0
  487. package/src/types/screening.ts +32 -0
  488. package/src/vite-env.d.ts +1 -0
  489. package/tailwind.config.js +65 -0
  490. package/tsconfig.json +22 -0
  491. package/vite.config.ts +11 -0
  492. package/vitest.config.ts +19 -0
@@ -0,0 +1,98 @@
1
+ import { render, screen, waitFor } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
4
+ import { MemoryRouter } from 'react-router-dom'
5
+
6
+ const mockSignup = vi.fn()
7
+ const mockNavigate = vi.fn()
8
+
9
+ vi.mock('react-router-dom', async () => ({
10
+ ...(await vi.importActual('react-router-dom')),
11
+ useNavigate: () => mockNavigate,
12
+ }))
13
+
14
+ vi.mock('../context/AuthContext', () => ({
15
+ useAuth: () => ({ signup: mockSignup }),
16
+ }))
17
+
18
+ vi.mock('../components/auth/SignInButtons', () => ({
19
+ default: () => <div data-testid="sign-in-buttons" />,
20
+ }))
21
+
22
+ vi.mock('../components/auth/AuthDivider', () => ({
23
+ default: () => <div data-testid="auth-divider" />,
24
+ }))
25
+
26
+ import { Signup } from './Signup'
27
+
28
+ const renderSignup = () =>
29
+ render(
30
+ <MemoryRouter>
31
+ <Signup />
32
+ </MemoryRouter>
33
+ )
34
+
35
+ describe('Signup', () => {
36
+ beforeEach(() => { vi.clearAllMocks() })
37
+
38
+ it('renders all form fields', () => {
39
+ renderSignup()
40
+ expect(screen.getByLabelText('Organization name')).toBeInTheDocument()
41
+ expect(screen.getByLabelText('Email')).toBeInTheDocument()
42
+ expect(screen.getByLabelText('Password')).toBeInTheDocument()
43
+ expect(screen.getByLabelText('Country')).toBeInTheDocument()
44
+ })
45
+
46
+ it('renders submit button', () => {
47
+ renderSignup()
48
+ expect(screen.getByRole('button', { name: /create account/i })).toBeInTheDocument()
49
+ })
50
+
51
+ it('submits form data correctly', async () => {
52
+ mockSignup.mockResolvedValue(undefined)
53
+ renderSignup()
54
+ await userEvent.type(screen.getByLabelText('Organization name'), 'Acme')
55
+ await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
56
+ await userEvent.type(screen.getByLabelText('Password'), 'password123')
57
+ await userEvent.click(screen.getByRole('button', { name: /create account/i }))
58
+ expect(mockSignup).toHaveBeenCalledWith('a@b.com', 'password123', 'Acme', 'US')
59
+ })
60
+
61
+ it('navigates to onboarding on success', async () => {
62
+ mockSignup.mockResolvedValue(undefined)
63
+ renderSignup()
64
+ await userEvent.type(screen.getByLabelText('Organization name'), 'Acme')
65
+ await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
66
+ await userEvent.type(screen.getByLabelText('Password'), 'password123')
67
+ await userEvent.click(screen.getByRole('button', { name: /create account/i }))
68
+ await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/onboarding'))
69
+ })
70
+
71
+ it('displays error on signup failure', async () => {
72
+ mockSignup.mockRejectedValue(new Error('Email taken'))
73
+ renderSignup()
74
+ await userEvent.type(screen.getByLabelText('Organization name'), 'Acme')
75
+ await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
76
+ await userEvent.type(screen.getByLabelText('Password'), 'password123')
77
+ await userEvent.click(screen.getByRole('button', { name: /create account/i }))
78
+ await waitFor(() => {
79
+ expect(screen.getByRole('alert')).toHaveTextContent('Email taken')
80
+ })
81
+ })
82
+
83
+ it('shows loading state during submission', async () => {
84
+ mockSignup.mockImplementation(() => new Promise(() => {}))
85
+ renderSignup()
86
+ await userEvent.type(screen.getByLabelText('Organization name'), 'Acme')
87
+ await userEvent.type(screen.getByLabelText('Email'), 'a@b.com')
88
+ await userEvent.type(screen.getByLabelText('Password'), 'pass1234')
89
+ await userEvent.click(screen.getByRole('button', { name: /create account/i }))
90
+ expect(screen.getByRole('button', { name: /creating account/i })).toBeDisabled()
91
+ })
92
+
93
+ it('renders terms and privacy links', () => {
94
+ renderSignup()
95
+ expect(screen.getByText(/terms/i)).toBeInTheDocument()
96
+ expect(screen.getByText(/privacy policy/i)).toBeInTheDocument()
97
+ })
98
+ })
@@ -0,0 +1,92 @@
1
+ import React, { useState } from 'react'
2
+ import { useNavigate, Link } from 'react-router-dom'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { useAuth } from '../context/AuthContext'
5
+ import SignInButtons from '../components/auth/SignInButtons'
6
+ import AuthDivider from '../components/auth/AuthDivider'
7
+ import { COUNTRIES } from '../components/auth/countries'
8
+ import { SignupLeftPanel } from '../components/auth/SignupLeftPanel'
9
+ import { PasswordStrength } from '../components/auth/PasswordStrength'
10
+
11
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
12
+
13
+ export function Signup() {
14
+ const navigate = useNavigate()
15
+ const { t } = useTranslation('auth')
16
+ const { signup } = useAuth()
17
+ const [form, setForm] = useState({ orgName: '', email: '', password: '', country: 'US' })
18
+ const [error, setError] = useState('')
19
+ const [loading, setLoading] = useState(false)
20
+
21
+ const set = (key: string) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
22
+ setForm((f) => ({ ...f, [key]: e.target.value }))
23
+
24
+ const handleSubmit = async (e: React.FormEvent) => {
25
+ e.preventDefault()
26
+ if (!EMAIL_RE.test(form.email)) { setError('Please enter a valid email address.'); return }
27
+ if (form.password.length < 8) { setError('Password must be at least 8 characters.'); return }
28
+ setLoading(true); setError('')
29
+ try {
30
+ await signup(form.email, form.password, form.orgName, form.country)
31
+ navigate('/onboarding')
32
+ } catch (err) {
33
+ setError(err instanceof Error ? err.message : t('signup.failed'))
34
+ } finally { setLoading(false) }
35
+ }
36
+
37
+ return (
38
+ <div className="min-h-screen bg-[#1C1C1E] flex">
39
+ <SignupLeftPanel />
40
+ <div className="flex-1 flex flex-col items-center justify-center px-6 py-10">
41
+ <Link to="/" className="mb-8 lg:hidden">
42
+ <img src="/logo.svg" alt="AMLIQ" className="h-8 w-8" />
43
+ </Link>
44
+ <div className="w-full max-w-sm space-y-6">
45
+ <div className="text-center">
46
+ <h2 className="text-xl font-semibold text-white mb-2">{t('signup.title')}</h2>
47
+ <p className="text-sm text-apple-label-secondary">{t('signup.subtitle')}</p>
48
+ </div>
49
+ <SignInButtons action="sign_up" />
50
+ <AuthDivider />
51
+ {error && (
52
+ <div role="alert" className="p-3 bg-apple-red/10 border border-apple-red/20 rounded-lg">
53
+ <p className="text-apple-red text-sm">{error}</p>
54
+ </div>
55
+ )}
56
+ <form onSubmit={handleSubmit} className="space-y-3">
57
+ <input type="text" value={form.orgName} onChange={set('orgName')} required
58
+ aria-label="Organization name" autoComplete="organization"
59
+ placeholder={t('signup.org_placeholder')} className="input-field w-full" />
60
+ <input type="email" value={form.email} onChange={set('email')} required
61
+ aria-label="Email" autoComplete="email" pattern="[^\s@]+@[^\s@]+\.[^\s@]+"
62
+ placeholder={t('signup.email_placeholder')} className="input-field w-full" />
63
+ <div>
64
+ <input type="password" value={form.password} onChange={set('password')} required
65
+ aria-label="Password" autoComplete="new-password" minLength={8}
66
+ placeholder={t('signup.password_placeholder')} className="input-field w-full" />
67
+ <PasswordStrength password={form.password} />
68
+ </div>
69
+ <select value={form.country} onChange={set('country')} aria-label="Country"
70
+ className="input-field w-full">
71
+ {COUNTRIES.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
72
+ </select>
73
+ <button type="submit" disabled={loading} aria-busy={loading}
74
+ className="button-primary w-full text-center disabled:opacity-50">
75
+ {loading ? t('signup.submitting') : t('signup.submit')}
76
+ </button>
77
+ </form>
78
+ <p className="text-center text-xs text-apple-label-secondary leading-relaxed">
79
+ {t('signup.agree_prefix')}{' '}
80
+ <Link to="/terms" className="text-apple-blue hover:underline">{t('signup.terms_link')}</Link>{' '}
81
+ {t('signup.and')}{' '}
82
+ <Link to="/privacy" className="text-apple-blue hover:underline">{t('signup.privacy_link')}</Link>.
83
+ </p>
84
+ <p className="text-center text-xs text-apple-label-tertiary">
85
+ {t('signup.have_account')}{' '}
86
+ <Link to="/login" className="text-apple-blue hover:underline">{t('signup.sign_in_link')}</Link>
87
+ </p>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
@@ -0,0 +1,183 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Clock, CheckCircle, XCircle, Loader, Bell } from 'lucide-react'
3
+ import { PageHeader } from '../components/layout/PageHeader'
4
+ import { LoadingSpinner } from '../components/ui/LoadingSpinner'
5
+ import { Badge } from '../components/ui/Badge'
6
+ import { api } from '../api/client'
7
+
8
+ interface TaskEntry {
9
+ id: string
10
+ task_name: string
11
+ trigger: string
12
+ status: 'running' | 'success' | 'failed' | 'canceled'
13
+ started_at: string
14
+ duration_ms: number
15
+ output?: string
16
+ error?: string
17
+ }
18
+
19
+ interface AlertCfg {
20
+ email?: string
21
+ whatsapp?: string
22
+ slack_webhook?: string
23
+ enabled: boolean
24
+ }
25
+
26
+ export function TaskHistory() {
27
+ const [tasks, setTasks] = useState<TaskEntry[]>([])
28
+ const [loading, setLoading] = useState(true)
29
+ const [alertCfg, setAlertCfg] = useState<AlertCfg>({ enabled: false })
30
+ const [showAlerts, setShowAlerts] = useState(false)
31
+
32
+ useEffect(() => {
33
+ api.get<{ tasks: TaskEntry[] }>('/tasks?limit=50')
34
+ .then(d => setTasks(d?.tasks ?? []))
35
+ .catch(() => {})
36
+ .finally(() => setLoading(false))
37
+ api.get<AlertCfg>('/tasks/alerts')
38
+ .then(d => { if (d) setAlertCfg(d) })
39
+ .catch(() => {})
40
+ }, [])
41
+
42
+ const saveAlertCfg = async () => {
43
+ await api.put('/tasks/alerts', alertCfg)
44
+ setShowAlerts(false)
45
+ }
46
+
47
+ if (loading) {
48
+ return (
49
+ <div className="flex items-center justify-center h-96">
50
+ <LoadingSpinner />
51
+ </div>
52
+ )
53
+ }
54
+
55
+ return (
56
+ <div>
57
+ <PageHeader
58
+ title="Task History"
59
+ description="View scheduled task outcomes and set failure alerts"
60
+ />
61
+
62
+ <div className="flex justify-end mb-lg">
63
+ <button
64
+ onClick={() => setShowAlerts(!showAlerts)}
65
+ className="flex items-center gap-sm px-md py-sm rounded-apple-lg
66
+ bg-white/10 hover:bg-white/15 text-sm cursor-pointer"
67
+ >
68
+ <Bell className="w-4 h-4" />
69
+ Alert Settings
70
+ </button>
71
+ </div>
72
+
73
+ {showAlerts && (
74
+ <AlertSettings
75
+ cfg={alertCfg}
76
+ onChange={setAlertCfg}
77
+ onSave={saveAlertCfg}
78
+ />
79
+ )}
80
+
81
+ {/* Desktop */}
82
+ <div className="hidden md:block glass-panel rounded-apple-lg overflow-hidden">
83
+ <table className="w-full text-sm">
84
+ <thead>
85
+ <tr className="text-left text-apple-label-secondary border-b border-white/10">
86
+ <th className="px-lg py-md">Task</th>
87
+ <th className="px-lg py-md">Status</th>
88
+ <th className="px-lg py-md">Duration</th>
89
+ <th className="px-lg py-md">Started</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody>
93
+ {tasks.map(t => (
94
+ <tr key={t.id} className="border-b border-white/5 hover:bg-white/5">
95
+ <td className="px-lg py-md">{t.task_name}</td>
96
+ <td className="px-lg py-md"><StatusIcon status={t.status} /></td>
97
+ <td className="px-lg py-md">{fmtMs(t.duration_ms)}</td>
98
+ <td className="px-lg py-md text-xs text-apple-label-secondary">
99
+ {new Date(t.started_at).toLocaleString()}
100
+ </td>
101
+ </tr>
102
+ ))}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+
107
+ {/* Mobile */}
108
+ <div className="md:hidden space-y-sm">
109
+ {tasks.map(t => (
110
+ <div key={t.id} className="glass-panel rounded-apple-lg p-lg">
111
+ <div className="flex items-center justify-between mb-xs">
112
+ <span className="font-medium text-sm">{t.task_name}</span>
113
+ <StatusIcon status={t.status} />
114
+ </div>
115
+ <span className="sf-caption text-apple-label-secondary">
116
+ {fmtMs(t.duration_ms)} &middot; {new Date(t.started_at).toLocaleString()}
117
+ </span>
118
+ </div>
119
+ ))}
120
+ </div>
121
+
122
+ {tasks.length === 0 && (
123
+ <p className="text-center py-xl text-apple-label-tertiary sf-body">
124
+ No task history available
125
+ </p>
126
+ )}
127
+ </div>
128
+ )
129
+ }
130
+
131
+ function AlertSettings({ cfg, onChange, onSave }: {
132
+ cfg: AlertCfg
133
+ onChange: (c: AlertCfg) => void
134
+ onSave: () => void
135
+ }) {
136
+ return (
137
+ <div className="glass-panel rounded-apple-lg p-lg mb-lg space-y-md">
138
+ <h3 className="sf-headline">Failure Alert Settings</h3>
139
+ <label className="flex items-center gap-sm cursor-pointer">
140
+ <input type="checkbox" checked={cfg.enabled}
141
+ onChange={e => onChange({ ...cfg, enabled: e.target.checked })}
142
+ className="accent-apple-blue" />
143
+ <span className="text-sm">Enable failure alerts</span>
144
+ </label>
145
+ <input type="email" placeholder="Email address"
146
+ value={cfg.email ?? ''} className="input-field w-full"
147
+ onChange={e => onChange({ ...cfg, email: e.target.value })} />
148
+ <input type="text" placeholder="WhatsApp number (international)"
149
+ value={cfg.whatsapp ?? ''} className="input-field w-full"
150
+ onChange={e => onChange({ ...cfg, whatsapp: e.target.value })} />
151
+ <input type="url" placeholder="Slack webhook URL"
152
+ value={cfg.slack_webhook ?? ''} className="input-field w-full"
153
+ onChange={e => onChange({ ...cfg, slack_webhook: e.target.value })} />
154
+ <button onClick={onSave}
155
+ className="px-lg py-sm rounded-apple-lg bg-apple-blue text-white
156
+ text-sm font-medium hover:bg-apple-blue/80 cursor-pointer">
157
+ Save Alert Settings
158
+ </button>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ function StatusIcon({ status }: { status: string }) {
164
+ const m: Record<string, { c: 'green' | 'red' | 'blue' | 'gray'; I: typeof Clock }> = {
165
+ running: { c: 'blue', I: Loader },
166
+ success: { c: 'green', I: CheckCircle },
167
+ failed: { c: 'red', I: XCircle },
168
+ canceled: { c: 'gray', I: Clock },
169
+ }
170
+ const { c, I } = m[status] ?? m.canceled
171
+ return (
172
+ <Badge size="sm" color={c}>
173
+ <I className="w-3 h-3 mr-1 inline" />{status}
174
+ </Badge>
175
+ )
176
+ }
177
+
178
+ function fmtMs(ms: number) {
179
+ if (!ms) return '—'
180
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`
181
+ }
182
+
183
+ export default TaskHistory
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ describe('Team Page', () => {
4
+ it('should validate role options', () => {
5
+ const roles = ['admin', 'analyst', 'auditor', 'viewer']
6
+ expect(roles).toHaveLength(4)
7
+ expect(roles).toContain('admin')
8
+ expect(roles).toContain('viewer')
9
+ })
10
+
11
+ it('should require email for invite', () => {
12
+ const email = ''
13
+ expect(email.length).toBe(0)
14
+ })
15
+ })
@@ -0,0 +1,140 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { PageHeader } from '../components/layout/PageHeader'
4
+ import { Card } from '../components/ui/Card'
5
+ import { Button } from '../components/ui/Button'
6
+ import { Badge } from '../components/ui/Badge'
7
+ import { LoadingSpinner } from '../components/ui/LoadingSpinner'
8
+ import { EmptyState } from '../components/ui/EmptyState'
9
+ import { api } from '../api/client'
10
+
11
+ interface Member {
12
+ ID: string
13
+ Email: string
14
+ Role: string
15
+ ActivatedAt: string
16
+ }
17
+
18
+ export function Team() {
19
+ const { t } = useTranslation('team')
20
+ const [members, setMembers] = useState<Member[]>([])
21
+ const [loading, setLoading] = useState(true)
22
+ const [error, setError] = useState('')
23
+ const [email, setEmail] = useState('')
24
+ const [role, setRole] = useState('analyst')
25
+ const [inviting, setInviting] = useState(false)
26
+
27
+ const load = useCallback(async () => {
28
+ setLoading(true)
29
+ try {
30
+ const d = await api.get<{ members: Member[] }>('/team')
31
+ setMembers(d?.members ?? [])
32
+ } catch (err) {
33
+ setError(err instanceof Error ? err.message : 'Failed to load team')
34
+ } finally {
35
+ setLoading(false)
36
+ }
37
+ }, [])
38
+
39
+ useEffect(() => { load() }, [load])
40
+
41
+ const invite = async () => {
42
+ if (!email.trim()) return
43
+ setInviting(true)
44
+ setError('')
45
+ try {
46
+ await api.post('/team/invite', { email, role })
47
+ setEmail('')
48
+ load()
49
+ } catch (err) {
50
+ setError(err instanceof Error ? err.message : 'Invite failed')
51
+ } finally {
52
+ setInviting(false)
53
+ }
54
+ }
55
+
56
+ const remove = async (id: string) => {
57
+ if (!confirm('Remove this team member?')) return
58
+ try {
59
+ await api.del(`/team/${id}`)
60
+ load()
61
+ } catch (err) {
62
+ setError(err instanceof Error ? err.message : 'Remove failed')
63
+ }
64
+ }
65
+
66
+ const changeRole = async (id: string, newRole: string) => {
67
+ try {
68
+ await api.put(`/team/${id}/role`, { role: newRole })
69
+ load()
70
+ } catch (err) {
71
+ setError(err instanceof Error ? err.message : 'Role change failed')
72
+ }
73
+ }
74
+
75
+ return (
76
+ <div>
77
+ <PageHeader title={t('title')} description="Manage your team members and roles" />
78
+ <Card className="mb-lg">
79
+ <div className="flex flex-col md:flex-row gap-sm">
80
+ <input placeholder={t('email_placeholder')} value={email}
81
+ aria-label={t('email_placeholder')}
82
+ onChange={e => setEmail(e.target.value)} className="input-field flex-1" />
83
+ <select value={role} onChange={e => setRole(e.target.value)}
84
+ aria-label={t('roles.label')} className="input-field">
85
+ {['admin', 'analyst', 'auditor', 'viewer'].map(r => (
86
+ <option key={r} value={r}>{t(`roles.${r}`)}</option>
87
+ ))}
88
+ </select>
89
+ <Button onClick={invite} disabled={inviting || !email.trim()}>
90
+ {inviting ? 'Inviting...' : t('invite')}
91
+ </Button>
92
+ </div>
93
+ </Card>
94
+
95
+ {error && <Card className="mb-lg"><p role="alert" className="text-apple-red sf-body">{error}</p></Card>}
96
+ {loading && <LoadingSpinner />}
97
+
98
+ {!loading && members.length === 0 && (
99
+ <EmptyState title={t('no_members')} />
100
+ )}
101
+
102
+ {!loading && members.length > 0 && (
103
+ <Card>
104
+ {members.map(m => (
105
+ <TeamRow key={m.ID} member={m} onRemove={remove} onChangeRole={changeRole} />
106
+ ))}
107
+ </Card>
108
+ )}
109
+ </div>
110
+ )
111
+ }
112
+
113
+ function TeamRow({ member, onRemove, onChangeRole }: {
114
+ member: Member; onRemove: (id: string) => void; onChangeRole: (id: string, role: string) => void
115
+ }) {
116
+ const roleColor = { admin: 'red', analyst: 'blue', auditor: 'orange', viewer: 'green' } as const
117
+ return (
118
+ <div className="px-lg py-md flex flex-col sm:flex-row sm:items-center justify-between border-b border-white/[0.04] last:border-0 gap-sm">
119
+ <div className="min-w-0">
120
+ <p className="sf-body truncate">{member.Email}</p>
121
+ <p className="sf-caption text-white/40">{new Date(member.ActivatedAt).toLocaleDateString()}</p>
122
+ </div>
123
+ <div className="flex items-center gap-md shrink-0">
124
+ <select value={member.Role} onChange={e => onChangeRole(member.ID, e.target.value)}
125
+ className="bg-transparent text-sm text-white/80 cursor-pointer">
126
+ {['admin', 'analyst', 'auditor', 'viewer'].map(r => (
127
+ <option key={r} value={r}>{r}</option>
128
+ ))}
129
+ </select>
130
+ <Badge color={roleColor[member.Role as keyof typeof roleColor] ?? 'blue'} size="sm">
131
+ {member.Role}
132
+ </Badge>
133
+ <button onClick={() => onRemove(member.ID)}
134
+ className="text-apple-red/60 hover:text-apple-red text-sm cursor-pointer">Remove</button>
135
+ </div>
136
+ </div>
137
+ )
138
+ }
139
+
140
+ export default Team
@@ -0,0 +1,18 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
3
+ import { TransactionMonitoring } from './TransactionMonitoring'
4
+
5
+ beforeEach(() => {
6
+ global.fetch = vi.fn(() =>
7
+ Promise.resolve({
8
+ json: () => Promise.resolve({ data: { alerts: [], summary: {} } }),
9
+ })
10
+ ) as any
11
+ })
12
+
13
+ describe('TransactionMonitoring', () => {
14
+ it('renders title', () => {
15
+ render(<TransactionMonitoring />)
16
+ expect(screen.getByText('Transaction Monitoring')).toBeInTheDocument()
17
+ })
18
+ })
@@ -0,0 +1,118 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { AlertTriangle, ArrowUpRight, Shield, Clock } from 'lucide-react'
4
+ import { PageHeader } from '../components/layout/PageHeader'
5
+ import { LoadingSpinner } from '../components/ui/LoadingSpinner'
6
+ import { Badge } from '../components/ui/Badge'
7
+ import { api } from '../api/client'
8
+ import { WebhookCTA } from '../components/transactions/WebhookCTA'
9
+
10
+ interface TxnAlert {
11
+ id: string
12
+ transaction_id: string
13
+ alert_type: string
14
+ severity: number
15
+ description: string
16
+ created_at: string
17
+ }
18
+
19
+ const TYPE_ICONS: Record<string, typeof AlertTriangle> = {
20
+ high_value: ArrowUpRight, rapid_movement: Clock,
21
+ structuring: Shield, high_risk_country: AlertTriangle,
22
+ unusual_pattern: AlertTriangle,
23
+ }
24
+
25
+ export function TransactionMonitoring() {
26
+ const { t } = useTranslation('compliance')
27
+ const [alerts, setAlerts] = useState<TxnAlert[]>([])
28
+ const [summary, setSummary] = useState<Record<string, number>>({})
29
+ const [loading, setLoading] = useState(true)
30
+
31
+ useEffect(() => {
32
+ Promise.all([
33
+ api.get<{ alerts: TxnAlert[] }>('/transactions/alerts')
34
+ .then(d => setAlerts(d?.alerts ?? [])).catch(() => setAlerts([])),
35
+ api.get<Record<string, number>>('/transactions/alerts/summary')
36
+ .then(d => setSummary(d ?? {})).catch(() => setSummary({})),
37
+ ]).finally(() => setLoading(false))
38
+ }, [])
39
+
40
+ const typeLabels: Record<string, string> = {
41
+ high_value: t('transactions.high_value'),
42
+ rapid_movement: t('transactions.rapid_movement'),
43
+ structuring: t('transactions.structuring'),
44
+ high_risk_country: t('transactions.high_risk_country'),
45
+ unusual_pattern: t('transactions.unusual_pattern'),
46
+ }
47
+
48
+ if (loading) return <div className="flex items-center justify-center h-96"><LoadingSpinner /></div>
49
+
50
+ return (
51
+ <div>
52
+ <PageHeader title={t('transactions.title')} description="Real-time transaction alert monitoring" />
53
+ <SummaryCards summary={summary} typeLabels={typeLabels} />
54
+ <h2 className="sf-headline mb-md">{t('transactions.recent_alerts')}</h2>
55
+ <div className="space-y-sm">
56
+ {alerts.map(a => <AlertCard key={a.id} alert={a} typeLabels={typeLabels} />)}
57
+ {alerts.length === 0 && <WebhookCTA />}
58
+ </div>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ function SummaryCards({ summary, typeLabels }: {
64
+ summary: Record<string, number>; typeLabels: Record<string, string>;
65
+ }) {
66
+ const entries = Object.entries(summary)
67
+ if (entries.length === 0) return null
68
+ return (
69
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-sm mb-xl">
70
+ {entries.map(([type_, count]) => {
71
+ const Icon = TYPE_ICONS[type_] ?? AlertTriangle
72
+ return (
73
+ <div key={type_} className="card-vibrancy p-md">
74
+ <div className="flex items-center gap-xs mb-xs">
75
+ <Icon className="w-4 h-4 text-apple-label-secondary flex-shrink-0" />
76
+ <p className="sf-caption text-apple-label-secondary truncate">{typeLabels[type_] || type_}</p>
77
+ </div>
78
+ <p className="text-2xl font-bold">{count}</p>
79
+ </div>
80
+ )
81
+ })}
82
+ </div>
83
+ )
84
+ }
85
+
86
+ function AlertCard({ alert, typeLabels }: { alert: TxnAlert; typeLabels: Record<string, string> }) {
87
+ const Icon = TYPE_ICONS[alert.alert_type] ?? AlertTriangle
88
+ const time = new Date(alert.created_at).toLocaleString()
89
+ return (
90
+ <div className="card-vibrancy p-md">
91
+ <div className="flex items-start gap-md">
92
+ <div className="flex-shrink-0 mt-0.5">
93
+ <Icon className="w-5 h-5 text-apple-orange" />
94
+ </div>
95
+ <div className="flex-1 min-w-0">
96
+ <p className="sf-body font-medium text-white mb-xs">{alert.description}</p>
97
+ <div className="flex flex-wrap items-center gap-sm">
98
+ <Badge size="sm" color="orange">{typeLabels[alert.alert_type] ?? alert.alert_type}</Badge>
99
+ <span className="sf-caption text-apple-label-tertiary">TXN {alert.transaction_id}</span>
100
+ <span className="sf-caption text-apple-label-tertiary hidden sm:inline">{time}</span>
101
+ </div>
102
+ </div>
103
+ <SeverityBadge severity={alert.severity} />
104
+ </div>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ function SeverityBadge({ severity }: { severity: number }) {
110
+ const color = severity >= 8 ? 'bg-apple-red/20 text-apple-red'
111
+ : severity >= 5 ? 'bg-apple-orange/20 text-apple-orange'
112
+ : 'bg-apple-yellow/20 text-apple-yellow'
113
+ return (
114
+ <span className={`flex-shrink-0 px-sm py-xs text-xs font-semibold rounded-full ${color}`}>
115
+ {severity}/10
116
+ </span>
117
+ )
118
+ }