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,1570 @@
1
+ import { useEffect, useMemo, useState, useRef, useCallback } from "react";
2
+ import { useQuery, useMutation } from "@tanstack/react-query";
3
+ import { useRoute, useLocation } from "wouter";
4
+ import { useForm, useWatch } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import { ArrowLeft, Loader2, User, Tag, Globe, ChevronDown, X, Plus, Lightbulb, Building2, DollarSign, Target, AlertTriangle } from "lucide-react";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Input } from "@/components/ui/input";
10
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
11
+ import { Checkbox } from "@/components/ui/checkbox";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Switch } from "@/components/ui/switch";
14
+ import {
15
+ Form,
16
+ FormControl,
17
+ FormField,
18
+ FormItem,
19
+ FormLabel,
20
+ FormMessage,
21
+ FormDescription,
22
+ } from "@/components/ui/form";
23
+ import {
24
+ Popover,
25
+ PopoverContent,
26
+ PopoverTrigger,
27
+ } from "@/components/ui/popover";
28
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
29
+ import { PageHeader } from "@/components/page-header";
30
+ import { SearchableCombobox, ComboboxOption } from "@/components/searchable-combobox";
31
+ import { Skeleton } from "@/components/ui/skeleton";
32
+ import { useToast } from "@/hooks/use-toast";
33
+ import { apiRequest, queryClient } from "@/lib/queryClient";
34
+ import type { Deal, Brand, Agency, SspPartner, DspPartner } from "@shared/schema";
35
+ import { MEDIA_TYPES, ADPLAY_VERIFICATION_PROVIDERS, DEAL_TYPES, GOAL_TYPES } from "@shared/schema";
36
+ import { BrandInsightsPanel } from "@/components/brand-insights-panel";
37
+ import { MarketInsightsPanel } from "@/components/market-insights-panel";
38
+ import { CreateBrandDrawer } from "@/components/create-brand-drawer";
39
+ import { CreateAgencyDrawer } from "@/components/create-agency-drawer";
40
+
41
+ // Section-specific Quick Tips that change based on scroll position
42
+ type TipSection = "basic" | "ssp" | "brand" | "budget";
43
+
44
+ interface QuickTip {
45
+ title: string;
46
+ tips: string[];
47
+ }
48
+
49
+ const SECTION_TIPS: Record<TipSection, QuickTip> = {
50
+ basic: {
51
+ title: "Deal Basics",
52
+ tips: [
53
+ "Deal Name should be descriptive and unique to easily identify it in lists",
54
+ "Status controls whether the deal is Active (running) or Paused (stopped)",
55
+ "Media Type defines the channel - DOOH (Digital Out-of-Home) is for digital screens",
56
+ "External Deal ID helps sync with external systems like your CRM",
57
+ "Billable flag determines if this deal is included in invoicing",
58
+ ],
59
+ },
60
+ ssp: {
61
+ title: "Supply Configuration",
62
+ tips: [
63
+ "SSP Partner (Supply-Side Platform) provides access to inventory networks",
64
+ "Deal Type determines pricing model - Traditional or Programmatic",
65
+ "Programmatic deals enable automated real-time bidding with DSP partners",
66
+ "Media Owner is selected at Line Item level for more granular control",
67
+ "Client Type indicates if this is a direct client or through an agency",
68
+ ],
69
+ },
70
+ brand: {
71
+ title: "Brand & Verification",
72
+ tips: [
73
+ "Selecting a brand enables brand-specific insights and recommendations",
74
+ "AdPlay Verification ensures ads are actually played on screens",
75
+ "Countries selected here limit geography targeting in line items",
76
+ "Line items can only target cities/areas within the deal's countries",
77
+ ],
78
+ },
79
+ budget: {
80
+ title: "Budget & Goals",
81
+ tips: [
82
+ "Currency determines the monetary unit for all budget and pricing",
83
+ "Currency margin (3%) applies when inventory uses different currencies",
84
+ "Goal Type sets what you're optimizing for - Impressions, Reach, etc.",
85
+ "Budget and Goal values help with better inventory recommendations in Line Items",
86
+ "Setting budget enables spend tracking and pacing recommendations",
87
+ ],
88
+ },
89
+ };
90
+
91
+ interface CountryOption {
92
+ code: string;
93
+ name: string;
94
+ flag: string;
95
+ }
96
+
97
+ const COUNTRIES: CountryOption[] = [
98
+ { code: "MY", name: "Malaysia", flag: "MY" },
99
+ { code: "US", name: "United States", flag: "US" },
100
+ { code: "CN", name: "China", flag: "CN" },
101
+ { code: "IN", name: "India", flag: "IN" },
102
+ { code: "GB", name: "United Kingdom", flag: "GB" },
103
+ { code: "SG", name: "Singapore", flag: "SG" },
104
+ { code: "TH", name: "Thailand", flag: "TH" },
105
+ { code: "ID", name: "Indonesia", flag: "ID" },
106
+ { code: "PH", name: "Philippines", flag: "PH" },
107
+ { code: "VN", name: "Vietnam", flag: "VN" },
108
+ { code: "AU", name: "Australia", flag: "AU" },
109
+ { code: "JP", name: "Japan", flag: "JP" },
110
+ { code: "KR", name: "South Korea", flag: "KR" },
111
+ { code: "DE", name: "Germany", flag: "DE" },
112
+ { code: "FR", name: "France", flag: "FR" },
113
+ { code: "BR", name: "Brazil", flag: "BR" },
114
+ { code: "MX", name: "Mexico", flag: "MX" },
115
+ { code: "CA", name: "Canada", flag: "CA" },
116
+ { code: "AE", name: "UAE", flag: "AE" },
117
+ { code: "SA", name: "Saudi Arabia", flag: "SA" },
118
+ ];
119
+
120
+ // Country to currency mapping
121
+ const COUNTRY_CURRENCY_MAP: Record<string, string> = {
122
+ MY: "MYR",
123
+ US: "USD",
124
+ CN: "CNY",
125
+ IN: "INR",
126
+ GB: "GBP",
127
+ SG: "SGD",
128
+ TH: "USD", // Thailand uses USD for DOOH typically
129
+ ID: "USD", // Indonesia uses USD for DOOH typically
130
+ PH: "USD", // Philippines uses USD for DOOH typically
131
+ VN: "USD", // Vietnam uses USD for DOOH typically
132
+ AU: "AUD",
133
+ JP: "JPY",
134
+ KR: "USD", // South Korea uses USD for DOOH typically
135
+ DE: "EUR",
136
+ FR: "EUR",
137
+ BR: "USD", // Brazil uses USD for DOOH typically
138
+ MX: "USD", // Mexico uses USD for DOOH typically
139
+ CA: "CAD",
140
+ AE: "AED",
141
+ SA: "USD", // Saudi Arabia uses USD for DOOH typically
142
+ };
143
+
144
+ const CURRENCIES: { value: string; label: string }[] = [
145
+ { value: "USD", label: "USD - US Dollar" },
146
+ { value: "EUR", label: "EUR - Euro" },
147
+ { value: "GBP", label: "GBP - British Pound" },
148
+ { value: "MYR", label: "MYR - Malaysian Ringgit" },
149
+ { value: "SGD", label: "SGD - Singapore Dollar" },
150
+ { value: "AUD", label: "AUD - Australian Dollar" },
151
+ { value: "JPY", label: "JPY - Japanese Yen" },
152
+ { value: "INR", label: "INR - Indian Rupee" },
153
+ { value: "CNY", label: "CNY - Chinese Yuan" },
154
+ { value: "AED", label: "AED - UAE Dirham" },
155
+ ];
156
+
157
+ // Dummy DSP seats for demo - in production this comes from Admin Console
158
+ // Seat IDs are 6-7 digit numbers
159
+ const DUMMY_DSP_SEATS: { dspId: string; seatName: string; seatId: string }[] = [
160
+ { dspId: "dsp-activate-default", seatName: "Activate Primary", seatId: "123456" },
161
+ { dspId: "dsp-activate-default", seatName: "Activate Agency Desk", seatId: "234567" },
162
+ { dspId: "dsp-activate-default", seatName: "Activate Self-Serve", seatId: "3456789" },
163
+ { dspId: "dsp-1", seatName: "Main Trading Desk", seatId: "456789" },
164
+ { dspId: "dsp-1", seatName: "Secondary Desk", seatId: "567890" },
165
+ { dspId: "dsp-2", seatName: "Primary Account", seatId: "6789012" },
166
+ { dspId: "dsp-2", seatName: "Agency Account", seatId: "789012" },
167
+ ];
168
+
169
+ const dealFormSchema = z.object({
170
+ name: z.string().min(1, "Deal name is required"),
171
+ status: z.enum(["active", "paused"]).default("active"),
172
+ externalDealId: z.string().optional(),
173
+ billable: z.boolean().default(true),
174
+ mediaType: z.enum(["dooh", "mobile", "youtube", "ctv"]).default("dooh"),
175
+ sspId: z.string().min(1, "SSP Partner is required"),
176
+ dealType: z.enum(["traditional", "pg", "preferred_deal", "pmp", "always_on", "open_auction"]).default("traditional"),
177
+ brandId: z.string().optional(),
178
+ clientType: z.enum(["direct", "agency"]).default("direct"),
179
+ agencyId: z.string().optional(),
180
+ dspId: z.string().optional(),
181
+ seatName: z.string().optional(),
182
+ seatId: z.string().optional(),
183
+ adPlayVerificationEnabled: z.boolean().default(false),
184
+ adPlayVerificationProvider: z.string().optional(),
185
+ countries: z.array(z.string()).min(1, "At least one country is required").default(["MY"]),
186
+ currency: z.string().min(1, "Currency is required").default("USD"),
187
+ budget: z.string().optional(),
188
+ goalType: z.string().optional(),
189
+ goalValue: z.string().optional(),
190
+ minimumTargetThreshold: z.string().optional(),
191
+ hardStop: z.boolean().default(false),
192
+ }).refine((data) => {
193
+ if (data.adPlayVerificationEnabled && !data.adPlayVerificationProvider) {
194
+ return false;
195
+ }
196
+ return true;
197
+ }, {
198
+ message: "Service provider is required when AdPlay Verification is enabled",
199
+ path: ["adPlayVerificationProvider"],
200
+ }).refine((data) => {
201
+ if (data.clientType === "agency" && !data.agencyId) {
202
+ return false;
203
+ }
204
+ return true;
205
+ }, {
206
+ message: "Please select an agency",
207
+ path: ["agencyId"],
208
+ }).refine((data) => {
209
+ if (data.dealType === "pg" && !data.minimumTargetThreshold) {
210
+ return false;
211
+ }
212
+ return true;
213
+ }, {
214
+ message: "Minimum Target Threshold is required for Programmatic Guaranteed deals",
215
+ path: ["minimumTargetThreshold"],
216
+ });
217
+
218
+ type DealFormData = z.infer<typeof dealFormSchema>;
219
+
220
+ const statusOptions: ComboboxOption[] = [
221
+ { value: "active", label: "Active" },
222
+ { value: "paused", label: "Paused" },
223
+ ];
224
+
225
+ const clientTypeOptions: ComboboxOption[] = [
226
+ { value: "direct", label: "Direct Advertiser" },
227
+ { value: "agency", label: "Agency" },
228
+ ];
229
+
230
+ const mediaTypeOptions: ComboboxOption[] = MEDIA_TYPES.map((mt) => ({
231
+ value: mt.value,
232
+ label: mt.label,
233
+ disabled: mt.disabled,
234
+ }));
235
+
236
+ const dealTypeOptions: ComboboxOption[] = [
237
+ { value: "traditional", label: "Traditional" },
238
+ { value: "programmatic", label: "Programmatic" },
239
+ ];
240
+
241
+ const programmaticSubtypeOptions: ComboboxOption[] = [
242
+ { value: "pg", label: "Programmatic Guaranteed" },
243
+ { value: "preferred_deal", label: "Preferred Deal" },
244
+ { value: "pmp", label: "Private Marketplace (PMP)" },
245
+ { value: "always_on", label: "Always On" },
246
+ { value: "open_auction", label: "Open Auction" },
247
+ ];
248
+
249
+ const providerOptions: ComboboxOption[] = ADPLAY_VERIFICATION_PROVIDERS.map((provider) => ({
250
+ value: provider,
251
+ label: provider,
252
+ }));
253
+
254
+ // Currency options will be computed dynamically based on selected countries
255
+
256
+ const goalTypeOptions: ComboboxOption[] = GOAL_TYPES.map((gt) => ({
257
+ value: gt.value,
258
+ label: gt.label,
259
+ }));
260
+
261
+ export default function DealForm() {
262
+ const [, setLocation] = useLocation();
263
+ const [matchNew] = useRoute("/deals/new");
264
+ const [matchEdit, params] = useRoute("/deals/:id/edit");
265
+ const { toast } = useToast();
266
+
267
+ const isEditing = Boolean(matchEdit);
268
+ const dealId = params?.id;
269
+
270
+ const { data: existingDeal, isLoading: dealLoading } = useQuery<Deal>({
271
+ queryKey: ["/api/deals", dealId],
272
+ enabled: isEditing && Boolean(dealId),
273
+ });
274
+
275
+ const { data: brands } = useQuery<Brand[]>({
276
+ queryKey: ["/api/brands"],
277
+ });
278
+
279
+ const { data: agencies } = useQuery<Agency[]>({
280
+ queryKey: ["/api/agencies"],
281
+ });
282
+
283
+ const { data: sspPartners } = useQuery<SspPartner[]>({
284
+ queryKey: ["/api/ssp-partners"],
285
+ });
286
+
287
+ const { data: dspPartners } = useQuery<DspPartner[]>({
288
+ queryKey: ["/api/dsp-partners"],
289
+ });
290
+
291
+ const brandOptions: ComboboxOption[] = useMemo(() => {
292
+ return (brands || []).map((brand) => ({
293
+ value: brand.id,
294
+ label: brand.name,
295
+ }));
296
+ }, [brands]);
297
+
298
+ const agencyOptions: ComboboxOption[] = useMemo(() => {
299
+ return (agencies || []).map((agency) => {
300
+ const countryName = agency.country
301
+ ? COUNTRIES.find(c => c.code === agency.country)?.name || agency.country
302
+ : undefined;
303
+ return {
304
+ value: agency.id,
305
+ label: agency.name,
306
+ description: countryName,
307
+ };
308
+ });
309
+ }, [agencies]);
310
+
311
+ const sspOptions: ComboboxOption[] = useMemo(() => {
312
+ return (sspPartners || []).map((ssp) => ({
313
+ value: ssp.id,
314
+ label: ssp.name,
315
+ }));
316
+ }, [sspPartners]);
317
+
318
+ const dspOptions: ComboboxOption[] = useMemo(() => {
319
+ return (dspPartners || []).map((dsp) => ({
320
+ value: dsp.id,
321
+ label: dsp.name,
322
+ }));
323
+ }, [dspPartners]);
324
+
325
+ // Generate default deal name with format Deal_Jan_23_26_001
326
+ const generateDealName = () => {
327
+ const now = new Date();
328
+ const month = now.toLocaleDateString("en-US", { month: "short" });
329
+ const day = now.getDate().toString().padStart(2, "0");
330
+ const year = now.getFullYear().toString().slice(-2);
331
+ return `Deal_${month}_${day}_${year}_001`;
332
+ };
333
+ const defaultDealName = isEditing ? "" : generateDealName();
334
+
335
+ const form = useForm<DealFormData>({
336
+ resolver: zodResolver(dealFormSchema),
337
+ defaultValues: {
338
+ name: defaultDealName,
339
+ status: "active",
340
+ externalDealId: "",
341
+ billable: true,
342
+ mediaType: "dooh",
343
+ sspId: "ssp-influence-default",
344
+ dealType: "traditional",
345
+ brandId: "",
346
+ clientType: "direct",
347
+ agencyId: "",
348
+ dspId: "dsp-activate-default",
349
+ seatName: "",
350
+ seatId: "",
351
+ adPlayVerificationEnabled: false,
352
+ adPlayVerificationProvider: "",
353
+ countries: ["MY"],
354
+ currency: "USD",
355
+ budget: "",
356
+ goalType: "",
357
+ goalValue: "",
358
+ minimumTargetThreshold: "",
359
+ hardStop: false,
360
+ },
361
+ });
362
+
363
+ // Watch fields for conditional logic
364
+ const watchedClientType = useWatch({ control: form.control, name: "clientType" });
365
+ const watchedDealType = useWatch({ control: form.control, name: "dealType" });
366
+ const watchedBrandId = useWatch({ control: form.control, name: "brandId" });
367
+ const watchedAgencyId = useWatch({ control: form.control, name: "agencyId" });
368
+ const watchedDspId = useWatch({ control: form.control, name: "dspId" });
369
+ const watchedSspId = useWatch({ control: form.control, name: "sspId" });
370
+ const watchedCountries = useWatch({ control: form.control, name: "countries" });
371
+ const watchedSeatName = useWatch({ control: form.control, name: "seatName" });
372
+ const watchedCurrency = useWatch({ control: form.control, name: "currency" });
373
+ const adPlayVerificationEnabled = useWatch({ control: form.control, name: "adPlayVerificationEnabled" });
374
+
375
+ // Check if deal type is programmatic
376
+ const isProgrammatic = watchedDealType !== "traditional";
377
+ const isPG = watchedDealType === "pg";
378
+
379
+ // Show DSP fields when programmatic AND (agency or brand selected)
380
+ const showDspFields = isProgrammatic && (watchedAgencyId || watchedBrandId);
381
+
382
+ // Get seat options for selected DSP
383
+ const seatOptions: ComboboxOption[] = useMemo(() => {
384
+ if (!watchedDspId) return [];
385
+ return DUMMY_DSP_SEATS
386
+ .filter((seat) => seat.dspId === watchedDspId)
387
+ .map((seat) => ({
388
+ value: seat.seatName,
389
+ label: seat.seatName,
390
+ }));
391
+ }, [watchedDspId]);
392
+
393
+ // Get seat ID for selected seat name
394
+ const selectedSeatId = useMemo(() => {
395
+ if (!watchedDspId || !watchedSeatName) return "";
396
+ const seat = DUMMY_DSP_SEATS.find(
397
+ (s) => s.dspId === watchedDspId && s.seatName === watchedSeatName
398
+ );
399
+ return seat?.seatId || "";
400
+ }, [watchedDspId, watchedSeatName]);
401
+
402
+ // Update seat ID when seat name changes
403
+ useEffect(() => {
404
+ form.setValue("seatId", selectedSeatId);
405
+ }, [selectedSeatId, form]);
406
+
407
+ // Update currency when countries change (use first country's currency)
408
+ useEffect(() => {
409
+ if (watchedCountries && watchedCountries.length > 0 && !isEditing) {
410
+ const firstCountry = watchedCountries[0];
411
+ const currency = COUNTRY_CURRENCY_MAP[firstCountry] || "USD";
412
+ form.setValue("currency", currency);
413
+ }
414
+ }, [watchedCountries, form, isEditing]);
415
+
416
+ // State for countries popover
417
+ const [countriesOpen, setCountriesOpen] = useState(false);
418
+
419
+ // State for create brand/agency drawer
420
+ const [brandDrawerOpen, setBrandDrawerOpen] = useState(false);
421
+ const [agencyDrawerOpen, setAgencyDrawerOpen] = useState(false);
422
+
423
+ // State for deal type selection
424
+ const [dealTypeCategory, setDealTypeCategory] = useState<"traditional" | "programmatic">("traditional");
425
+
426
+ // State for tracking visible section for Quick Tips
427
+ const [visibleSection, setVisibleSection] = useState<TipSection>("basic");
428
+
429
+ // Refs for section tracking
430
+ const basicSectionRef = useRef<HTMLDivElement>(null);
431
+ const sspSectionRef = useRef<HTMLDivElement>(null);
432
+ const brandSectionRef = useRef<HTMLDivElement>(null);
433
+ const budgetSectionRef = useRef<HTMLDivElement>(null);
434
+ const formContainerRef = useRef<HTMLDivElement>(null);
435
+
436
+ // IntersectionObserver for tracking visible sections
437
+ useEffect(() => {
438
+ const options = {
439
+ root: formContainerRef.current,
440
+ rootMargin: "-20% 0px -60% 0px",
441
+ threshold: 0,
442
+ };
443
+
444
+ const observer = new IntersectionObserver((entries) => {
445
+ entries.forEach((entry) => {
446
+ if (entry.isIntersecting) {
447
+ const sectionId = entry.target.getAttribute("data-section");
448
+ if (sectionId) {
449
+ setVisibleSection(sectionId as TipSection);
450
+ }
451
+ }
452
+ });
453
+ }, options);
454
+
455
+ const sections = [basicSectionRef.current, sspSectionRef.current, brandSectionRef.current, budgetSectionRef.current];
456
+ sections.forEach((section) => {
457
+ if (section) observer.observe(section);
458
+ });
459
+
460
+ return () => observer.disconnect();
461
+ }, []);
462
+
463
+ // Find the selected brand for insights panel
464
+ const selectedBrand = useMemo(() => {
465
+ if (!watchedBrandId || !brands) return null;
466
+ return brands.find((b) => b.id === watchedBrandId) || null;
467
+ }, [watchedBrandId, brands]);
468
+
469
+ // Dynamic currency options based on selected countries
470
+ const currencyOptions: ComboboxOption[] = useMemo(() => {
471
+ if (!watchedCountries || watchedCountries.length === 0) {
472
+ // Show all currencies when no countries selected
473
+ return CURRENCIES.map((c) => ({ value: c.value, label: c.label }));
474
+ }
475
+
476
+ // Get unique currencies from selected countries
477
+ const countryCurrencies = new Set<string>();
478
+ watchedCountries.forEach((countryCode) => {
479
+ const currency = COUNTRY_CURRENCY_MAP[countryCode];
480
+ if (currency) {
481
+ countryCurrencies.add(currency);
482
+ }
483
+ });
484
+
485
+ // Always include USD as a fallback option
486
+ countryCurrencies.add("USD");
487
+
488
+ // Filter CURRENCIES to only include those from selected countries
489
+ return CURRENCIES
490
+ .filter((c) => countryCurrencies.has(c.value))
491
+ .map((c) => ({ value: c.value, label: c.label }));
492
+ }, [watchedCountries]);
493
+
494
+ // Check if selected currency differs from any selected country's currency (for margin warning)
495
+ const currencyMismatchWarning = useMemo(() => {
496
+ if (!watchedCurrency || !watchedCountries || watchedCountries.length === 0) return null;
497
+
498
+ const mismatchedCountries: string[] = [];
499
+ watchedCountries.forEach((countryCode) => {
500
+ const countryCurrency = COUNTRY_CURRENCY_MAP[countryCode];
501
+ if (countryCurrency && countryCurrency !== watchedCurrency) {
502
+ const country = COUNTRIES.find((c) => c.code === countryCode);
503
+ if (country) {
504
+ mismatchedCountries.push(country.name);
505
+ }
506
+ }
507
+ });
508
+
509
+ if (mismatchedCountries.length === 0) return null;
510
+ return mismatchedCountries;
511
+ }, [watchedCurrency, watchedCountries]);
512
+
513
+ useEffect(() => {
514
+ if (existingDeal) {
515
+ const isProgDeal = existingDeal.dealType !== "traditional";
516
+ setDealTypeCategory(isProgDeal ? "programmatic" : "traditional");
517
+
518
+ form.reset({
519
+ name: existingDeal.name,
520
+ status: (existingDeal.status as "active" | "paused") ?? "active",
521
+ externalDealId: existingDeal.externalDealId ?? "",
522
+ billable: existingDeal.billable ?? true,
523
+ mediaType: (existingDeal.mediaType as "dooh" | "mobile" | "youtube" | "ctv") ?? "dooh",
524
+ sspId: existingDeal.sspId ?? "ssp-influence-default",
525
+ dealType: (existingDeal.dealType as any) ?? "traditional",
526
+ brandId: existingDeal.brandId ?? "",
527
+ clientType: (existingDeal.clientType as "direct" | "agency") ?? "direct",
528
+ agencyId: existingDeal.agencyId ?? "",
529
+ dspId: existingDeal.dspId ?? "dsp-activate-default",
530
+ seatName: existingDeal.seatName ?? "",
531
+ seatId: existingDeal.seatId ?? "",
532
+ adPlayVerificationEnabled: existingDeal.adPlayVerificationEnabled ?? false,
533
+ adPlayVerificationProvider: existingDeal.adPlayVerificationProvider ?? "",
534
+ countries: existingDeal.countries ?? ["MY"],
535
+ currency: existingDeal.currency ?? "USD",
536
+ budget: existingDeal.budget ? String(existingDeal.budget) : "",
537
+ goalType: existingDeal.goalType ?? "",
538
+ goalValue: existingDeal.goalValue ? String(existingDeal.goalValue) : "",
539
+ minimumTargetThreshold: existingDeal.minimumTargetThreshold ? String(existingDeal.minimumTargetThreshold) : "",
540
+ hardStop: existingDeal.hardStop ?? false,
541
+ });
542
+ }
543
+ }, [existingDeal, form]);
544
+
545
+ const createMutation = useMutation({
546
+ mutationFn: async (data: DealFormData) => {
547
+ return apiRequest("POST", "/api/deals", {
548
+ ...data,
549
+ brandId: data.brandId || undefined,
550
+ agencyId: data.agencyId || undefined,
551
+
552
+ dspId: data.dspId || undefined,
553
+ countries: data.countries.length > 0 ? data.countries : undefined,
554
+ budget: data.budget ? parseFloat(data.budget) : undefined,
555
+ goalValue: data.goalValue ? parseInt(data.goalValue) : undefined,
556
+ minimumTargetThreshold: data.minimumTargetThreshold ? parseInt(data.minimumTargetThreshold) : undefined,
557
+ adPlayVerificationProvider: data.adPlayVerificationEnabled ? data.adPlayVerificationProvider : undefined,
558
+ });
559
+ },
560
+ onSuccess: () => {
561
+ queryClient.invalidateQueries({ queryKey: ["/api/deals"] });
562
+ toast({
563
+ title: "Deal created",
564
+ description: "Your deal has been created successfully.",
565
+ });
566
+ setLocation("/deals");
567
+ },
568
+ onError: () => {
569
+ toast({
570
+ title: "Error",
571
+ description: "Failed to create deal. Please try again.",
572
+ variant: "destructive",
573
+ });
574
+ },
575
+ });
576
+
577
+ const updateMutation = useMutation({
578
+ mutationFn: async (data: DealFormData) => {
579
+ return apiRequest("PATCH", `/api/deals/${dealId}`, {
580
+ ...data,
581
+ brandId: data.brandId || undefined,
582
+ agencyId: data.agencyId || undefined,
583
+
584
+ dspId: data.dspId || undefined,
585
+ countries: data.countries.length > 0 ? data.countries : undefined,
586
+ budget: data.budget ? parseFloat(data.budget) : undefined,
587
+ goalValue: data.goalValue ? parseInt(data.goalValue) : undefined,
588
+ minimumTargetThreshold: data.minimumTargetThreshold ? parseInt(data.minimumTargetThreshold) : undefined,
589
+ adPlayVerificationProvider: data.adPlayVerificationEnabled ? data.adPlayVerificationProvider : undefined,
590
+ });
591
+ },
592
+ onSuccess: () => {
593
+ queryClient.invalidateQueries({ queryKey: ["/api/deals"] });
594
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId] });
595
+ toast({
596
+ title: "Deal updated",
597
+ description: "Your deal has been updated successfully.",
598
+ });
599
+ setLocation("/deals");
600
+ },
601
+ onError: () => {
602
+ toast({
603
+ title: "Error",
604
+ description: "Failed to update deal. Please try again.",
605
+ variant: "destructive",
606
+ });
607
+ },
608
+ });
609
+
610
+ const onSubmit = (data: DealFormData) => {
611
+ if (isEditing) {
612
+ updateMutation.mutate(data);
613
+ } else {
614
+ createMutation.mutate(data);
615
+ }
616
+ };
617
+
618
+ const handleCancel = () => {
619
+ setLocation("/deals");
620
+ };
621
+
622
+ const isSubmitting = createMutation.isPending || updateMutation.isPending;
623
+ const isLoadingData = isEditing && dealLoading;
624
+
625
+ if (isLoadingData) {
626
+ return (
627
+ <div className="flex flex-col gap-6 p-6">
628
+ <PageHeader
629
+ title={isEditing ? "Edit Deal" : "New Deal"}
630
+ description={isEditing ? "Update deal details" : "Create a new advertising deal"}
631
+ />
632
+ <Card>
633
+ <CardContent className="pt-6 space-y-6">
634
+ <Skeleton className="h-10 w-full" />
635
+ <Skeleton className="h-10 w-full" />
636
+ <Skeleton className="h-10 w-full" />
637
+ <Skeleton className="h-6 w-48" />
638
+ <Skeleton className="h-10 w-full" />
639
+ </CardContent>
640
+ </Card>
641
+ </div>
642
+ );
643
+ }
644
+
645
+ return (
646
+ <div className="flex flex-col min-h-0 h-full">
647
+ <div ref={formContainerRef} className="flex-1 min-h-0 overflow-auto">
648
+ <div className="flex flex-col gap-6 p-6 pb-6">
649
+ <PageHeader
650
+ title={isEditing ? "Edit Deal" : "New Deal"}
651
+ description={isEditing ? "Update deal details" : "Create a new advertising deal"}
652
+ actions={
653
+ <Button
654
+ variant="outline"
655
+ onClick={handleCancel}
656
+ data-testid="button-back-deals"
657
+ >
658
+ <ArrowLeft className="h-4 w-4 mr-2" />
659
+ Back to Deals
660
+ </Button>
661
+ }
662
+ />
663
+
664
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
665
+ <div className="lg:col-span-2">
666
+ <Card>
667
+ <CardContent className="pt-6">
668
+ <div className="flex items-center gap-3 mb-6">
669
+ <div className="p-2 rounded-lg bg-muted">
670
+ <User className="h-5 w-5 text-muted-foreground" />
671
+ </div>
672
+ <div>
673
+ <h3 className="font-semibold">Deal Details</h3>
674
+ <p className="text-sm text-muted-foreground">Set up your deal information</p>
675
+ </div>
676
+ </div>
677
+ <Form {...form}>
678
+ <form id="deal-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
679
+ {/* Section: Basic Info (Name, Status, External ID, Billable, Media Type) */}
680
+ <div ref={basicSectionRef} data-section="basic" className="space-y-6">
681
+ {/* Row 1: Name + Status */}
682
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
683
+ <FormField
684
+ control={form.control}
685
+ name="name"
686
+ render={({ field }) => (
687
+ <FormItem className="md:col-span-3">
688
+ <FormLabel>Deal Name <span className="text-destructive">*</span></FormLabel>
689
+ <FormControl>
690
+ <Input
691
+ placeholder="Enter deal name"
692
+ {...field}
693
+ data-testid="input-deal-name"
694
+ />
695
+ </FormControl>
696
+ <FormMessage />
697
+ </FormItem>
698
+ )}
699
+ />
700
+
701
+ <FormField
702
+ control={form.control}
703
+ name="status"
704
+ render={({ field }) => (
705
+ <FormItem>
706
+ <FormLabel>Status</FormLabel>
707
+ <FormControl>
708
+ <SearchableCombobox
709
+ options={statusOptions}
710
+ value={field.value}
711
+ onValueChange={field.onChange}
712
+ placeholder="Select..."
713
+ searchPlaceholder="Search..."
714
+ emptyMessage="No status found."
715
+ disabled={existingDeal?.reopened ?? false}
716
+ data-testid="combobox-status"
717
+ />
718
+ </FormControl>
719
+ {existingDeal?.reopened && (
720
+ <p className="text-xs text-muted-foreground">Status cannot be changed after reopening</p>
721
+ )}
722
+ <FormMessage />
723
+ </FormItem>
724
+ )}
725
+ />
726
+ </div>
727
+
728
+ {/* Row 2: External Deal ID + Billable */}
729
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
730
+ <FormField
731
+ control={form.control}
732
+ name="externalDealId"
733
+ render={({ field }) => (
734
+ <FormItem>
735
+ <FormLabel>External Deal ID</FormLabel>
736
+ <FormControl>
737
+ <Input
738
+ placeholder="Enter external deal ID"
739
+ {...field}
740
+ disabled={existingDeal?.reopened ?? false}
741
+ data-testid="input-external-deal-id"
742
+ />
743
+ </FormControl>
744
+ <FormMessage />
745
+ </FormItem>
746
+ )}
747
+ />
748
+
749
+ <FormField
750
+ control={form.control}
751
+ name="billable"
752
+ render={({ field }) => (
753
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
754
+ <div className="space-y-0.5">
755
+ <FormLabel className="text-base">Billable</FormLabel>
756
+ <FormDescription>
757
+ Mark this deal as billable
758
+ </FormDescription>
759
+ </div>
760
+ <FormControl>
761
+ <Switch
762
+ checked={field.value}
763
+ onCheckedChange={field.onChange}
764
+ disabled={existingDeal?.reopened ?? false}
765
+ data-testid="switch-billable"
766
+ />
767
+ </FormControl>
768
+ </FormItem>
769
+ )}
770
+ />
771
+ </div>
772
+
773
+ {/* Media Type */}
774
+ <FormField
775
+ control={form.control}
776
+ name="mediaType"
777
+ render={({ field }) => (
778
+ <FormItem>
779
+ <FormLabel>Media Type <span className="text-destructive">*</span></FormLabel>
780
+ <FormControl>
781
+ <SearchableCombobox
782
+ options={mediaTypeOptions}
783
+ value={field.value}
784
+ onValueChange={field.onChange}
785
+ placeholder="Select media type..."
786
+ searchPlaceholder="Search media types..."
787
+ emptyMessage="No media types found."
788
+ disabled={existingDeal?.reopened ?? false}
789
+ data-testid="combobox-media-type"
790
+ />
791
+ </FormControl>
792
+ <FormMessage />
793
+ </FormItem>
794
+ )}
795
+ />
796
+
797
+ </div>
798
+ {/* End of Basic Section */}
799
+
800
+ {/* Section: SSP and Deal Type */}
801
+ <div ref={sspSectionRef} data-section="ssp" className="space-y-6">
802
+ {/* SSP Partner */}
803
+ <FormField
804
+ control={form.control}
805
+ name="sspId"
806
+ render={({ field }) => (
807
+ <FormItem>
808
+ <FormLabel>SSP Partner <span className="text-destructive">*</span></FormLabel>
809
+ <FormControl>
810
+ <SearchableCombobox
811
+ options={sspOptions}
812
+ value={field.value}
813
+ onValueChange={field.onChange}
814
+ placeholder="Select SSP partner..."
815
+ searchPlaceholder="Search SSP partners..."
816
+ emptyMessage="No SSP partners found."
817
+ disabled={existingDeal?.reopened ?? false}
818
+ data-testid="combobox-ssp"
819
+ />
820
+ </FormControl>
821
+ <FormMessage />
822
+ </FormItem>
823
+ )}
824
+ />
825
+
826
+ {/* Deal Type */}
827
+ <div className="space-y-4">
828
+ <FormItem>
829
+ <FormLabel>Deal Type <span className="text-destructive">*</span></FormLabel>
830
+ {existingDeal?.reopened ? (
831
+ <div className="space-y-2">
832
+ <div className="flex items-center gap-2">
833
+ <Badge variant="outline" className="text-sm">
834
+ {dealTypeCategory === "traditional" ? "Traditional" : "Programmatic"}
835
+ {dealTypeCategory === "programmatic" && watchedDealType !== "traditional" && (
836
+ <span className="ml-1">({DEAL_TYPES.find(dt => dt.value === watchedDealType)?.label || watchedDealType})</span>
837
+ )}
838
+ </Badge>
839
+ <Badge variant="secondary" className="text-xs">From Deal</Badge>
840
+ </div>
841
+ <p className="text-sm text-muted-foreground">
842
+ Deal type cannot be changed after the deal has been reopened.
843
+ </p>
844
+ </div>
845
+ ) : (
846
+ <div className="flex gap-2">
847
+ <Button
848
+ type="button"
849
+ variant={dealTypeCategory === "traditional" ? "default" : "outline"}
850
+ onClick={() => {
851
+ setDealTypeCategory("traditional");
852
+ form.setValue("dealType", "traditional");
853
+ }}
854
+ data-testid="button-deal-type-traditional"
855
+ >
856
+ Traditional
857
+ </Button>
858
+ <Button
859
+ type="button"
860
+ variant={dealTypeCategory === "programmatic" ? "default" : "outline"}
861
+ onClick={() => {
862
+ setDealTypeCategory("programmatic");
863
+ form.setValue("dealType", "pg");
864
+ }}
865
+ data-testid="button-deal-type-programmatic"
866
+ >
867
+ Programmatic
868
+ </Button>
869
+ </div>
870
+ )}
871
+ </FormItem>
872
+
873
+ {dealTypeCategory === "programmatic" && !existingDeal?.reopened && (
874
+ <FormField
875
+ control={form.control}
876
+ name="dealType"
877
+ render={({ field }) => (
878
+ <FormItem>
879
+ <FormLabel>Programmatic Type</FormLabel>
880
+ <FormControl>
881
+ <SearchableCombobox
882
+ options={programmaticSubtypeOptions}
883
+ value={field.value}
884
+ onValueChange={field.onChange}
885
+ placeholder="Select programmatic type..."
886
+ searchPlaceholder="Search..."
887
+ emptyMessage="No types found."
888
+ data-testid="combobox-programmatic-type"
889
+ />
890
+ </FormControl>
891
+ <FormMessage />
892
+ </FormItem>
893
+ )}
894
+ />
895
+ )}
896
+ </div>
897
+ </div>
898
+ {/* End of SSP Section */}
899
+
900
+ {/* Section: Brand and Client */}
901
+ <div ref={brandSectionRef} data-section="brand" className="space-y-6">
902
+ {/* Brand */}
903
+ <FormField
904
+ control={form.control}
905
+ name="brandId"
906
+ render={({ field }) => (
907
+ <FormItem>
908
+ <FormLabel>
909
+ <div className="flex items-center gap-2">
910
+ <Tag className="h-4 w-4" />
911
+ Brand
912
+ </div>
913
+ </FormLabel>
914
+ <FormControl>
915
+ <SearchableCombobox
916
+ options={brandOptions}
917
+ value={field.value}
918
+ onValueChange={field.onChange}
919
+ placeholder="Select brand..."
920
+ searchPlaceholder="Search brands..."
921
+ emptyMessage="No brands found."
922
+ disabled={existingDeal?.reopened ?? false}
923
+ data-testid="combobox-brand"
924
+ footer={
925
+ <Button
926
+ type="button"
927
+ variant="ghost"
928
+ size="sm"
929
+ className="w-full justify-start"
930
+ data-testid="button-create-brand"
931
+ >
932
+ <Plus className="h-4 w-4 mr-2" />
933
+ Create New Brand
934
+ </Button>
935
+ }
936
+ onFooterClick={() => setBrandDrawerOpen(true)}
937
+ />
938
+ </FormControl>
939
+ <FormMessage />
940
+ </FormItem>
941
+ )}
942
+ />
943
+
944
+ {/* Client Type */}
945
+ <FormField
946
+ control={form.control}
947
+ name="clientType"
948
+ render={({ field }) => (
949
+ <FormItem>
950
+ <FormLabel>
951
+ <div className="flex items-center gap-2">
952
+ <User className="h-4 w-4" />
953
+ Client <span className="text-destructive">*</span>
954
+ </div>
955
+ </FormLabel>
956
+ <FormControl>
957
+ <SearchableCombobox
958
+ options={clientTypeOptions}
959
+ value={field.value}
960
+ onValueChange={(value) => {
961
+ field.onChange(value);
962
+ if (value === "direct") {
963
+ form.setValue("agencyId", "");
964
+ }
965
+ }}
966
+ placeholder="Select client type..."
967
+ searchPlaceholder="Search..."
968
+ emptyMessage="No options found."
969
+ disabled={existingDeal?.reopened ?? false}
970
+ data-testid="combobox-client-type"
971
+ />
972
+ </FormControl>
973
+ <FormMessage />
974
+ </FormItem>
975
+ )}
976
+ />
977
+
978
+ {watchedClientType === "agency" && (
979
+ <FormField
980
+ control={form.control}
981
+ name="agencyId"
982
+ render={({ field }) => (
983
+ <FormItem>
984
+ <FormLabel>
985
+ <div className="flex items-center gap-2">
986
+ <Building2 className="h-4 w-4" />
987
+ Agency
988
+ </div>
989
+ </FormLabel>
990
+ <FormControl>
991
+ <SearchableCombobox
992
+ options={agencyOptions}
993
+ value={field.value}
994
+ onValueChange={field.onChange}
995
+ placeholder="Select agency..."
996
+ searchPlaceholder="Search agencies..."
997
+ emptyMessage="No agencies found."
998
+ disabled={existingDeal?.reopened ?? false}
999
+ data-testid="combobox-agency"
1000
+ footer={
1001
+ <Button
1002
+ type="button"
1003
+ variant="ghost"
1004
+ size="sm"
1005
+ className="w-full justify-start"
1006
+ data-testid="button-create-agency"
1007
+ >
1008
+ <Plus className="mr-2 h-4 w-4" />
1009
+ Add Agency
1010
+ </Button>
1011
+ }
1012
+ onFooterClick={() => setAgencyDrawerOpen(true)}
1013
+ />
1014
+ </FormControl>
1015
+ <FormMessage />
1016
+ </FormItem>
1017
+ )}
1018
+ />
1019
+ )}
1020
+
1021
+ {/* DSP Configuration - Show when Programmatic AND (Agency or Brand selected) */}
1022
+ {showDspFields && (
1023
+ <div className="space-y-4 p-4 border rounded-lg bg-muted/50">
1024
+ <h4 className="font-medium text-sm">DSP Configuration</h4>
1025
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1026
+ <FormField
1027
+ control={form.control}
1028
+ name="dspId"
1029
+ render={({ field }) => (
1030
+ <FormItem>
1031
+ <FormLabel>DSP Partner</FormLabel>
1032
+ <FormControl>
1033
+ <SearchableCombobox
1034
+ options={dspOptions}
1035
+ value={field.value}
1036
+ onValueChange={(value) => {
1037
+ field.onChange(value);
1038
+ form.setValue("seatName", "");
1039
+ form.setValue("seatId", "");
1040
+ }}
1041
+ placeholder="Select DSP..."
1042
+ searchPlaceholder="Search DSPs..."
1043
+ emptyMessage="No DSPs found."
1044
+ disabled={existingDeal?.reopened ?? false}
1045
+ data-testid="combobox-dsp"
1046
+ />
1047
+ </FormControl>
1048
+ <FormMessage />
1049
+ </FormItem>
1050
+ )}
1051
+ />
1052
+
1053
+ <FormField
1054
+ control={form.control}
1055
+ name="seatName"
1056
+ render={({ field }) => (
1057
+ <FormItem>
1058
+ <FormLabel>Seat Name</FormLabel>
1059
+ <FormControl>
1060
+ <SearchableCombobox
1061
+ options={seatOptions}
1062
+ value={field.value}
1063
+ onValueChange={field.onChange}
1064
+ placeholder="Select seat..."
1065
+ searchPlaceholder="Search seats..."
1066
+ emptyMessage="No seats found."
1067
+ disabled={!watchedDspId || (existingDeal?.reopened ?? false)}
1068
+ data-testid="combobox-seat-name"
1069
+ />
1070
+ </FormControl>
1071
+ <FormMessage />
1072
+ </FormItem>
1073
+ )}
1074
+ />
1075
+
1076
+ <FormField
1077
+ control={form.control}
1078
+ name="seatId"
1079
+ render={({ field }) => (
1080
+ <FormItem>
1081
+ <FormLabel>Seat ID</FormLabel>
1082
+ <FormControl>
1083
+ <Input
1084
+ {...field}
1085
+ disabled
1086
+ placeholder="Auto-populated"
1087
+ data-testid="input-seat-id"
1088
+ />
1089
+ </FormControl>
1090
+ <FormDescription>
1091
+ Fetched from Admin Console
1092
+ </FormDescription>
1093
+ <FormMessage />
1094
+ </FormItem>
1095
+ )}
1096
+ />
1097
+ </div>
1098
+ </div>
1099
+ )}
1100
+
1101
+ </div>
1102
+ {/* End of Brand Section */}
1103
+
1104
+ {/* Section: Budget, Goals, and Verification */}
1105
+ <div ref={budgetSectionRef} data-section="budget" className="space-y-6">
1106
+ {/* AdPlay Verification */}
1107
+ <FormField
1108
+ control={form.control}
1109
+ name="adPlayVerificationEnabled"
1110
+ render={({ field }) => (
1111
+ <FormItem className="flex flex-row items-start space-x-3 space-y-0">
1112
+ <FormControl>
1113
+ <Checkbox
1114
+ checked={field.value}
1115
+ onCheckedChange={field.onChange}
1116
+ data-testid="checkbox-adplay-verification"
1117
+ />
1118
+ </FormControl>
1119
+ <div className="space-y-1 leading-none">
1120
+ <FormLabel>AdPlay Verification Report</FormLabel>
1121
+ </div>
1122
+ </FormItem>
1123
+ )}
1124
+ />
1125
+
1126
+ {adPlayVerificationEnabled && (
1127
+ <FormField
1128
+ control={form.control}
1129
+ name="adPlayVerificationProvider"
1130
+ render={({ field }) => (
1131
+ <FormItem>
1132
+ <FormLabel>Service Provider</FormLabel>
1133
+ <FormControl>
1134
+ <SearchableCombobox
1135
+ options={providerOptions}
1136
+ value={field.value}
1137
+ onValueChange={field.onChange}
1138
+ placeholder="Select service provider..."
1139
+ searchPlaceholder="Search providers..."
1140
+ emptyMessage="No providers found."
1141
+ data-testid="combobox-service-provider"
1142
+ />
1143
+ </FormControl>
1144
+ <FormMessage />
1145
+ </FormItem>
1146
+ )}
1147
+ />
1148
+ )}
1149
+
1150
+ {/* Countries */}
1151
+ <FormField
1152
+ control={form.control}
1153
+ name="countries"
1154
+ render={({ field }) => (
1155
+ <FormItem>
1156
+ <FormLabel>
1157
+ <div className="flex items-center gap-2">
1158
+ <Globe className="h-4 w-4" />
1159
+ Countries <span className="text-destructive">*</span>
1160
+ </div>
1161
+ </FormLabel>
1162
+ <FormDescription>
1163
+ Select countries for this deal. Line items can only target locations within these countries.
1164
+ </FormDescription>
1165
+ <Popover open={countriesOpen} onOpenChange={setCountriesOpen}>
1166
+ <PopoverTrigger asChild>
1167
+ <FormControl>
1168
+ <Button
1169
+ variant="outline"
1170
+ role="combobox"
1171
+ className="w-full justify-between h-auto min-h-10"
1172
+ disabled={existingDeal?.reopened ?? false}
1173
+ data-testid="button-countries-select"
1174
+ >
1175
+ {(field.value || []).length > 0 ? (
1176
+ <div className="flex flex-wrap gap-1">
1177
+ {(field.value || []).slice(0, 3).map((code) => {
1178
+ const country = COUNTRIES.find((c) => c.code === code);
1179
+ return country ? (
1180
+ <Badge
1181
+ key={code}
1182
+ variant="secondary"
1183
+ className="flex items-center gap-1"
1184
+ data-testid={`badge-country-${code}`}
1185
+ >
1186
+ <span>{country.flag}</span>
1187
+ <span>{country.name}</span>
1188
+ <button
1189
+ type="button"
1190
+ className="h-3 w-3 cursor-pointer hover:opacity-70"
1191
+ onClick={(e) => {
1192
+ e.stopPropagation();
1193
+ field.onChange((field.value || []).filter((v) => v !== code));
1194
+ }}
1195
+ data-testid={`button-remove-country-${code}`}
1196
+ >
1197
+ <X className="h-3 w-3" />
1198
+ </button>
1199
+ </Badge>
1200
+ ) : null;
1201
+ })}
1202
+ {(field.value || []).length > 3 && (
1203
+ <Badge variant="secondary" data-testid="badge-country-more">
1204
+ +{(field.value || []).length - 3} more
1205
+ </Badge>
1206
+ )}
1207
+ </div>
1208
+ ) : (
1209
+ <span className="text-muted-foreground">Select countries...</span>
1210
+ )}
1211
+ <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
1212
+ </Button>
1213
+ </FormControl>
1214
+ </PopoverTrigger>
1215
+ <PopoverContent className="w-[400px] p-0" align="start">
1216
+ <Command>
1217
+ <CommandInput placeholder="Search countries..." data-testid="input-search-countries" />
1218
+ <CommandList>
1219
+ <CommandEmpty>No country found.</CommandEmpty>
1220
+ <CommandGroup>
1221
+ {COUNTRIES.map((country) => {
1222
+ const values = field.value || [];
1223
+ const isSelected = values.includes(country.code);
1224
+ return (
1225
+ <CommandItem
1226
+ key={country.code}
1227
+ value={country.name}
1228
+ onSelect={() => {
1229
+ if (isSelected) {
1230
+ field.onChange(values.filter((v) => v !== country.code));
1231
+ } else {
1232
+ field.onChange([...values, country.code]);
1233
+ }
1234
+ }}
1235
+ data-testid={`option-country-${country.code}`}
1236
+ >
1237
+ <div className="flex items-center gap-2 w-full">
1238
+ <Checkbox checked={isSelected} className="pointer-events-none" />
1239
+ <span>{country.flag}</span>
1240
+ <span>{country.name}</span>
1241
+ </div>
1242
+ </CommandItem>
1243
+ );
1244
+ })}
1245
+ </CommandGroup>
1246
+ </CommandList>
1247
+ </Command>
1248
+ </PopoverContent>
1249
+ </Popover>
1250
+ {(field.value || []).length > 0 && (
1251
+ <Button
1252
+ type="button"
1253
+ variant="ghost"
1254
+ size="sm"
1255
+ onClick={() => field.onChange([])}
1256
+ className="mt-1"
1257
+ data-testid="button-clear-countries"
1258
+ >
1259
+ <X className="h-3 w-3 mr-1" />
1260
+ Clear all
1261
+ </Button>
1262
+ )}
1263
+ <FormMessage />
1264
+ </FormItem>
1265
+ )}
1266
+ />
1267
+
1268
+ {/* Currency + Budget */}
1269
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1270
+ <FormField
1271
+ control={form.control}
1272
+ name="currency"
1273
+ render={({ field }) => (
1274
+ <FormItem>
1275
+ <FormLabel>
1276
+ <div className="flex items-center gap-2">
1277
+ <DollarSign className="h-4 w-4" />
1278
+ Currency <span className="text-destructive">*</span>
1279
+ </div>
1280
+ </FormLabel>
1281
+ <FormControl>
1282
+ <SearchableCombobox
1283
+ options={currencyOptions}
1284
+ value={field.value}
1285
+ onValueChange={field.onChange}
1286
+ placeholder="Select currency..."
1287
+ searchPlaceholder="Search currencies..."
1288
+ emptyMessage="No currencies found."
1289
+ disabled={existingDeal?.reopened ?? false}
1290
+ data-testid="combobox-currency"
1291
+ />
1292
+ </FormControl>
1293
+ <FormMessage />
1294
+ {currencyMismatchWarning && currencyMismatchWarning.length > 0 && (
1295
+ <div className="mt-2 p-2 rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800" data-testid="currency-margin-warning">
1296
+ <div className="flex items-start gap-2">
1297
+ <AlertTriangle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
1298
+ <div className="text-xs">
1299
+ <span className="font-medium text-amber-800 dark:text-amber-300">
1300
+ 3% margin applied
1301
+ </span>
1302
+ <span className="text-amber-700 dark:text-amber-400">
1303
+ {" "}for {currencyMismatchWarning.join(", ")} floor rates
1304
+ </span>
1305
+ </div>
1306
+ </div>
1307
+ </div>
1308
+ )}
1309
+ </FormItem>
1310
+ )}
1311
+ />
1312
+
1313
+ <FormField
1314
+ control={form.control}
1315
+ name="budget"
1316
+ render={({ field }) => (
1317
+ <FormItem>
1318
+ <FormLabel>Budget</FormLabel>
1319
+ <FormControl>
1320
+ <Input
1321
+ type="number"
1322
+ placeholder="Enter budget amount"
1323
+ {...field}
1324
+ disabled={existingDeal?.reopened ?? false}
1325
+ data-testid="input-budget"
1326
+ />
1327
+ </FormControl>
1328
+ <FormMessage />
1329
+ </FormItem>
1330
+ )}
1331
+ />
1332
+ </div>
1333
+
1334
+ {/* Goal */}
1335
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1336
+ <FormField
1337
+ control={form.control}
1338
+ name="goalType"
1339
+ render={({ field }) => (
1340
+ <FormItem>
1341
+ <FormLabel>
1342
+ <div className="flex items-center gap-2">
1343
+ <Target className="h-4 w-4" />
1344
+ Goal Type
1345
+ </div>
1346
+ </FormLabel>
1347
+ <FormControl>
1348
+ <SearchableCombobox
1349
+ options={goalTypeOptions}
1350
+ value={field.value}
1351
+ onValueChange={field.onChange}
1352
+ placeholder="Select goal type..."
1353
+ searchPlaceholder="Search goal types..."
1354
+ emptyMessage="No goal types found."
1355
+ disabled={existingDeal?.reopened ?? false}
1356
+ data-testid="combobox-goal-type"
1357
+ />
1358
+ </FormControl>
1359
+ <FormMessage />
1360
+ </FormItem>
1361
+ )}
1362
+ />
1363
+
1364
+ <FormField
1365
+ control={form.control}
1366
+ name="goalValue"
1367
+ render={({ field }) => (
1368
+ <FormItem>
1369
+ <FormLabel>Goal Value</FormLabel>
1370
+ <FormControl>
1371
+ <Input
1372
+ type="number"
1373
+ placeholder="Enter goal value"
1374
+ {...field}
1375
+ disabled={existingDeal?.reopened ?? false}
1376
+ data-testid="input-goal-value"
1377
+ />
1378
+ </FormControl>
1379
+ <FormMessage />
1380
+ </FormItem>
1381
+ )}
1382
+ />
1383
+ </div>
1384
+
1385
+ {/* PG-only fields: Minimum Target Threshold + Hard Stop */}
1386
+ {isPG && (
1387
+ <div className="space-y-4 p-4 border rounded-lg bg-muted/50">
1388
+ <h4 className="font-medium text-sm">Programmatic Guaranteed Settings</h4>
1389
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1390
+ <FormField
1391
+ control={form.control}
1392
+ name="minimumTargetThreshold"
1393
+ render={({ field }) => (
1394
+ <FormItem>
1395
+ <FormLabel>Minimum Target Threshold <span className="text-destructive">*</span></FormLabel>
1396
+ <FormControl>
1397
+ <Input
1398
+ type="number"
1399
+ placeholder="Enter threshold"
1400
+ {...field}
1401
+ disabled={existingDeal?.reopened ?? false}
1402
+ data-testid="input-minimum-target-threshold"
1403
+ />
1404
+ </FormControl>
1405
+ <FormDescription>
1406
+ Minimum impressions guaranteed (required for PG)
1407
+ </FormDescription>
1408
+ <FormMessage />
1409
+ </FormItem>
1410
+ )}
1411
+ />
1412
+
1413
+ <FormField
1414
+ control={form.control}
1415
+ name="hardStop"
1416
+ render={({ field }) => (
1417
+ <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4 bg-background">
1418
+ <div className="space-y-0.5">
1419
+ <FormLabel className="text-base">Hard Stop</FormLabel>
1420
+ <FormDescription>
1421
+ Stop delivery when goal is reached
1422
+ </FormDescription>
1423
+ </div>
1424
+ <FormControl>
1425
+ <Switch
1426
+ checked={field.value}
1427
+ onCheckedChange={field.onChange}
1428
+ disabled={existingDeal?.reopened ?? false}
1429
+ data-testid="switch-hard-stop"
1430
+ />
1431
+ </FormControl>
1432
+ </FormItem>
1433
+ )}
1434
+ />
1435
+ </div>
1436
+ </div>
1437
+ )}
1438
+ </div>
1439
+ {/* End of Budget Section */}
1440
+ </form>
1441
+ </Form>
1442
+ </CardContent>
1443
+ </Card>
1444
+ </div>
1445
+
1446
+ <div className="lg:col-span-1">
1447
+ <div className="sticky top-6 space-y-4">
1448
+ {/* Show Brand Info card when brand is selected and in brand section */}
1449
+ {selectedBrand && visibleSection === "brand" ? (
1450
+ <Card data-testid="card-brand-info">
1451
+ <CardHeader className="pb-3">
1452
+ <CardTitle className="text-sm font-medium flex items-center gap-2" data-testid="title-brand-info">
1453
+ <Tag className="h-4 w-4 text-primary" />
1454
+ Brand Information
1455
+ </CardTitle>
1456
+ </CardHeader>
1457
+ <CardContent className="pt-0 space-y-4">
1458
+ {/* Brand Logo Placeholder */}
1459
+ <div className="flex items-center gap-3">
1460
+ <div className="w-16 h-16 rounded-lg bg-muted flex items-center justify-center border">
1461
+ <span className="text-2xl font-bold text-muted-foreground">
1462
+ {selectedBrand.name.charAt(0).toUpperCase()}
1463
+ </span>
1464
+ </div>
1465
+ <div>
1466
+ <h4 className="font-semibold" data-testid="text-brand-name">{selectedBrand.name}</h4>
1467
+ <p className="text-sm text-muted-foreground" data-testid="text-brand-status">
1468
+ {selectedBrand.status === "active" ? "Active Brand" : "Inactive Brand"}
1469
+ </p>
1470
+ </div>
1471
+ </div>
1472
+
1473
+ {/* IAB Category */}
1474
+ {selectedBrand.iabCategory && (
1475
+ <div className="space-y-1">
1476
+ <label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
1477
+ IAB Category
1478
+ </label>
1479
+ <div className="flex items-center gap-2">
1480
+ <Badge variant="secondary" data-testid="badge-iab-category">
1481
+ {selectedBrand.iabCategory}
1482
+ </Badge>
1483
+ </div>
1484
+ <p className="text-xs text-muted-foreground mt-1" data-testid="text-iab-code">
1485
+ IAB Code: {selectedBrand.iabCategory.split(" ").map(word => word.charAt(0)).join("").toUpperCase()}
1486
+ </p>
1487
+ </div>
1488
+ )}
1489
+
1490
+ {/* Brand Insights Link */}
1491
+ <div className="pt-2 border-t">
1492
+ <p className="text-xs text-muted-foreground">
1493
+ Brand selection enables personalized inventory recommendations in line items.
1494
+ </p>
1495
+ </div>
1496
+ </CardContent>
1497
+ </Card>
1498
+ ) : (
1499
+ /* Dynamic Quick Tips based on visible section */
1500
+ <Card data-testid="card-quick-tips">
1501
+ <CardHeader className="pb-3">
1502
+ <CardTitle className="text-sm font-medium flex items-center gap-2" data-testid="title-quick-tips">
1503
+ <Lightbulb className="h-4 w-4 text-amber-500" />
1504
+ {SECTION_TIPS[visibleSection].title}
1505
+ </CardTitle>
1506
+ </CardHeader>
1507
+ <CardContent className="pt-0">
1508
+ <ul className="space-y-2 text-sm text-muted-foreground">
1509
+ {SECTION_TIPS[visibleSection].tips.map((tip, index) => (
1510
+ <li key={index} className="flex items-start gap-2" data-testid={`text-tip-${index}`}>
1511
+ <span className="text-amber-500 mt-1 flex-shrink-0">•</span>
1512
+ <span>{tip}</span>
1513
+ </li>
1514
+ ))}
1515
+ </ul>
1516
+ </CardContent>
1517
+ </Card>
1518
+ )}
1519
+
1520
+ {/* Market Insights when countries are selected */}
1521
+ {watchedCountries && watchedCountries.length > 0 && (
1522
+ <MarketInsightsPanel selectedCountries={watchedCountries} />
1523
+ )}
1524
+ </div>
1525
+ </div>
1526
+ </div>
1527
+ </div>
1528
+ </div>
1529
+
1530
+ <div className="shrink-0 border-t bg-background p-4">
1531
+ <div className="flex justify-end gap-4">
1532
+ <Button
1533
+ type="button"
1534
+ variant="outline"
1535
+ onClick={handleCancel}
1536
+ disabled={isSubmitting}
1537
+ data-testid="button-cancel"
1538
+ >
1539
+ Cancel
1540
+ </Button>
1541
+ <Button
1542
+ type="submit"
1543
+ form="deal-form"
1544
+ disabled={isSubmitting}
1545
+ data-testid="button-submit"
1546
+ >
1547
+ {isSubmitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
1548
+ {isEditing ? "Update Deal" : "Create Deal"}
1549
+ </Button>
1550
+ </div>
1551
+ </div>
1552
+
1553
+ <CreateBrandDrawer
1554
+ open={brandDrawerOpen}
1555
+ onOpenChange={setBrandDrawerOpen}
1556
+ onBrandCreated={(brandId) => {
1557
+ form.setValue("brandId", brandId);
1558
+ }}
1559
+ />
1560
+
1561
+ <CreateAgencyDrawer
1562
+ open={agencyDrawerOpen}
1563
+ onOpenChange={setAgencyDrawerOpen}
1564
+ onAgencyCreated={(agencyId) => {
1565
+ form.setValue("agencyId", agencyId);
1566
+ }}
1567
+ />
1568
+ </div>
1569
+ );
1570
+ }