adserver-dashboard 1.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 (525) hide show
  1. package/.ci/staging.yml +191 -0
  2. package/.dockerignore +117 -0
  3. package/.env +40 -0
  4. package/.env.staging +38 -0
  5. package/.gitlab-ci.yml +16 -0
  6. package/DEMO_STATUS.md +579 -0
  7. package/Dockerfile +61 -0
  8. package/Influence-MW-AdServer-12-02-2026/client/index.html +17 -0
  9. package/Influence-MW-AdServer-12-02-2026/client/public/favicon.png +0 -0
  10. package/Influence-MW-AdServer-12-02-2026/client/src/App.tsx +91 -0
  11. package/Influence-MW-AdServer-12-02-2026/client/src/components/advanced-map-drawer.tsx +1131 -0
  12. package/Influence-MW-AdServer-12-02-2026/client/src/components/ai-recommendation-panel.tsx +379 -0
  13. package/Influence-MW-AdServer-12-02-2026/client/src/components/app-sidebar.tsx +183 -0
  14. package/Influence-MW-AdServer-12-02-2026/client/src/components/auto-optimize-button.tsx +184 -0
  15. package/Influence-MW-AdServer-12-02-2026/client/src/components/availability-drawer.tsx +385 -0
  16. package/Influence-MW-AdServer-12-02-2026/client/src/components/brand-insights-panel.tsx +87 -0
  17. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-agency-drawer.tsx +198 -0
  18. package/Influence-MW-AdServer-12-02-2026/client/src/components/create-brand-drawer.tsx +275 -0
  19. package/Influence-MW-AdServer-12-02-2026/client/src/components/creative-assignment.tsx +526 -0
  20. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table-toolbar.tsx +148 -0
  21. package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table.tsx +158 -0
  22. package/Influence-MW-AdServer-12-02-2026/client/src/components/filter-drawer.tsx +356 -0
  23. package/Influence-MW-AdServer-12-02-2026/client/src/components/form-insights-panel.tsx +82 -0
  24. package/Influence-MW-AdServer-12-02-2026/client/src/components/geography-selector.tsx +699 -0
  25. package/Influence-MW-AdServer-12-02-2026/client/src/components/header-user-menu.tsx +178 -0
  26. package/Influence-MW-AdServer-12-02-2026/client/src/components/history-drawer.tsx +313 -0
  27. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-availability-section.tsx +176 -0
  28. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-format-drawer.tsx +173 -0
  29. package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-selector.tsx +401 -0
  30. package/Influence-MW-AdServer-12-02-2026/client/src/components/manual-inventory-drawer.tsx +368 -0
  31. package/Influence-MW-AdServer-12-02-2026/client/src/components/mapbox-map.tsx +368 -0
  32. package/Influence-MW-AdServer-12-02-2026/client/src/components/market-insights-panel.tsx +202 -0
  33. package/Influence-MW-AdServer-12-02-2026/client/src/components/media-owner-drawer.tsx +217 -0
  34. package/Influence-MW-AdServer-12-02-2026/client/src/components/metric-card.tsx +58 -0
  35. package/Influence-MW-AdServer-12-02-2026/client/src/components/page-header.tsx +27 -0
  36. package/Influence-MW-AdServer-12-02-2026/client/src/components/player-status-indicator.tsx +137 -0
  37. package/Influence-MW-AdServer-12-02-2026/client/src/components/poi-targeting-drawer.tsx +298 -0
  38. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommendation-score-badge.tsx +102 -0
  39. package/Influence-MW-AdServer-12-02-2026/client/src/components/recommended-inventories-panel.tsx +248 -0
  40. package/Influence-MW-AdServer-12-02-2026/client/src/components/searchable-combobox.tsx +134 -0
  41. package/Influence-MW-AdServer-12-02-2026/client/src/components/signal-visualizations.tsx +407 -0
  42. package/Influence-MW-AdServer-12-02-2026/client/src/components/status-badge.tsx +35 -0
  43. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-provider.tsx +73 -0
  44. package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-toggle.tsx +37 -0
  45. package/Influence-MW-AdServer-12-02-2026/client/src/components/traffic-slider.tsx +75 -0
  46. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/accordion.tsx +56 -0
  47. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert-dialog.tsx +139 -0
  48. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert.tsx +59 -0
  49. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/aspect-ratio.tsx +5 -0
  50. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/avatar.tsx +51 -0
  51. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/badge.tsx +38 -0
  52. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/breadcrumb.tsx +115 -0
  53. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/button.tsx +62 -0
  54. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/calendar.tsx +68 -0
  55. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/card.tsx +85 -0
  56. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/carousel.tsx +260 -0
  57. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/chart.tsx +365 -0
  58. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/checkbox.tsx +28 -0
  59. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/collapsible.tsx +11 -0
  60. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/command.tsx +151 -0
  61. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/context-menu.tsx +198 -0
  62. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dialog.tsx +122 -0
  63. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/drawer.tsx +118 -0
  64. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dropdown-menu.tsx +198 -0
  65. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/form.tsx +178 -0
  66. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/hover-card.tsx +29 -0
  67. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input-otp.tsx +69 -0
  68. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input.tsx +23 -0
  69. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/label.tsx +24 -0
  70. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/menubar.tsx +256 -0
  71. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/navigation-menu.tsx +128 -0
  72. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/pagination.tsx +117 -0
  73. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/popover.tsx +29 -0
  74. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/progress.tsx +28 -0
  75. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/radio-group.tsx +42 -0
  76. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/resizable.tsx +45 -0
  77. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/scroll-area.tsx +46 -0
  78. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/select.tsx +160 -0
  79. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/separator.tsx +29 -0
  80. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sheet.tsx +140 -0
  81. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sidebar.tsx +727 -0
  82. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/skeleton.tsx +15 -0
  83. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/slider.tsx +26 -0
  84. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/switch.tsx +27 -0
  85. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/table.tsx +117 -0
  86. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tabs.tsx +53 -0
  87. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/textarea.tsx +22 -0
  88. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toast.tsx +127 -0
  89. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toaster.tsx +33 -0
  90. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle-group.tsx +61 -0
  91. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle.tsx +43 -0
  92. package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tooltip.tsx +30 -0
  93. package/Influence-MW-AdServer-12-02-2026/client/src/components/vendor-stores-modal.tsx +336 -0
  94. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-drawer.tsx +359 -0
  95. package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-selector.tsx +436 -0
  96. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-mobile.tsx +19 -0
  97. package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-toast.ts +191 -0
  98. package/Influence-MW-AdServer-12-02-2026/client/src/index.css +244 -0
  99. package/Influence-MW-AdServer-12-02-2026/client/src/lib/queryClient.ts +57 -0
  100. package/Influence-MW-AdServer-12-02-2026/client/src/lib/utils.ts +39 -0
  101. package/Influence-MW-AdServer-12-02-2026/client/src/lib/venue-taxonomy.ts +532 -0
  102. package/Influence-MW-AdServer-12-02-2026/client/src/main.tsx +5 -0
  103. package/Influence-MW-AdServer-12-02-2026/client/src/pages/assign-creative.tsx +781 -0
  104. package/Influence-MW-AdServer-12-02-2026/client/src/pages/content-hub.tsx +995 -0
  105. package/Influence-MW-AdServer-12-02-2026/client/src/pages/custom-pois.tsx +431 -0
  106. package/Influence-MW-AdServer-12-02-2026/client/src/pages/dashboard.tsx +620 -0
  107. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-detail.tsx +1062 -0
  108. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-form.tsx +1570 -0
  109. package/Influence-MW-AdServer-12-02-2026/client/src/pages/deals.tsx +716 -0
  110. package/Influence-MW-AdServer-12-02-2026/client/src/pages/edit-creative-assignment.tsx +1051 -0
  111. package/Influence-MW-AdServer-12-02-2026/client/src/pages/geotargeting.tsx +675 -0
  112. package/Influence-MW-AdServer-12-02-2026/client/src/pages/integrations.tsx +425 -0
  113. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-creatives.tsx +622 -0
  114. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-form.tsx +3132 -0
  115. package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-items.tsx +530 -0
  116. package/Influence-MW-AdServer-12-02-2026/client/src/pages/not-found.tsx +21 -0
  117. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play-upload.tsx +479 -0
  118. package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play.tsx +880 -0
  119. package/Influence-MW-AdServer-12-02-2026/client/src/pages/reports.tsx +235 -0
  120. package/Influence-MW-AdServer-12-02-2026/client/src/pages/settings.tsx +652 -0
  121. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signal-form.tsx +1117 -0
  122. package/Influence-MW-AdServer-12-02-2026/client/src/pages/signals.tsx +366 -0
  123. package/Influence-MW-AdServer-12-02-2026/client/src/pages/tags.tsx +332 -0
  124. package/Influence-MW-AdServer-12-02-2026/client/src/pages/venues.tsx +381 -0
  125. package/Influence-MW-AdServer-12-02-2026/client/src/types/mapbox-gl-draw.d.ts +37 -0
  126. package/Influence-MW-AdServer-12-02-2026/client/src/types/react-simple-maps.d.ts +57 -0
  127. package/Influence-MW-AdServer-12-02-2026/components.json +20 -0
  128. package/Influence-MW-AdServer-12-02-2026/docs/PRD.md +3373 -0
  129. package/Influence-MW-AdServer-12-02-2026/docs/influence-feature-mapping.csv +498 -0
  130. package/Influence-MW-AdServer-12-02-2026/drizzle.config.ts +14 -0
  131. package/Influence-MW-AdServer-12-02-2026/package-lock.json +9672 -0
  132. package/Influence-MW-AdServer-12-02-2026/package.json +118 -0
  133. package/Influence-MW-AdServer-12-02-2026/postcss.config.js +6 -0
  134. package/Influence-MW-AdServer-12-02-2026/replit.md +91 -0
  135. package/Influence-MW-AdServer-12-02-2026/script/build.ts +67 -0
  136. package/Influence-MW-AdServer-12-02-2026/scripts/create-miro-diagrams.cjs +318 -0
  137. package/Influence-MW-AdServer-12-02-2026/scripts/create-remaining-diagrams.cjs +270 -0
  138. package/Influence-MW-AdServer-12-02-2026/server/index.ts +103 -0
  139. package/Influence-MW-AdServer-12-02-2026/server/recommendation-service.ts +319 -0
  140. package/Influence-MW-AdServer-12-02-2026/server/routes.ts +1890 -0
  141. package/Influence-MW-AdServer-12-02-2026/server/static.ts +19 -0
  142. package/Influence-MW-AdServer-12-02-2026/server/storage.ts +2058 -0
  143. package/Influence-MW-AdServer-12-02-2026/server/vite.ts +58 -0
  144. package/Influence-MW-AdServer-12-02-2026/shared/schema.ts +1595 -0
  145. package/Influence-MW-AdServer-12-02-2026/tailwind.config.ts +107 -0
  146. package/Influence-MW-AdServer-12-02-2026/tsconfig.json +23 -0
  147. package/Influence-MW-AdServer-12-02-2026/vite.config.ts +40 -0
  148. package/LINE_ITEM_BUDGET_FIELD_MAPPING.md +178 -0
  149. package/PCM/.env.example +92 -0
  150. package/PCM/README.md +558 -0
  151. package/PCM/docs/TEST_CASES.md +422 -0
  152. package/PCM/index.js +106 -0
  153. package/PCM/package-lock.json +3282 -0
  154. package/PCM/package.json +32 -0
  155. package/PCM/replit.md +64 -0
  156. package/PCM/schema.sql +495 -0
  157. package/PCM/scripts/export-schema.js +183 -0
  158. package/PCM/scripts/seed-comprehensive.js +631 -0
  159. package/PCM/scripts/seed-production.js +477 -0
  160. package/PCM/src/config/db.js +56 -0
  161. package/PCM/src/config/swagger.js +5975 -0
  162. package/PCM/src/dto/EmailRequestDTO.js +166 -0
  163. package/PCM/src/middleware/errorHandler.js +52 -0
  164. package/PCM/src/middleware/logger.js +26 -0
  165. package/PCM/src/migrations/001_add_campaign_mode_fields.sql +36 -0
  166. package/PCM/src/migrations/002_create_deal_id_counters.sql +22 -0
  167. package/PCM/src/migrations/003_update_publishers_column.sql +15 -0
  168. package/PCM/src/migrations/004_add_direct_dealtype_and_advertiser.sql +5 -0
  169. package/PCM/src/migrations/005_add_programmatic_fields_and_update_enums.sql +31 -0
  170. package/PCM/src/migrations/006_add_line_item_programmatic_fields.sql +12 -0
  171. package/PCM/src/migrations/007_add_line_item_direct_fields.sql +15 -0
  172. package/PCM/src/migrations/008_add_inventory_fields.sql +45 -0
  173. package/PCM/src/migrations/009_move_inventory_fields_to_metadata.sql +32 -0
  174. package/PCM/src/migrations/010_add_draft_status_and_line_items_count.sql +23 -0
  175. package/PCM/src/migrations/011_add_planning_field.sql +21 -0
  176. package/PCM/src/migrations/012_fix_inventory_composite_pk.sql +17 -0
  177. package/PCM/src/migrations/013_make_external_id_optional.sql +3 -0
  178. package/PCM/src/migrations/014_create_change_history.sql +38 -0
  179. package/PCM/src/migrations/016_create_publisher_insertion_orders.sql +33 -0
  180. package/PCM/src/migrations/017_fix_line_item_id_fk_reference.sql +86 -0
  181. package/PCM/src/migrations/018_create_approval_tables.sql +44 -0
  182. package/PCM/src/migrations/019_add_encrypted_token_column.sql +2 -0
  183. package/PCM/src/migrations/020_add_rejection_reason_to_deals.sql +10 -0
  184. package/PCM/src/migrations/021_add_publisher_external_id_to_inventories.sql +12 -0
  185. package/PCM/src/migrations/022_add_line_item_extended_fields.sql +24 -0
  186. package/PCM/src/migrations/023_add_base_price_fields.sql +8 -0
  187. package/PCM/src/migrations/run-migrations.js +46 -0
  188. package/PCM/src/models/ApprovalOTP.js +51 -0
  189. package/PCM/src/models/ApprovalToken.js +79 -0
  190. package/PCM/src/models/ChangeHistory.js +107 -0
  191. package/PCM/src/models/Deal.js +186 -0
  192. package/PCM/src/models/DealIdCounter.js +28 -0
  193. package/PCM/src/models/LineItem.js +227 -0
  194. package/PCM/src/models/LineItemCreative.js +89 -0
  195. package/PCM/src/models/LineItemInventory.js +115 -0
  196. package/PCM/src/models/PublisherInsertionOrder.js +93 -0
  197. package/PCM/src/models/TransactionHistory.js +34 -0
  198. package/PCM/src/models/associations.js +81 -0
  199. package/PCM/src/routes/approval.js +321 -0
  200. package/PCM/src/routes/creatives.js +437 -0
  201. package/PCM/src/routes/deals.js +1638 -0
  202. package/PCM/src/routes/digitalSignage.js +242 -0
  203. package/PCM/src/routes/insertionOrders.js +380 -0
  204. package/PCM/src/routes/lineItems.js +926 -0
  205. package/PCM/src/routes/system.js +384 -0
  206. package/PCM/src/services/ApprovalService.js +885 -0
  207. package/PCM/src/services/CampaignImportConverter.js +631 -0
  208. package/PCM/src/services/CampaignModeService.js +273 -0
  209. package/PCM/src/services/CampaignStatusService.js +395 -0
  210. package/PCM/src/services/ChangeHistoryService.js +316 -0
  211. package/PCM/src/services/DealIdService.js +94 -0
  212. package/PCM/src/services/DealResponseFormatter.js +90 -0
  213. package/PCM/src/services/EmailNotificationService.js +315 -0
  214. package/PCM/src/services/LineItemResponseFormatter.js +122 -0
  215. package/PCM/src/services/LineItemStatusService.js +380 -0
  216. package/PCM/src/tests/comprehensiveTestRunner.js +360 -0
  217. package/PCM/src/tests/comprehensiveTests.js +1277 -0
  218. package/PCM/src/tests/dealTypeUnitTests.js +1058 -0
  219. package/PCM/src/tests/testRunner.js +248 -0
  220. package/PCM/src/utils/caseConverter.js +92 -0
  221. package/PCM/src/utils/dealCalculations.js +206 -0
  222. package/PCM/src/utils/lineItemPayloadNormalizer.js +41 -0
  223. package/PCM/src/utils/payloadNormalizer.js +34 -0
  224. package/PCM/src/utils/sourceNormalizer.js +56 -0
  225. package/PCM/src/validators/creativeValidator.js +27 -0
  226. package/PCM/src/validators/dealValidator.js +203 -0
  227. package/PCM/src/validators/lineItemValidator.js +489 -0
  228. package/PCM/tests/approval-flows.test.js +238 -0
  229. package/PCM/tests/approval-workflow.test.js +291 -0
  230. package/PCM/tests/campaign-import-converter.test.js +543 -0
  231. package/PCM/tests/campaign-import-e2e.test.js +520 -0
  232. package/PCM/tests/campaign-status.test.js +539 -0
  233. package/PCM/tests/direct-publisher-split-reimport.test.js +460 -0
  234. package/PCM/tests/e2e/digital-signage.test.js +145 -0
  235. package/PCM/tests/e2e/search-filter-pagination.test.js +399 -0
  236. package/PCM/tests/e2e-comprehensive.test.js +3446 -0
  237. package/PCM/tests/edge-cases.test.js +340 -0
  238. package/PCM/tests/line-item-status.test.js +340 -0
  239. package/PCM/tests/seller-account-external-ids.test.js +877 -0
  240. package/PCM/tests/source-validation.test.js +324 -0
  241. package/PRD.md +3373 -0
  242. package/README.md +186 -0
  243. package/client/index.html +35 -0
  244. package/client/public/DEMO_STATUS.md +579 -0
  245. package/client/public/img/MW-logo-trans_1754045676555.png +0 -0
  246. package/client/public/locales/ar/approval.json +144 -0
  247. package/client/public/locales/ar/buyer.json +61 -0
  248. package/client/public/locales/ar/campaigns.json +1 -0
  249. package/client/public/locales/ar/common.json +218 -0
  250. package/client/public/locales/ar/contentHub.json +266 -0
  251. package/client/public/locales/ar/creatives.json +79 -0
  252. package/client/public/locales/ar/dashboard.json +57 -0
  253. package/client/public/locales/ar/deals.json +886 -0
  254. package/client/public/locales/ar/dsp.json +131 -0
  255. package/client/public/locales/ar/inventory.json +201 -0
  256. package/client/public/locales/ar/lineItems.json +553 -0
  257. package/client/public/locales/ar/navigation.json +48 -0
  258. package/client/public/locales/ar/wizard.json +1 -0
  259. package/client/public/locales/en/approval.json +144 -0
  260. package/client/public/locales/en/buyer.json +65 -0
  261. package/client/public/locales/en/campaigns.json +1 -0
  262. package/client/public/locales/en/common.json +218 -0
  263. package/client/public/locales/en/contentHub.json +266 -0
  264. package/client/public/locales/en/creatives.json +79 -0
  265. package/client/public/locales/en/dashboard.json +57 -0
  266. package/client/public/locales/en/deals.json +886 -0
  267. package/client/public/locales/en/dsp.json +131 -0
  268. package/client/public/locales/en/inventory.json +201 -0
  269. package/client/public/locales/en/lineItems.json +659 -0
  270. package/client/public/locales/en/navigation.json +48 -0
  271. package/client/public/locales/en/wizard.json +1 -0
  272. package/client/public/locales/ja/approval.json +144 -0
  273. package/client/public/locales/ja/buyer.json +61 -0
  274. package/client/public/locales/ja/campaigns.json +1 -0
  275. package/client/public/locales/ja/common.json +218 -0
  276. package/client/public/locales/ja/contentHub.json +266 -0
  277. package/client/public/locales/ja/creatives.json +79 -0
  278. package/client/public/locales/ja/dashboard.json +57 -0
  279. package/client/public/locales/ja/deals.json +886 -0
  280. package/client/public/locales/ja/dsp.json +131 -0
  281. package/client/public/locales/ja/inventory.json +201 -0
  282. package/client/public/locales/ja/lineItems.json +553 -0
  283. package/client/public/locales/ja/navigation.json +48 -0
  284. package/client/public/locales/ja/wizard.json +1 -0
  285. package/client/public/locales/zh/approval.json +144 -0
  286. package/client/public/locales/zh/buyer.json +61 -0
  287. package/client/public/locales/zh/campaigns.json +1 -0
  288. package/client/public/locales/zh/common.json +218 -0
  289. package/client/public/locales/zh/contentHub.json +266 -0
  290. package/client/public/locales/zh/creatives.json +79 -0
  291. package/client/public/locales/zh/dashboard.json +57 -0
  292. package/client/public/locales/zh/deals.json +886 -0
  293. package/client/public/locales/zh/dsp.json +131 -0
  294. package/client/public/locales/zh/inventory.json +201 -0
  295. package/client/public/locales/zh/lineItems.json +553 -0
  296. package/client/public/locales/zh/navigation.json +48 -0
  297. package/client/public/locales/zh/wizard.json +1 -0
  298. package/client/public/manifest.json +36 -0
  299. package/client/src/App.tsx +464 -0
  300. package/client/src/components/app-sidebar.tsx +312 -0
  301. package/client/src/components/approval/approval-decision-form.test.tsx +294 -0
  302. package/client/src/components/approval/approval-decision-form.tsx +326 -0
  303. package/client/src/components/approval/approval-sheet.tsx +631 -0
  304. package/client/src/components/approval/line-item-details-sheet.tsx +371 -0
  305. package/client/src/components/approval/otp-verification.test.tsx +337 -0
  306. package/client/src/components/approval/otp-verification.tsx +180 -0
  307. package/client/src/components/content-hub/bulk-transcode-dialog.tsx +379 -0
  308. package/client/src/components/content-hub/content-hub-manager-v2.tsx +574 -0
  309. package/client/src/components/content-hub/content-hub-manager.tsx +330 -0
  310. package/client/src/components/content-hub/creative-card.tsx +456 -0
  311. package/client/src/components/content-hub/creative-detail-sheet.tsx +685 -0
  312. package/client/src/components/content-hub/creative-filters.tsx +457 -0
  313. package/client/src/components/content-hub/creative-grid.tsx +329 -0
  314. package/client/src/components/content-hub/creative-selector.tsx +415 -0
  315. package/client/src/components/content-hub/creative-upload.tsx +547 -0
  316. package/client/src/components/content-hub/folder-dialogs.tsx +445 -0
  317. package/client/src/components/content-hub/folder-list.tsx +280 -0
  318. package/client/src/components/content-hub/review-dialogs.tsx +268 -0
  319. package/client/src/components/content-hub/transcode-dialog.tsx +226 -0
  320. package/client/src/components/creative-library/creative-details-view.tsx +446 -0
  321. package/client/src/components/creative-library/creative-filters-panel.tsx +203 -0
  322. package/client/src/components/creative-library/creative-list.tsx +360 -0
  323. package/client/src/components/creative-library/creative-status-badge.tsx +71 -0
  324. package/client/src/components/creative-library/folder-card.tsx +78 -0
  325. package/client/src/components/creative-library/index.ts +27 -0
  326. package/client/src/components/creative-library/new-creative-card.tsx +211 -0
  327. package/client/src/components/creative-library/upload-creative-dialog.tsx +261 -0
  328. package/client/src/components/dashboard-overview.tsx +109 -0
  329. package/client/src/components/deals/approval-history-panel.test.tsx +240 -0
  330. package/client/src/components/deals/approval-history-panel.tsx +156 -0
  331. package/client/src/components/deals/deal-status-badge.tsx +92 -0
  332. package/client/src/components/deals/import-from-planner-dialog.tsx +399 -0
  333. package/client/src/components/deals/market-insights-panel.tsx +237 -0
  334. package/client/src/components/deals/reopen-deal-sheet.tsx +191 -0
  335. package/client/src/components/deals/request-approval-sheet.test.tsx +323 -0
  336. package/client/src/components/deals/request-approval-sheet.tsx +136 -0
  337. package/client/src/components/deals/resend-approval-sheet.tsx +201 -0
  338. package/client/src/components/direct-campaigns/campaign-card.tsx +283 -0
  339. package/client/src/components/direct-campaigns/deal-filter-panel.tsx +325 -0
  340. package/client/src/components/inventory/advanced-filters-panel.tsx +273 -0
  341. package/client/src/components/inventory/csv-upload-modal.tsx +639 -0
  342. package/client/src/components/inventory/inventory-availability-view.tsx +486 -0
  343. package/client/src/components/inventory/inventory-details-sheet.tsx +376 -0
  344. package/client/src/components/inventory/inventory-map-view.tsx +596 -0
  345. package/client/src/components/inventory/inventory-settings-menu.tsx +52 -0
  346. package/client/src/components/language-switcher.tsx +53 -0
  347. package/client/src/components/line-items/campaign-forecast-panel.tsx +138 -0
  348. package/client/src/components/line-items/form-insights.tsx +89 -0
  349. package/client/src/components/line-items/geofencing/LocationCsvUploadDrawer.tsx +100 -0
  350. package/client/src/components/line-items/geofencing/POIDropdown.tsx +379 -0
  351. package/client/src/components/line-items/geofencing/SelectedLocationsSidebar.tsx +436 -0
  352. package/client/src/components/line-items/geofencing/ViewFileLocationDrawer.tsx +199 -0
  353. package/client/src/components/line-items/geofencing/components/ExistingFilesTab.tsx +268 -0
  354. package/client/src/components/line-items/geofencing/components/TemplateDownloadSection.tsx +59 -0
  355. package/client/src/components/line-items/geofencing/components/UploadTab.tsx +215 -0
  356. package/client/src/components/line-items/geofencing-map.tsx +1270 -0
  357. package/client/src/components/line-items/inventory-availability-section.tsx +178 -0
  358. package/client/src/components/line-items/line-item-schedule-manager.tsx +313 -0
  359. package/client/src/components/line-items/manual-inventory-drawer.tsx +346 -0
  360. package/client/src/components/line-items/planner-inventory-card.tsx +495 -0
  361. package/client/src/components/line-items/planner-schedule-grid.tsx +495 -0
  362. package/client/src/components/line-items/schedule-rule-editor.tsx +649 -0
  363. package/client/src/components/line-items/schedule-rule-types.ts +122 -0
  364. package/client/src/components/line-items/steps/creatives-step.tsx +681 -0
  365. package/client/src/components/line-items/steps/inventory-schedule-step.tsx +1596 -0
  366. package/client/src/components/line-items/steps/inventory-step.tsx +1533 -0
  367. package/client/src/components/line-items/steps/line-item-details-step.tsx +916 -0
  368. package/client/src/components/line-items/steps/schedule-step.tsx +273 -0
  369. package/client/src/components/line-items/steps/summary-step.tsx +680 -0
  370. package/client/src/components/line-items/steps/targeting-step.tsx +1708 -0
  371. package/client/src/components/product-switcher.tsx +105 -0
  372. package/client/src/components/protected-route.tsx +49 -0
  373. package/client/src/components/skip-link.tsx +22 -0
  374. package/client/src/components/stat-card.tsx +53 -0
  375. package/client/src/components/status-badge.tsx +96 -0
  376. package/client/src/components/ui/hierarchical-venue-selector.tsx +389 -0
  377. package/client/src/components/ui/toaster.tsx +111 -0
  378. package/client/src/contexts/auth-context.tsx +181 -0
  379. package/client/src/contexts/sidebar-state.tsx +50 -0
  380. package/client/src/contexts/theme-context.tsx +66 -0
  381. package/client/src/data/campaign-data.json +107 -0
  382. package/client/src/data/countries.json +22 -0
  383. package/client/src/hooks/use-approval.ts +366 -0
  384. package/client/src/hooks/use-keyboard-shortcuts.ts +74 -0
  385. package/client/src/hooks/use-media-query.ts +46 -0
  386. package/client/src/hooks/use-mobile.tsx +19 -0
  387. package/client/src/hooks/use-page-title.ts +21 -0
  388. package/client/src/hooks/use-toast.ts +195 -0
  389. package/client/src/index.css +694 -0
  390. package/client/src/lib/__tests__/accessibility.test.ts +104 -0
  391. package/client/src/lib/__tests__/date-utils.test.ts +199 -0
  392. package/client/src/lib/__tests__/dsp-buyer-api.test.ts +127 -0
  393. package/client/src/lib/__tests__/dsp-buyer-integration.test.ts +247 -0
  394. package/client/src/lib/__tests__/storage-utils.test.ts +167 -0
  395. package/client/src/lib/__tests__/utils.test.ts +57 -0
  396. package/client/src/lib/accessibility.ts +141 -0
  397. package/client/src/lib/api-config.ts +9 -0
  398. package/client/src/lib/auth-service.ts +209 -0
  399. package/client/src/lib/campaign-creative-api.ts +82 -0
  400. package/client/src/lib/company-api.ts +61 -0
  401. package/client/src/lib/content-hub-api.ts +407 -0
  402. package/client/src/lib/creative-mapper.ts +61 -0
  403. package/client/src/lib/date-utils.ts +119 -0
  404. package/client/src/lib/deal-helpers.ts +220 -0
  405. package/client/src/lib/dsp-buyer-api.ts +196 -0
  406. package/client/src/lib/geo-import-api.ts +151 -0
  407. package/client/src/lib/google-poi-api.ts +305 -0
  408. package/client/src/lib/i18n/__tests__/formatting.test.ts +202 -0
  409. package/client/src/lib/i18n/formatting.ts +130 -0
  410. package/client/src/lib/i18n/index.ts +8 -0
  411. package/client/src/lib/i18n-compat.ts +76 -0
  412. package/client/src/lib/influence-deals-api.ts +896 -0
  413. package/client/src/lib/inventory-api.ts +399 -0
  414. package/client/src/lib/oauth-service.ts +678 -0
  415. package/client/src/lib/poi-types.ts +75 -0
  416. package/client/src/lib/queryClient.ts +144 -0
  417. package/client/src/lib/recommendation-api.ts +380 -0
  418. package/client/src/lib/storage-utils.ts +104 -0
  419. package/client/src/lib/tolgee.ts +85 -0
  420. package/client/src/lib/utils.ts +0 -0
  421. package/client/src/main.tsx +67 -0
  422. package/client/src/mapbox-draw-modes.d.ts +32 -0
  423. package/client/src/pages/all-folders.tsx +203 -0
  424. package/client/src/pages/auth-callback.tsx +115 -0
  425. package/client/src/pages/buyer-form.tsx +339 -0
  426. package/client/src/pages/buyer-list.tsx +622 -0
  427. package/client/src/pages/content-hub.tsx +1358 -0
  428. package/client/src/pages/create-deal.tsx +2093 -0
  429. package/client/src/pages/creative-assignment-page.tsx +548 -0
  430. package/client/src/pages/creatives.tsx +5 -0
  431. package/client/src/pages/custom-pois.tsx +425 -0
  432. package/client/src/pages/dashboard.tsx +615 -0
  433. package/client/src/pages/deal-history.tsx +434 -0
  434. package/client/src/pages/deal-line-items.tsx +1703 -0
  435. package/client/src/pages/demo-status.tsx +113 -0
  436. package/client/src/pages/direct-campaign-details.tsx +361 -0
  437. package/client/src/pages/direct-campaigns-new.tsx +824 -0
  438. package/client/src/pages/dsp-form.tsx +803 -0
  439. package/client/src/pages/dsp-list.tsx +239 -0
  440. package/client/src/pages/folder-content.tsx +336 -0
  441. package/client/src/pages/integrations.tsx +429 -0
  442. package/client/src/pages/line-item-creatives.tsx +789 -0
  443. package/client/src/pages/line-item-detail-page.tsx +684 -0
  444. package/client/src/pages/line-item-form-page.tsx +3261 -0
  445. package/client/src/pages/line-item-wizard.tsx +1207 -0
  446. package/client/src/pages/login.tsx +154 -0
  447. package/client/src/pages/not-found.tsx +23 -0
  448. package/client/src/pages/proof-of-play.tsx +397 -0
  449. package/client/src/pages/public-approval.tsx +551 -0
  450. package/client/src/pages/reports.tsx +231 -0
  451. package/client/src/pages/settings.tsx +760 -0
  452. package/client/src/pages/signals.tsx +389 -0
  453. package/client/src/pages/tags.tsx +318 -0
  454. package/client/src/pages/test-results.tsx +328 -0
  455. package/client/src/store/hooks.ts +5 -0
  456. package/client/src/store/index.ts +15 -0
  457. package/client/src/store/mapMarkerLocationsSlice.ts +241 -0
  458. package/client/src/styles/design-tokens.css +324 -0
  459. package/client/src/test/setup.ts +261 -0
  460. package/client/src/test/test-utils.tsx +40 -0
  461. package/client/src/types/approval.ts +221 -0
  462. package/client/src/types/content-hub.ts +209 -0
  463. package/client/src/types/geofencing.ts +67 -0
  464. package/client/src/types/transcoding.ts +140 -0
  465. package/client/src/vite-env.d.ts +18 -0
  466. package/components.json +20 -0
  467. package/creative-api.json +1 -0
  468. package/docs/AI_REFERENCE.md +459 -0
  469. package/docs/MWDesign-Prompt.md +132 -0
  470. package/docs/MWDesign-System.md +344 -0
  471. package/docs/test-plan.md +277 -0
  472. package/e2e/AUTONOMOUS-TESTING.md +406 -0
  473. package/e2e/README.md +219 -0
  474. package/e2e/autonomous-flow.spec.ts +308 -0
  475. package/e2e/debug-sso.spec.ts +163 -0
  476. package/e2e/direct-campaigns.spec.ts +219 -0
  477. package/e2e/explore-sso.spec.ts +149 -0
  478. package/e2e/fixtures/auth.ts +26 -0
  479. package/e2e/fixtures/enhanced-test.ts +331 -0
  480. package/e2e/pagination.spec.ts +280 -0
  481. package/e2e/view-toggle.spec.ts +312 -0
  482. package/generated-icon.png +0 -0
  483. package/i18next-scanner.config.cjs +46 -0
  484. package/package.json +141 -0
  485. package/playwright.config.ts +93 -0
  486. package/postcss.config.js +6 -0
  487. package/replit.md +196 -0
  488. package/screenshot-after-login.png +0 -0
  489. package/screenshot-contenthub-grid.png +0 -0
  490. package/screenshot-contenthub-list-fixed.png +0 -0
  491. package/screenshot-contenthub-list.png +0 -0
  492. package/screenshot-create-deal.png +0 -0
  493. package/screenshot-dashboard.png +0 -0
  494. package/screenshot-deals.png +0 -0
  495. package/screenshot-login-filled.png +0 -0
  496. package/screenshot-login.png +0 -0
  497. package/screenshot.mjs +24 -0
  498. package/scripts/deploy-stg.sh +185 -0
  499. package/shared/direct-io-schema.ts +383 -0
  500. package/shared/schema.ts +439 -0
  501. package/shared/screen-types.ts +149 -0
  502. package/springdocDefault.json +1 -0
  503. package/swagger-ui-bundle.js +2 -0
  504. package/swagger-ui-init.js +10316 -0
  505. package/tailwind.config.ts +282 -0
  506. package/terraform/README.md +306 -0
  507. package/terraform/cloudfront.tf +289 -0
  508. package/terraform/ecs.tf +727 -0
  509. package/terraform/environments/dev.tfvars +59 -0
  510. package/terraform/environments/production.tfvars +60 -0
  511. package/terraform/main.tf +47 -0
  512. package/terraform/outputs.tf +145 -0
  513. package/terraform/s3.tf +192 -0
  514. package/terraform/variables.tf +226 -0
  515. package/terraform/waf.tf +165 -0
  516. package/terraform-frontend/.terraform.lock.hcl +25 -0
  517. package/terraform-frontend/README.md +85 -0
  518. package/terraform-frontend/cloudfront.tf +125 -0
  519. package/terraform-frontend/main.tf +31 -0
  520. package/terraform-frontend/outputs.tf +24 -0
  521. package/terraform-frontend/terraform.tfvars +12 -0
  522. package/terraform-frontend/variables.tf +53 -0
  523. package/tsconfig.json +23 -0
  524. package/vite.config.ts +226 -0
  525. package/vitest.config.ts +56 -0
@@ -0,0 +1,1117 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useQuery, useMutation } from "@tanstack/react-query";
3
+ import { useRoute, useLocation, Link } from "wouter";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import {
8
+ Users,
9
+ Footprints,
10
+ Search,
11
+ Cloud,
12
+ Plus,
13
+ Trash2,
14
+ Lightbulb,
15
+ Pencil,
16
+ Loader2,
17
+ } from "lucide-react";
18
+ import { Button } from "@/components/ui/button";
19
+ import { Input } from "@/components/ui/input";
20
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
21
+ import { Badge } from "@/components/ui/badge";
22
+ import { cn } from "@/lib/utils";
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormField,
27
+ FormItem,
28
+ FormLabel,
29
+ FormMessage,
30
+ } from "@/components/ui/form";
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from "@/components/ui/select";
38
+ import { useToast } from "@/hooks/use-toast";
39
+ import { apiRequest, queryClient } from "@/lib/queryClient";
40
+ import type { Signal, SignalRule, Brand } from "@shared/schema";
41
+ import { SIGNAL_TYPES } from "@shared/schema";
42
+ import { SignalVisualization } from "@/components/signal-visualizations";
43
+ import { VendorStoresModal } from "@/components/vendor-stores-modal";
44
+
45
+ const signalFormSchema = z.object({
46
+ name: z.string().min(1, "Signal name is required"),
47
+ signalType: z.string().min(1, "Please select a signal type"),
48
+ status: z.enum(["active", "inactive"]).default("active"),
49
+ });
50
+
51
+ type SignalFormData = z.infer<typeof signalFormSchema>;
52
+
53
+ interface RuleData {
54
+ id: string;
55
+ name: string;
56
+ conditionType: string;
57
+ parameters: Record<string, unknown>;
58
+ isNew?: boolean;
59
+ isSaved?: boolean;
60
+ }
61
+
62
+ const WEATHER_CONDITIONS = [
63
+ { value: "temperature_above", label: "Temperature Above" },
64
+ { value: "temperature_below", label: "Temperature Below" },
65
+ { value: "weather_condition", label: "Weather Condition" },
66
+ ];
67
+
68
+ const WEATHER_CONDITIONS_OPTIONS = [
69
+ { value: "sunny", label: "Sunny" },
70
+ { value: "cloudy", label: "Cloudy" },
71
+ { value: "rainy", label: "Rainy" },
72
+ { value: "stormy", label: "Stormy" },
73
+ { value: "snowy", label: "Snowy" },
74
+ ];
75
+
76
+ const AUDIENCE_SEGMENTS = [
77
+ { value: "high_intent", label: "High Intent Shoppers" },
78
+ { value: "frequent_visitors", label: "Frequent Visitors" },
79
+ { value: "new_customers", label: "New Customers" },
80
+ { value: "loyalty_members", label: "Loyalty Members" },
81
+ ];
82
+
83
+ const SEARCH_TREND_TYPES = [
84
+ { value: "trending_up", label: "Trending Up" },
85
+ { value: "trending_down", label: "Trending Down" },
86
+ { value: "above_threshold", label: "Above Threshold" },
87
+ { value: "below_threshold", label: "Below Threshold" },
88
+ ];
89
+
90
+ const FOOTFALL_TIPS = {
91
+ title: "Footfall Signal - Location-Based Automation",
92
+ tips: "Footfall signals automatically activate your line items based on real-time visitor density at physical locations. Use this to reach audiences when foot traffic matches your campaign goals - whether targeting busy shopping periods or quieter times for specific messaging.",
93
+ examples: [
94
+ "Retail promotion: Activate ads when your store's foot traffic drops below 30% to drive more visitors during slow periods.",
95
+ "Competitor targeting: Show your ads near competitor locations when their foot traffic exceeds 70% to capture overflow audiences.",
96
+ "Peak hours: Target high-traffic periods (above 80%) at transit hubs for maximum brand exposure during rush hours.",
97
+ ],
98
+ };
99
+
100
+ const WEATHER_TIPS = {
101
+ title: "Weather Signal - Condition-Based Automation",
102
+ tips: "Weather signals trigger your line items automatically when specific weather conditions occur in your target locations. This enables highly relevant, contextual advertising that resonates with how people feel and behave during different weather patterns.",
103
+ examples: [
104
+ "Hot weather (above 28C): Promote cold beverages, ice cream, or air conditioning services when temperatures rise.",
105
+ "Rainy conditions: Advertise delivery services, indoor entertainment, or rain gear when precipitation is detected.",
106
+ "Cold weather (below 10C): Highlight warm drinks, heating solutions, or cozy indoor dining options.",
107
+ ],
108
+ };
109
+
110
+ const AUDIENCES_TIPS = {
111
+ title: "Audience Signal - Segment-Based Automation",
112
+ tips: "Audience signals activate line items when specific customer segments are detected in your target areas. Leverage first-party and third-party audience data to reach the right people at the right moment with personalized messaging.",
113
+ examples: [
114
+ "High-intent shoppers: Activate promotional ads when purchase-ready consumers are detected near your retail locations.",
115
+ "Loyalty members: Trigger exclusive offers when your loyalty program members are in proximity to your stores.",
116
+ "Demographic targeting: Show relevant content when your target age group or income bracket is prevalent in an area.",
117
+ ],
118
+ };
119
+
120
+ const SEARCH_TIPS = {
121
+ title: "Search Signal - Trend-Based Automation",
122
+ tips: "Search signals monitor keyword trends and automatically activate your line items when search interest spikes for relevant terms. Stay ahead of demand by responding in real-time to what people are actively searching for online.",
123
+ examples: [
124
+ "Brand momentum: Trigger brand awareness ads when searches for your company name increase by 25% or more.",
125
+ "Category trends: Activate product campaigns when searches for your product category are trending upward.",
126
+ "Competitive response: Show your ads when competitor brand searches decline, capturing audience attention during their weak moments.",
127
+ ],
128
+ };
129
+
130
+ function getTipsForType(type: string | undefined) {
131
+ switch (type) {
132
+ case "footfall":
133
+ return FOOTFALL_TIPS;
134
+ case "weather":
135
+ return WEATHER_TIPS;
136
+ case "audiences":
137
+ return AUDIENCES_TIPS;
138
+ case "search":
139
+ return SEARCH_TIPS;
140
+ default:
141
+ return null;
142
+ }
143
+ }
144
+
145
+ interface FootfallRuleFormProps {
146
+ rule: RuleData;
147
+ onUpdate: (rule: RuleData) => void;
148
+ onSave: (rule: RuleData) => void;
149
+ onDelete: (id: string) => void;
150
+ index: number;
151
+ brands: Brand[];
152
+ }
153
+
154
+ function FootfallRuleForm({ rule, onUpdate, onSave, onDelete, index, brands }: FootfallRuleFormProps) {
155
+ const [vendorStoresModalOpen, setVendorStoresModalOpen] = useState(false);
156
+
157
+ const params = rule.parameters as {
158
+ brandId?: string;
159
+ storeIds?: string[];
160
+ ruleType?: string;
161
+ minBusy?: number;
162
+ maxBusy?: number;
163
+ };
164
+
165
+ const selectedBrand = brands.find(b => b.id === params.brandId);
166
+
167
+ const handleSaveStores = (storeIds: string[]) => {
168
+ onUpdate({
169
+ ...rule,
170
+ parameters: { ...params, storeIds },
171
+ });
172
+ };
173
+
174
+ return (
175
+ <Card className={cn(
176
+ "border-l-4",
177
+ rule.isSaved ? "border-l-green-500" : "border-l-primary"
178
+ )}>
179
+ <CardContent className="pt-4">
180
+ <div className="flex items-center justify-between mb-4">
181
+ <div className="flex items-center gap-2">
182
+ <h4 className="font-semibold">Rule {index + 1}</h4>
183
+ {rule.isSaved && (
184
+ <Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
185
+ Saved
186
+ </Badge>
187
+ )}
188
+ </div>
189
+ <Button
190
+ type="button"
191
+ variant="ghost"
192
+ size="icon"
193
+ onClick={() => onDelete(rule.id)}
194
+ data-testid={`button-delete-rule-${index}`}
195
+ >
196
+ <Trash2 className="h-4 w-4 text-destructive" />
197
+ </Button>
198
+ </div>
199
+
200
+ <div className="space-y-4">
201
+ <div>
202
+ <label className="text-sm font-medium">Brand</label>
203
+ <Select
204
+ value={params.brandId || ""}
205
+ onValueChange={(value) =>
206
+ onUpdate({
207
+ ...rule,
208
+ parameters: { ...params, brandId: value, storeIds: [] },
209
+ })
210
+ }
211
+ >
212
+ <SelectTrigger data-testid={`select-brand-${index}`}>
213
+ <SelectValue placeholder="Select your brand" />
214
+ </SelectTrigger>
215
+ <SelectContent>
216
+ {brands.map((brand) => (
217
+ <SelectItem key={brand.id} value={brand.id}>
218
+ {brand.name}
219
+ </SelectItem>
220
+ ))}
221
+ </SelectContent>
222
+ </Select>
223
+ {selectedBrand?.iabCategory && (
224
+ <p className="text-xs text-muted-foreground mt-1">
225
+ Category: {selectedBrand.iabCategory}
226
+ </p>
227
+ )}
228
+ </div>
229
+
230
+ <div>
231
+ <label className="text-sm font-medium">Vendor Stores</label>
232
+ <div className="flex items-center gap-2 mt-1">
233
+ <span className="text-sm text-muted-foreground">
234
+ {params.storeIds?.length || 0} Vendor Stores are selected
235
+ </span>
236
+ <Button
237
+ type="button"
238
+ variant="ghost"
239
+ className="text-primary p-0 h-auto"
240
+ onClick={() => setVendorStoresModalOpen(true)}
241
+ data-testid={`button-add-stores-${index}`}
242
+ >
243
+ <Pencil className="h-3 w-3 mr-1" />
244
+ {params.storeIds?.length ? "EDIT STORES" : "+ ADD STORES"}
245
+ </Button>
246
+ </div>
247
+ <VendorStoresModal
248
+ open={vendorStoresModalOpen}
249
+ onOpenChange={setVendorStoresModalOpen}
250
+ brandId={params.brandId || null}
251
+ brandName={selectedBrand?.name}
252
+ selectedStoreIds={params.storeIds || []}
253
+ onSave={handleSaveStores}
254
+ />
255
+ </div>
256
+
257
+ <div>
258
+ <label className="text-sm font-medium">Rule Type</label>
259
+ <Select
260
+ value={params.ruleType || "threshold"}
261
+ onValueChange={(value) =>
262
+ onUpdate({
263
+ ...rule,
264
+ parameters: { ...params, ruleType: value },
265
+ })
266
+ }
267
+ >
268
+ <SelectTrigger data-testid={`select-rule-type-${index}`}>
269
+ <SelectValue placeholder="Select rule type" />
270
+ </SelectTrigger>
271
+ <SelectContent>
272
+ <SelectItem value="threshold">Set your threshold</SelectItem>
273
+ </SelectContent>
274
+ </Select>
275
+ </div>
276
+
277
+ <div className="grid grid-cols-2 gap-4">
278
+ <div>
279
+ <label className="text-sm font-medium">Min Busy %</label>
280
+ <Input
281
+ type="number"
282
+ min={0}
283
+ max={100}
284
+ value={params.minBusy ?? 0}
285
+ onChange={(e) =>
286
+ onUpdate({
287
+ ...rule,
288
+ parameters: { ...params, minBusy: parseInt(e.target.value) || 0 },
289
+ })
290
+ }
291
+ data-testid={`input-min-busy-${index}`}
292
+ />
293
+ </div>
294
+ <div>
295
+ <label className="text-sm font-medium">Max Busy %</label>
296
+ <Input
297
+ type="number"
298
+ min={0}
299
+ max={100}
300
+ value={params.maxBusy ?? 100}
301
+ onChange={(e) =>
302
+ onUpdate({
303
+ ...rule,
304
+ parameters: { ...params, maxBusy: parseInt(e.target.value) || 0 },
305
+ })
306
+ }
307
+ data-testid={`input-max-busy-${index}`}
308
+ />
309
+ </div>
310
+ </div>
311
+
312
+ <div className="flex justify-end">
313
+ <Button
314
+ type="button"
315
+ variant="ghost"
316
+ className="text-primary"
317
+ onClick={() => onSave(rule)}
318
+ data-testid={`button-save-rule-${index}`}
319
+ >
320
+ Save
321
+ </Button>
322
+ </div>
323
+ </div>
324
+ </CardContent>
325
+ </Card>
326
+ );
327
+ }
328
+
329
+ interface WeatherRuleFormProps {
330
+ rule: RuleData;
331
+ onUpdate: (rule: RuleData) => void;
332
+ onSave: (rule: RuleData) => void;
333
+ onDelete: (id: string) => void;
334
+ index: number;
335
+ }
336
+
337
+ function WeatherRuleForm({ rule, onUpdate, onSave, onDelete, index }: WeatherRuleFormProps) {
338
+ const params = rule.parameters as {
339
+ location?: string;
340
+ condition?: string;
341
+ value?: number | string;
342
+ };
343
+
344
+ const isWeatherCondition = params.condition === "weather_condition";
345
+
346
+ return (
347
+ <Card className={cn(
348
+ "border-l-4",
349
+ rule.isSaved ? "border-l-green-500" : "border-l-primary"
350
+ )}>
351
+ <CardContent className="pt-4">
352
+ <div className="flex items-center justify-between mb-4">
353
+ <div className="flex items-center gap-2">
354
+ <h4 className="font-semibold">Rule {index + 1}</h4>
355
+ {rule.isSaved && (
356
+ <Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
357
+ Saved
358
+ </Badge>
359
+ )}
360
+ </div>
361
+ <Button
362
+ type="button"
363
+ variant="ghost"
364
+ size="icon"
365
+ onClick={() => onDelete(rule.id)}
366
+ data-testid={`button-delete-rule-${index}`}
367
+ >
368
+ <Trash2 className="h-4 w-4 text-destructive" />
369
+ </Button>
370
+ </div>
371
+
372
+ <div className="space-y-4">
373
+ <div>
374
+ <label className="text-sm font-medium">Location</label>
375
+ <Input
376
+ placeholder="Enter location"
377
+ value={params.location || ""}
378
+ onChange={(e) =>
379
+ onUpdate({
380
+ ...rule,
381
+ parameters: { ...params, location: e.target.value },
382
+ })
383
+ }
384
+ data-testid={`input-location-${index}`}
385
+ />
386
+ </div>
387
+
388
+ <div>
389
+ <label className="text-sm font-medium">Condition</label>
390
+ <Select
391
+ value={params.condition || ""}
392
+ onValueChange={(value) =>
393
+ onUpdate({
394
+ ...rule,
395
+ parameters: { ...params, condition: value, value: undefined },
396
+ })
397
+ }
398
+ >
399
+ <SelectTrigger data-testid={`select-condition-${index}`}>
400
+ <SelectValue placeholder="Select condition" />
401
+ </SelectTrigger>
402
+ <SelectContent>
403
+ {WEATHER_CONDITIONS.map((cond) => (
404
+ <SelectItem key={cond.value} value={cond.value}>
405
+ {cond.label}
406
+ </SelectItem>
407
+ ))}
408
+ </SelectContent>
409
+ </Select>
410
+ </div>
411
+
412
+ <div>
413
+ <label className="text-sm font-medium">Value</label>
414
+ {isWeatherCondition ? (
415
+ <Select
416
+ value={(params.value as string) || ""}
417
+ onValueChange={(value) =>
418
+ onUpdate({
419
+ ...rule,
420
+ parameters: { ...params, value },
421
+ })
422
+ }
423
+ >
424
+ <SelectTrigger data-testid={`select-weather-value-${index}`}>
425
+ <SelectValue placeholder="Select weather condition" />
426
+ </SelectTrigger>
427
+ <SelectContent>
428
+ {WEATHER_CONDITIONS_OPTIONS.map((opt) => (
429
+ <SelectItem key={opt.value} value={opt.value}>
430
+ {opt.label}
431
+ </SelectItem>
432
+ ))}
433
+ </SelectContent>
434
+ </Select>
435
+ ) : (
436
+ <Input
437
+ type="number"
438
+ placeholder="Temperature (°C)"
439
+ value={params.value ?? ""}
440
+ onChange={(e) =>
441
+ onUpdate({
442
+ ...rule,
443
+ parameters: { ...params, value: parseInt(e.target.value) || 0 },
444
+ })
445
+ }
446
+ data-testid={`input-value-${index}`}
447
+ />
448
+ )}
449
+ </div>
450
+
451
+ <div className="flex justify-end">
452
+ <Button
453
+ type="button"
454
+ variant="ghost"
455
+ className="text-primary"
456
+ onClick={() => onSave(rule)}
457
+ data-testid={`button-save-rule-${index}`}
458
+ >
459
+ Save
460
+ </Button>
461
+ </div>
462
+ </div>
463
+ </CardContent>
464
+ </Card>
465
+ );
466
+ }
467
+
468
+ interface AudiencesRuleFormProps {
469
+ rule: RuleData;
470
+ onUpdate: (rule: RuleData) => void;
471
+ onSave: (rule: RuleData) => void;
472
+ onDelete: (id: string) => void;
473
+ index: number;
474
+ }
475
+
476
+ function AudiencesRuleForm({ rule, onUpdate, onSave, onDelete, index }: AudiencesRuleFormProps) {
477
+ const params = rule.parameters as {
478
+ segment?: string;
479
+ minThreshold?: number;
480
+ maxThreshold?: number;
481
+ };
482
+
483
+ return (
484
+ <Card className={cn(
485
+ "border-l-4",
486
+ rule.isSaved ? "border-l-green-500" : "border-l-primary"
487
+ )}>
488
+ <CardContent className="pt-4">
489
+ <div className="flex items-center justify-between mb-4">
490
+ <div className="flex items-center gap-2">
491
+ <h4 className="font-semibold">Rule {index + 1}</h4>
492
+ {rule.isSaved && (
493
+ <Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
494
+ Saved
495
+ </Badge>
496
+ )}
497
+ </div>
498
+ <Button
499
+ type="button"
500
+ variant="ghost"
501
+ size="icon"
502
+ onClick={() => onDelete(rule.id)}
503
+ data-testid={`button-delete-rule-${index}`}
504
+ >
505
+ <Trash2 className="h-4 w-4 text-destructive" />
506
+ </Button>
507
+ </div>
508
+
509
+ <div className="space-y-4">
510
+ <div>
511
+ <label className="text-sm font-medium">Audience Segment</label>
512
+ <Select
513
+ value={params.segment || ""}
514
+ onValueChange={(value) =>
515
+ onUpdate({
516
+ ...rule,
517
+ parameters: { ...params, segment: value },
518
+ })
519
+ }
520
+ >
521
+ <SelectTrigger data-testid={`select-segment-${index}`}>
522
+ <SelectValue placeholder="Select audience segment" />
523
+ </SelectTrigger>
524
+ <SelectContent>
525
+ {AUDIENCE_SEGMENTS.map((seg) => (
526
+ <SelectItem key={seg.value} value={seg.value}>
527
+ {seg.label}
528
+ </SelectItem>
529
+ ))}
530
+ </SelectContent>
531
+ </Select>
532
+ </div>
533
+
534
+ <div className="grid grid-cols-2 gap-4">
535
+ <div>
536
+ <label className="text-sm font-medium">Min Threshold %</label>
537
+ <Input
538
+ type="number"
539
+ min={0}
540
+ max={100}
541
+ value={params.minThreshold ?? 0}
542
+ onChange={(e) =>
543
+ onUpdate({
544
+ ...rule,
545
+ parameters: { ...params, minThreshold: parseInt(e.target.value) || 0 },
546
+ })
547
+ }
548
+ data-testid={`input-min-threshold-${index}`}
549
+ />
550
+ </div>
551
+ <div>
552
+ <label className="text-sm font-medium">Max Threshold %</label>
553
+ <Input
554
+ type="number"
555
+ min={0}
556
+ max={100}
557
+ value={params.maxThreshold ?? 100}
558
+ onChange={(e) =>
559
+ onUpdate({
560
+ ...rule,
561
+ parameters: { ...params, maxThreshold: parseInt(e.target.value) || 0 },
562
+ })
563
+ }
564
+ data-testid={`input-max-threshold-${index}`}
565
+ />
566
+ </div>
567
+ </div>
568
+
569
+ <div className="flex justify-end">
570
+ <Button
571
+ type="button"
572
+ variant="ghost"
573
+ className="text-primary"
574
+ onClick={() => onSave(rule)}
575
+ data-testid={`button-save-rule-${index}`}
576
+ >
577
+ Save
578
+ </Button>
579
+ </div>
580
+ </div>
581
+ </CardContent>
582
+ </Card>
583
+ );
584
+ }
585
+
586
+ interface SearchRuleFormProps {
587
+ rule: RuleData;
588
+ onUpdate: (rule: RuleData) => void;
589
+ onSave: (rule: RuleData) => void;
590
+ onDelete: (id: string) => void;
591
+ index: number;
592
+ }
593
+
594
+ function SearchRuleForm({ rule, onUpdate, onSave, onDelete, index }: SearchRuleFormProps) {
595
+ const params = rule.parameters as {
596
+ keyword?: string;
597
+ trendType?: string;
598
+ threshold?: number;
599
+ };
600
+
601
+ return (
602
+ <Card className={cn(
603
+ "border-l-4",
604
+ rule.isSaved ? "border-l-green-500" : "border-l-primary"
605
+ )}>
606
+ <CardContent className="pt-4">
607
+ <div className="flex items-center justify-between mb-4">
608
+ <div className="flex items-center gap-2">
609
+ <h4 className="font-semibold">Rule {index + 1}</h4>
610
+ {rule.isSaved && (
611
+ <Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
612
+ Saved
613
+ </Badge>
614
+ )}
615
+ </div>
616
+ <Button
617
+ type="button"
618
+ variant="ghost"
619
+ size="icon"
620
+ onClick={() => onDelete(rule.id)}
621
+ data-testid={`button-delete-rule-${index}`}
622
+ >
623
+ <Trash2 className="h-4 w-4 text-destructive" />
624
+ </Button>
625
+ </div>
626
+
627
+ <div className="space-y-4">
628
+ <div>
629
+ <label className="text-sm font-medium">Keyword</label>
630
+ <Input
631
+ placeholder="Enter search keyword"
632
+ value={params.keyword || ""}
633
+ onChange={(e) =>
634
+ onUpdate({
635
+ ...rule,
636
+ parameters: { ...params, keyword: e.target.value },
637
+ })
638
+ }
639
+ data-testid={`input-keyword-${index}`}
640
+ />
641
+ </div>
642
+
643
+ <div>
644
+ <label className="text-sm font-medium">Trend Type</label>
645
+ <Select
646
+ value={params.trendType || ""}
647
+ onValueChange={(value) =>
648
+ onUpdate({
649
+ ...rule,
650
+ parameters: { ...params, trendType: value },
651
+ })
652
+ }
653
+ >
654
+ <SelectTrigger data-testid={`select-trend-type-${index}`}>
655
+ <SelectValue placeholder="Select trend type" />
656
+ </SelectTrigger>
657
+ <SelectContent>
658
+ {SEARCH_TREND_TYPES.map((trend) => (
659
+ <SelectItem key={trend.value} value={trend.value}>
660
+ {trend.label}
661
+ </SelectItem>
662
+ ))}
663
+ </SelectContent>
664
+ </Select>
665
+ </div>
666
+
667
+ <div>
668
+ <label className="text-sm font-medium">Threshold %</label>
669
+ <Input
670
+ type="number"
671
+ min={0}
672
+ max={100}
673
+ value={params.threshold ?? 0}
674
+ onChange={(e) =>
675
+ onUpdate({
676
+ ...rule,
677
+ parameters: { ...params, threshold: parseInt(e.target.value) || 0 },
678
+ })
679
+ }
680
+ data-testid={`input-threshold-${index}`}
681
+ />
682
+ </div>
683
+
684
+ <div className="flex justify-end">
685
+ <Button
686
+ type="button"
687
+ variant="ghost"
688
+ className="text-primary"
689
+ onClick={() => onSave(rule)}
690
+ data-testid={`button-save-rule-${index}`}
691
+ >
692
+ Save
693
+ </Button>
694
+ </div>
695
+ </div>
696
+ </CardContent>
697
+ </Card>
698
+ );
699
+ }
700
+
701
+ export default function SignalForm() {
702
+ const [, setLocation] = useLocation();
703
+ const [matchCreate] = useRoute("/signals/create");
704
+ const [matchEdit, editParams] = useRoute("/signals/:id/edit");
705
+ const { toast } = useToast();
706
+
707
+ const isEditing = Boolean(matchEdit);
708
+ const signalId = editParams?.id;
709
+
710
+ const [rules, setRules] = useState<RuleData[]>([]);
711
+
712
+ const { data: existingSignal, isLoading: signalLoading } = useQuery<Signal>({
713
+ queryKey: ["/api/signals", signalId],
714
+ enabled: isEditing && Boolean(signalId),
715
+ });
716
+
717
+ const { data: existingRules = [] } = useQuery<SignalRule[]>({
718
+ queryKey: ["/api/signals", signalId, "rules"],
719
+ enabled: isEditing && Boolean(signalId),
720
+ });
721
+
722
+ const { data: brands = [] } = useQuery<Brand[]>({
723
+ queryKey: ["/api/brands"],
724
+ });
725
+
726
+ const form = useForm<SignalFormData>({
727
+ resolver: zodResolver(signalFormSchema),
728
+ defaultValues: {
729
+ name: "",
730
+ signalType: "",
731
+ status: "active",
732
+ },
733
+ });
734
+
735
+ const watchedType = form.watch("signalType");
736
+
737
+ useEffect(() => {
738
+ if (existingSignal) {
739
+ form.reset({
740
+ name: existingSignal.name,
741
+ signalType: existingSignal.signalType,
742
+ status: existingSignal.status as "active" | "inactive",
743
+ });
744
+ }
745
+ }, [existingSignal, form]);
746
+
747
+ useEffect(() => {
748
+ if (existingRules.length > 0) {
749
+ setRules(
750
+ existingRules.map((r) => ({
751
+ id: r.id,
752
+ name: r.name,
753
+ conditionType: r.conditionType,
754
+ parameters: (r.parameters as Record<string, unknown>) || {},
755
+ isSaved: true,
756
+ }))
757
+ );
758
+ }
759
+ }, [existingRules]);
760
+
761
+ const createSignalMutation = useMutation({
762
+ mutationFn: async (data: SignalFormData) => {
763
+ const response = await apiRequest("POST", "/api/signals", data);
764
+ return response.json();
765
+ },
766
+ onSuccess: async (signal: Signal) => {
767
+ for (const rule of rules) {
768
+ await apiRequest("POST", `/api/signals/${signal.id}/rules`, {
769
+ name: rule.name,
770
+ conditionType: rule.conditionType,
771
+ parameters: rule.parameters,
772
+ });
773
+ }
774
+ queryClient.invalidateQueries({ queryKey: ["/api/signals"] });
775
+ toast({ title: "Signal created successfully" });
776
+ setLocation("/signals");
777
+ },
778
+ onError: () => {
779
+ toast({ title: "Failed to create signal", variant: "destructive" });
780
+ },
781
+ });
782
+
783
+ const updateSignalMutation = useMutation({
784
+ mutationFn: async (data: SignalFormData) => {
785
+ const response = await apiRequest("PATCH", `/api/signals/${signalId}`, data);
786
+ return response.json();
787
+ },
788
+ onSuccess: async () => {
789
+ for (const rule of rules) {
790
+ if (rule.isNew) {
791
+ await apiRequest("POST", `/api/signals/${signalId}/rules`, {
792
+ name: rule.name,
793
+ conditionType: rule.conditionType,
794
+ parameters: rule.parameters,
795
+ });
796
+ } else {
797
+ await apiRequest("PATCH", `/api/signals/${signalId}/rules/${rule.id}`, {
798
+ name: rule.name,
799
+ conditionType: rule.conditionType,
800
+ parameters: rule.parameters,
801
+ });
802
+ }
803
+ }
804
+ queryClient.invalidateQueries({ queryKey: ["/api/signals"] });
805
+ queryClient.invalidateQueries({ queryKey: ["/api/signals", signalId] });
806
+ toast({ title: "Signal updated successfully" });
807
+ setLocation("/signals");
808
+ },
809
+ onError: () => {
810
+ toast({ title: "Failed to update signal", variant: "destructive" });
811
+ },
812
+ });
813
+
814
+ const onSubmit = (data: SignalFormData) => {
815
+ if (isEditing) {
816
+ updateSignalMutation.mutate(data);
817
+ } else {
818
+ createSignalMutation.mutate(data);
819
+ }
820
+ };
821
+
822
+ const addRule = () => {
823
+ const newRule: RuleData = {
824
+ id: `temp-${Date.now()}`,
825
+ name: `Rule ${rules.length + 1}`,
826
+ conditionType: watchedType,
827
+ parameters: {},
828
+ isNew: true,
829
+ };
830
+ setRules([...rules, newRule]);
831
+ };
832
+
833
+ const updateRule = (updatedRule: RuleData) => {
834
+ setRules(rules.map((r) => (r.id === updatedRule.id ? updatedRule : r)));
835
+ };
836
+
837
+ const saveRule = (rule: RuleData) => {
838
+ setRules(rules.map((r) => (r.id === rule.id ? { ...rule, isSaved: true } : r)));
839
+ toast({ title: `Rule saved` });
840
+ };
841
+
842
+ const deleteRule = async (ruleId: string) => {
843
+ const rule = rules.find((r) => r.id === ruleId);
844
+ if (rule && !rule.isNew && isEditing) {
845
+ try {
846
+ await apiRequest("DELETE", `/api/signals/${signalId}/rules/${ruleId}`);
847
+ } catch {
848
+ toast({ title: "Failed to delete rule", variant: "destructive" });
849
+ return;
850
+ }
851
+ }
852
+ setRules(rules.filter((r) => r.id !== ruleId));
853
+ toast({ title: "Rule deleted" });
854
+ };
855
+
856
+ const tips = getTipsForType(watchedType);
857
+ const isPending = createSignalMutation.isPending || updateSignalMutation.isPending;
858
+
859
+ if (isEditing && signalLoading) {
860
+ return (
861
+ <div className="flex items-center justify-center h-screen">
862
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
863
+ </div>
864
+ );
865
+ }
866
+
867
+ return (
868
+ <div className="flex flex-col h-screen">
869
+ <header className="sticky top-0 z-10 bg-background border-b px-6 py-4">
870
+ <div className="flex items-center justify-between">
871
+ <h1 className="text-xl font-semibold" data-testid="text-page-title">
872
+ {isEditing ? "Edit Signal" : "New Signal"}
873
+ </h1>
874
+ <div className="flex items-center gap-4">
875
+ <FormField
876
+ control={form.control}
877
+ name="status"
878
+ render={({ field }) => (
879
+ <Select value={field.value} onValueChange={field.onChange}>
880
+ <SelectTrigger className="w-32" data-testid="select-status">
881
+ <SelectValue />
882
+ </SelectTrigger>
883
+ <SelectContent>
884
+ <SelectItem value="active">Active</SelectItem>
885
+ <SelectItem value="inactive">Inactive</SelectItem>
886
+ </SelectContent>
887
+ </Select>
888
+ )}
889
+ />
890
+ <Link href="/signals">
891
+ <Button variant="outline" className="text-destructive border-destructive hover:bg-destructive/10" data-testid="button-cancel">
892
+ Cancel
893
+ </Button>
894
+ </Link>
895
+ </div>
896
+ </div>
897
+ <p className="text-sm text-muted-foreground mt-1">
898
+ You can add multiple rules to activate the signal. If any of the rules is true, the signal activates the associated line item automatically.
899
+ </p>
900
+ </header>
901
+
902
+ <div className="flex-1 overflow-y-auto">
903
+ <Form {...form}>
904
+ <form onSubmit={form.handleSubmit(onSubmit)} id="signal-form" className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
905
+ <div className="space-y-6">
906
+ <Card>
907
+ <CardHeader>
908
+ <CardTitle>Signal Details</CardTitle>
909
+ </CardHeader>
910
+ <CardContent className="space-y-4">
911
+ <FormField
912
+ control={form.control}
913
+ name="name"
914
+ render={({ field }) => (
915
+ <FormItem>
916
+ <FormLabel>Signal Name</FormLabel>
917
+ <FormControl>
918
+ <Input
919
+ placeholder="Enter a descriptive name for this signal"
920
+ {...field}
921
+ data-testid="input-signal-name"
922
+ />
923
+ </FormControl>
924
+ <FormMessage />
925
+ </FormItem>
926
+ )}
927
+ />
928
+
929
+ <FormField
930
+ control={form.control}
931
+ name="signalType"
932
+ render={({ field }) => (
933
+ <FormItem>
934
+ <FormLabel>Signal Type</FormLabel>
935
+ <Select
936
+ value={field.value}
937
+ onValueChange={(value) => {
938
+ field.onChange(value);
939
+ if (watchedType !== value) {
940
+ setRules([]);
941
+ }
942
+ }}
943
+ >
944
+ <SelectTrigger data-testid="select-signal-type">
945
+ <SelectValue placeholder="Select signal type" />
946
+ </SelectTrigger>
947
+ <SelectContent>
948
+ {SIGNAL_TYPES.map((type) => (
949
+ <SelectItem key={type.value} value={type.value}>
950
+ {type.label}
951
+ </SelectItem>
952
+ ))}
953
+ </SelectContent>
954
+ </Select>
955
+ <FormMessage />
956
+ </FormItem>
957
+ )}
958
+ />
959
+ </CardContent>
960
+ </Card>
961
+
962
+ <Card>
963
+ <CardHeader>
964
+ <div className="flex items-center justify-between">
965
+ <div>
966
+ <CardTitle>Rules</CardTitle>
967
+ <p className="text-sm text-muted-foreground mt-1">
968
+ Configure when this signal activates
969
+ </p>
970
+ </div>
971
+ <Button
972
+ type="button"
973
+ variant="outline"
974
+ onClick={addRule}
975
+ disabled={!watchedType}
976
+ data-testid="button-add-rule"
977
+ >
978
+ <Plus className="h-4 w-4 mr-2" />
979
+ Add Rule
980
+ </Button>
981
+ </div>
982
+ </CardHeader>
983
+ <CardContent>
984
+ {rules.length === 0 ? (
985
+ <div className="border-2 border-dashed rounded-lg p-8 text-center">
986
+ <div className="mx-auto w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
987
+ <Plus className="h-6 w-6 text-muted-foreground" />
988
+ </div>
989
+ <p className="font-medium mb-1">No rules configured</p>
990
+ <p className="text-sm text-muted-foreground mb-4">
991
+ Add rules to define when this signal should activate
992
+ </p>
993
+ <Button
994
+ type="button"
995
+ variant="outline"
996
+ onClick={addRule}
997
+ disabled={!watchedType}
998
+ data-testid="button-add-first-rule"
999
+ >
1000
+ Add Your First Rule
1001
+ </Button>
1002
+ </div>
1003
+ ) : (
1004
+ <div className="space-y-4">
1005
+ {rules.map((rule, index) => {
1006
+ switch (watchedType) {
1007
+ case "footfall":
1008
+ return (
1009
+ <FootfallRuleForm
1010
+ key={rule.id}
1011
+ rule={rule}
1012
+ onUpdate={updateRule}
1013
+ onSave={saveRule}
1014
+ onDelete={deleteRule}
1015
+ index={index}
1016
+ brands={brands}
1017
+ />
1018
+ );
1019
+ case "weather":
1020
+ return (
1021
+ <WeatherRuleForm
1022
+ key={rule.id}
1023
+ rule={rule}
1024
+ onUpdate={updateRule}
1025
+ onSave={saveRule}
1026
+ onDelete={deleteRule}
1027
+ index={index}
1028
+ />
1029
+ );
1030
+ case "audiences":
1031
+ return (
1032
+ <AudiencesRuleForm
1033
+ key={rule.id}
1034
+ rule={rule}
1035
+ onUpdate={updateRule}
1036
+ onSave={saveRule}
1037
+ onDelete={deleteRule}
1038
+ index={index}
1039
+ />
1040
+ );
1041
+ case "search":
1042
+ return (
1043
+ <SearchRuleForm
1044
+ key={rule.id}
1045
+ rule={rule}
1046
+ onUpdate={updateRule}
1047
+ onSave={saveRule}
1048
+ onDelete={deleteRule}
1049
+ index={index}
1050
+ />
1051
+ );
1052
+ default:
1053
+ return null;
1054
+ }
1055
+ })}
1056
+ </div>
1057
+ )}
1058
+ </CardContent>
1059
+ </Card>
1060
+ </div>
1061
+
1062
+ <div className="space-y-4">
1063
+ {watchedType && rules.length > 0 && (
1064
+ <Card>
1065
+ <CardContent className="p-6">
1066
+ <SignalVisualization type={watchedType} rules={rules} />
1067
+ </CardContent>
1068
+ </Card>
1069
+ )}
1070
+ <Card>
1071
+ <CardContent className="p-6">
1072
+ <div className="flex items-start gap-3">
1073
+ <div className="p-2 rounded-full bg-yellow-100 dark:bg-yellow-900/30">
1074
+ <Lightbulb className="h-5 w-5 text-yellow-600 dark:text-yellow-500" />
1075
+ </div>
1076
+ <div className="space-y-3">
1077
+ <p className="font-medium">
1078
+ {tips?.title || "Select signal type and add rules to view forecasting."}
1079
+ </p>
1080
+ {tips && (
1081
+ <>
1082
+ <p className="text-sm text-muted-foreground">{tips.tips}</p>
1083
+ <div className="text-sm text-muted-foreground">
1084
+ <p className="font-medium mb-2">Example:</p>
1085
+ <ol className="list-decimal list-inside space-y-1">
1086
+ {tips.examples.map((example, i) => (
1087
+ <li key={i}>{example}</li>
1088
+ ))}
1089
+ </ol>
1090
+ </div>
1091
+ </>
1092
+ )}
1093
+ </div>
1094
+ </div>
1095
+ </CardContent>
1096
+ </Card>
1097
+ </div>
1098
+ </form>
1099
+ </Form>
1100
+ </div>
1101
+
1102
+ <footer className="sticky bottom-0 z-10 bg-background border-t px-6 py-4">
1103
+ <div className="flex items-center justify-between gap-4">
1104
+ <Link href="/signals">
1105
+ <Button type="button" variant="ghost" data-testid="button-back">
1106
+ Back
1107
+ </Button>
1108
+ </Link>
1109
+ <Button type="submit" form="signal-form" disabled={isPending} data-testid="button-save">
1110
+ {isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
1111
+ Save
1112
+ </Button>
1113
+ </div>
1114
+ </footer>
1115
+ </div>
1116
+ );
1117
+ }