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,2093 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { useLocation, useRoute } from "wouter";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import { useTranslation } from "@/lib/i18n";
8
+ import { Button } from "@moving-walls/design-system";
9
+ import {
10
+ Form,
11
+ FormItem,
12
+ FormLabel,
13
+ FormControl,
14
+ FormMessage,
15
+ } from "@moving-walls/design-system";
16
+ import { Input } from "@moving-walls/design-system";
17
+ import {
18
+ SelectRoot,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "@moving-walls/design-system";
24
+ import { Card, CardContent, CardHeader, CardTitle, Checkbox, Textarea } from "@moving-walls/design-system";
25
+ import {
26
+ FileText,
27
+ Info,
28
+ Lightbulb,
29
+ Search,
30
+ Plus,
31
+ X,
32
+ ArrowLeft,
33
+ Building2,
34
+ Globe,
35
+ ExternalLink,
36
+ RefreshCw,
37
+ ChevronDown,
38
+ Tag,
39
+ } from "lucide-react";
40
+ import { Badge, cn } from "@moving-walls/design-system";
41
+ import { MarketInsightsPanel } from "@/components/deals/market-insights-panel";
42
+ import { useToast } from "@/hooks/use-toast";
43
+ import { useAuth } from "@/contexts/auth-context";
44
+ import {
45
+ InfluenceDealsAPI,
46
+ influenceDealsRequest,
47
+ formatAPIErrorForToast,
48
+ type Deal,
49
+ } from "@/lib/influence-deals-api";
50
+ import { fetchCompanyBrands, type CompanyBrand } from "@/lib/oauth-service";
51
+ import {
52
+ fetchDemandSideCompanies,
53
+ type DemandSideCompany,
54
+ } from "@/lib/company-api";
55
+ import { API_CONFIG } from "@/lib/api-config";
56
+ import { usePageTitle } from "@/hooks/use-page-title";
57
+ import { getDspDropdownList, listDspBuyers, type DspBuyer, type DspDropdownItem } from "@/lib/dsp-buyer-api";
58
+
59
+ const COUNTRIES = [
60
+ { value: "Malaysia", code: "MY", currency: "MYR", region: "APAC" },
61
+ { value: "Singapore", code: "SG", currency: "SGD", region: "APAC" },
62
+ { value: "Japan", code: "JP", currency: "JPY", region: "APAC" },
63
+ { value: "Indonesia", code: "ID", currency: "IDR", region: "APAC" },
64
+ { value: "Thailand", code: "TH", currency: "THB", region: "APAC" },
65
+ { value: "Philippines", code: "PH", currency: "PHP", region: "APAC" },
66
+ { value: "Australia", code: "AU", currency: "AUD", region: "APAC" },
67
+ { value: "United States", code: "US", currency: "USD", region: "NA" },
68
+ { value: "United Kingdom", code: "GB", currency: "GBP", region: "EMEA" },
69
+ { value: "Germany", code: "DE", currency: "EUR", region: "EMEA" },
70
+ { value: "France", code: "FR", currency: "EUR", region: "EMEA" },
71
+ { value: "India", code: "IN", currency: "INR", region: "APAC" },
72
+ { value: "China", code: "CN", currency: "CNY", region: "APAC" },
73
+ { value: "South Korea", code: "KR", currency: "KRW", region: "APAC" },
74
+ { value: "UAE", code: "AE", currency: "AED", region: "MENA" },
75
+ ];
76
+
77
+ const CURRENCIES = [
78
+ { value: "USD", label: "USD - US Dollar" },
79
+ { value: "MYR", label: "MYR - Malaysian Ringgit" },
80
+ { value: "SGD", label: "SGD - Singapore Dollar" },
81
+ { value: "JPY", label: "JPY - Japanese Yen" },
82
+ { value: "GBP", label: "GBP - British Pound" },
83
+ { value: "EUR", label: "EUR - Euro" },
84
+ { value: "AUD", label: "AUD - Australian Dollar" },
85
+ { value: "IDR", label: "IDR - Indonesian Rupiah" },
86
+ { value: "THB", label: "THB - Thai Baht" },
87
+ { value: "PHP", label: "PHP - Philippine Peso" },
88
+ { value: "INR", label: "INR - Indian Rupee" },
89
+ { value: "CNY", label: "CNY - Chinese Yuan" },
90
+ { value: "KRW", label: "KRW - Korean Won" },
91
+ { value: "AED", label: "AED - UAE Dirham" },
92
+ ];
93
+
94
+ const CLIENT_TYPE_OPTIONS = [
95
+ { value: "DIRECT_ADVERTISER", label: "Direct Advertiser" },
96
+ { value: "AGENCY", label: "Agency" },
97
+ ];
98
+
99
+ const DEAL_TYPE_OPTIONS = [
100
+ { value: "GUARANTEED", label: "Programmatic Guaranteed" },
101
+ { value: "PREFERRED_DEAL", label: "Preferred Deal" },
102
+ { value: "PRIVATE_AUCTION", label: "Private Auction" },
103
+ { value: "EVERGREEN_PMP", label: "Evergreen PMP" },
104
+ ];
105
+
106
+ const AUCTION_TYPE_OPTIONS = [
107
+ { value: "1", label: "First Price" },
108
+ { value: "2", label: "Second Price" },
109
+ ];
110
+
111
+ const IMPRESSION_MULTIPLIER_OPTIONS = [
112
+ { value: "MAD", label: "Measure" },
113
+ { value: "CUSTOM", label: "Custom Impression" },
114
+ ];
115
+
116
+ const TRANSACTION_TYPE_OPTIONS = [
117
+ { value: "SPOT", label: "Spot" },
118
+ { value: "AUDIENCE", label: "Audience" },
119
+ ];
120
+
121
+ const COST_TYPE_OPTIONS = [
122
+ { value: "CPD", label: "CPD - Cost Per Day" },
123
+ { value: "CPM", label: "CPM - Cost Per Mille" },
124
+ { value: "CPS", label: "CPS - Cost Per Spot" },
125
+ ];
126
+
127
+ const GOAL_TYPE_OPTIONS = [
128
+ { value: "IMPRESSIONS", label: "Impressions" },
129
+ { value: "REACH", label: "Reach" },
130
+ { value: "SHARE_OF_VOICE", label: "Share of Voice" },
131
+ ];
132
+
133
+
134
+ const STATUS_OPTIONS = [
135
+ { value: "ACTIVE", label: "Active" },
136
+ { value: "PAUSED", label: "Paused" },
137
+ { value: "DRAFT", label: "Draft" },
138
+ ];
139
+
140
+ const MEDIA_TYPE_OPTIONS = [
141
+ { value: "DOOH", label: "DOOH - Digital Out-of-Home" },
142
+ { value: "MOBILE", label: "Mobile" },
143
+ { value: "DISPLAY", label: "Display" },
144
+ { value: "VIDEO", label: "Video" },
145
+ ];
146
+
147
+ const SSP_PARTNER_OPTIONS = [
148
+ { value: "INFLUENCE_SSP", label: "Influence SSP" },
149
+ { value: "HIVESTACK", label: "Hivestack" },
150
+ { value: "VISTAR", label: "Vistar" },
151
+ { value: "PLACE_EXCHANGE", label: "Place Exchange" },
152
+ { value: "VIOOH", label: "VIOOH" },
153
+ { value: "BROADSIGN", label: "Broadsign" },
154
+ ];
155
+
156
+ type TipSection = "basic" | "supply" | "brand" | "budget";
157
+
158
+ interface QuickTip {
159
+ title: string;
160
+ tips: string[];
161
+ }
162
+
163
+ const SECTION_TIPS: Record<TipSection, QuickTip> = {
164
+ basic: {
165
+ title: "Deal Basics",
166
+ tips: [
167
+ "Deal Name should be descriptive and unique to easily identify it in lists",
168
+ "Status controls whether the deal is Active (running) or Paused (stopped)",
169
+ "Media Type defines the channel - DOOH (Digital Out-of-Home) is for digital screens",
170
+ ],
171
+ },
172
+ supply: {
173
+ title: "Supply Configuration",
174
+ tips: [
175
+ "SSP Partner (Supply-Side Platform) provides access to inventory networks",
176
+ "Deal Type determines pricing model - Traditional or Programmatic",
177
+ "Programmatic deals enable automated real-time bidding with DSP partners",
178
+ "Media Owner is selected at Line Item level for more granular control",
179
+ "Client Type indicates if this is a direct client or through an agency",
180
+ ],
181
+ },
182
+ brand: {
183
+ title: "Brand & Verification",
184
+ tips: [
185
+ "Selecting a brand enables brand-specific insights and recommendations",
186
+ "AdPlay Verification ensures ads are actually played on screens",
187
+ "Countries selected here limit geography targeting in line items",
188
+ "Line items can only target cities/areas within the deal's countries",
189
+ ],
190
+ },
191
+ budget: {
192
+ title: "Budget & Goals",
193
+ tips: [
194
+ "Currency determines the monetary unit for all budget and pricing",
195
+ "Currency margin (3%) applies when inventory uses different currencies",
196
+ "Goal Type sets what you're optimizing for - Impressions, Reach, etc.",
197
+ "Budget and Goal values help with better inventory recommendations in Line Items",
198
+ "Setting budget enables spend tracking and pacing recommendations",
199
+ ],
200
+ },
201
+ };
202
+
203
+ const directModeSchemaBase = z.object({
204
+ mode: z.literal("DIRECT"),
205
+ name: z
206
+ .string()
207
+ .min(3, "Deal name must be at least 3 characters")
208
+ .max(200, "Deal name must not exceed 200 characters"),
209
+ brand: z.string().min(1, "Brand is required"),
210
+ clientType: z.enum(["DIRECT_ADVERTISER", "AGENCY"]),
211
+ agencyId: z.string().optional(),
212
+ approvalEmails: z.array(z.string().email()).optional(),
213
+ country: z.string().min(1, "Country is required"),
214
+ currency: z.string().min(1, "Currency is required"),
215
+ totalBudget: z.number().min(0, "Budget must be positive").optional(),
216
+ dailyBudgetCap: z
217
+ .number()
218
+ .min(0, "Daily budget cap must be positive")
219
+ .optional(),
220
+ goalType: z.enum(["IMPRESSIONS", "REACH", "SHARE_OF_VOICE"]).optional(),
221
+ goalValue: z.number().min(0, "Goal value must be positive").optional(),
222
+ });
223
+
224
+ const programmaticModeSchema = z.object({
225
+ mode: z.literal("PROGRAMMATIC"),
226
+ name: z
227
+ .string()
228
+ .min(3, "Deal name must be at least 3 characters")
229
+ .max(200, "Deal name must not exceed 200 characters"),
230
+ dealType: z.enum([
231
+ "GUARANTEED",
232
+ "PREFERRED_DEAL",
233
+ "PRIVATE_AUCTION",
234
+ "EVERGREEN_PMP",
235
+ ]),
236
+ buyers: z
237
+ .array(
238
+ z.object({
239
+ dsp: z.string().min(1, "DSP is required"),
240
+ seatId: z.string().min(1, "Seat ID is required"),
241
+ buyerName: z.string().min(1, "Buyer Name is required"),
242
+ }),
243
+ )
244
+ .min(1, "At least one buyer is required"),
245
+ auctionType: z.number().min(1).max(3),
246
+ transactionType: z.enum(["SPOT", "AUDIENCE"]),
247
+ impMultiplierType: z.enum(["MAD", "CUSTOM"]).optional(),
248
+ description: z.string().optional(),
249
+ currency: z.string().optional(),
250
+ });
251
+
252
+ const formSchema = z.discriminatedUnion("mode", [
253
+ directModeSchemaBase,
254
+ programmaticModeSchema,
255
+ ]);
256
+
257
+ type FormData = z.infer<typeof formSchema>;
258
+
259
+ export default function CreateDealPage() {
260
+ const { t } = useTranslation();
261
+ const [, setLocation] = useLocation();
262
+ const [matchEdit, paramsEdit] = useRoute("/deals/edit/:dealId");
263
+ const dealId = matchEdit ? paramsEdit?.dealId : null;
264
+ const isEditMode = !!dealId;
265
+ usePageTitle(isEditMode ? "Edit Deal" : "Create Deal");
266
+
267
+ const { toast } = useToast();
268
+ const { user } = useAuth();
269
+ const queryClient = useQueryClient();
270
+ const [mode, setMode] = useState<"DIRECT" | "PROGRAMMATIC">("DIRECT");
271
+ const [brandSearchOpen, setBrandSearchOpen] = useState(false);
272
+ const [brandSearch, setBrandSearch] = useState("");
273
+ const [debouncedBrandSearch, setDebouncedBrandSearch] = useState("");
274
+ const [brandPage, setBrandPage] = useState(1);
275
+ const [accumulatedBrands, setAccumulatedBrands] = useState<CompanyBrand[]>(
276
+ [],
277
+ );
278
+ const [emailInput, setEmailInput] = useState("");
279
+ const [approvalEmails, setApprovalEmails] = useState<string[]>([]);
280
+ const [sellerEmail, setSellerEmail] = useState("");
281
+ const [sellerEmailInitialized, setSellerEmailInitialized] = useState(false);
282
+ const [selectedAgency, setSelectedAgency] = useState<string>("");
283
+ const [agencySearchOpen, setAgencySearchOpen] = useState(false);
284
+ const [agencySearch, setAgencySearch] = useState("");
285
+
286
+ // UI-only fields (not sent in API payload)
287
+ const [dealStatus, setDealStatus] = useState<string>("DRAFT");
288
+ const [mediaType, setMediaType] = useState<string>("DOOH");
289
+ const [sspPartner, setSspPartner] = useState<string>("INFLUENCE_SSP");
290
+ const [isNonBillable, setIsNonBillable] = useState<boolean>(false);
291
+ const [isHardStop, setIsHardStop] = useState<boolean>(false);
292
+ const [buyers, setBuyers] = useState<
293
+ { dsp: string; seatId: string; buyerName: string; buyerId?: string }[]
294
+ >([{ dsp: "", seatId: "", buyerName: "" }]);
295
+ const brandDropdownRef = useRef<HTMLDivElement>(null);
296
+ const agencyDropdownRef = useRef<HTMLDivElement>(null);
297
+ const [isDataLoaded, setIsDataLoaded] = useState(false);
298
+ const [visibleSection, setVisibleSection] = useState<TipSection>("basic");
299
+ const [isDealTypeLocked, setIsDealTypeLocked] = useState(false);
300
+ const [isDspLocked, setIsDspLocked] = useState(false);
301
+ const [selectedTransactionType, setSelectedTransactionType] = useState("");
302
+ const [impMultiplierType, setImpMultiplierType] = useState<string | null>(null);
303
+ const [sellerName, setSellerName] = useState("");
304
+ const [sellerPhone, setSellerPhone] = useState("");
305
+ const [description, setDescription] = useState("");
306
+ const formContainerRef = useRef<HTMLDivElement>(null);
307
+ const basicSectionRef = useRef<HTMLDivElement>(null);
308
+ const supplySectionRef = useRef<HTMLDivElement>(null);
309
+ const brandSectionRef = useRef<HTMLDivElement>(null);
310
+ const budgetSectionRef = useRef<HTMLDivElement>(null);
311
+
312
+ // Fetch existing deal data for edit mode
313
+ const { data: existingDeal, isLoading: isLoadingDeal } = useQuery({
314
+ queryKey: ["deal", dealId],
315
+ queryFn: async () => {
316
+ if (!dealId) return null;
317
+ const response = await influenceDealsRequest(
318
+ InfluenceDealsAPI.deals.get(dealId),
319
+ );
320
+ return response as Deal;
321
+ },
322
+ enabled: !!dealId,
323
+ });
324
+
325
+ const {
326
+ register,
327
+ handleSubmit,
328
+ setValue,
329
+ watch,
330
+ formState: { errors },
331
+ reset,
332
+ } = useForm<FormData>({
333
+ resolver: zodResolver(formSchema),
334
+ defaultValues: {
335
+ mode: "DIRECT",
336
+ name: "",
337
+ brand: "",
338
+ clientType: "DIRECT_ADVERTISER",
339
+ country: "Japan",
340
+ countries: [],
341
+ currency: "JPY",
342
+ totalBudget: undefined,
343
+ dailyBudgetCap: undefined,
344
+ goalType: undefined,
345
+ goalValue: undefined,
346
+ } as any,
347
+ });
348
+
349
+ const brand = watch("brand");
350
+ const dealType = watch("dealType" as any);
351
+ const clientType = watch("clientType" as any);
352
+
353
+ const { data: agenciesData, isLoading: isAgenciesLoading } = useQuery({
354
+ queryKey: ["agencies", clientType],
355
+ queryFn: fetchDemandSideCompanies,
356
+ staleTime: 0,
357
+ enabled: clientType === "AGENCY",
358
+ });
359
+
360
+ const agencies: DemandSideCompany[] = agenciesData || [];
361
+
362
+ const { data: dspOptions = [] } = useQuery({
363
+ queryKey: ["dsp-dropdown"],
364
+ queryFn: getDspDropdownList,
365
+ });
366
+
367
+ const [activeDspName, setActiveDspName] = useState("");
368
+ const [activeDspId, setActiveDspId] = useState("");
369
+
370
+ const { data: buyersForDsp = [] } = useQuery({
371
+ queryKey: ["dsp-buyers", activeDspName],
372
+ queryFn: () => listDspBuyers({ size: 200, sort: "seatName,asc", name: activeDspName }),
373
+ enabled: !!activeDspName,
374
+ staleTime: 0,
375
+ });
376
+
377
+ const handleAgencyChange = (agencyId: string) => {
378
+ setSelectedAgency(agencyId);
379
+ setValue("agencyId" as any, agencyId);
380
+ };
381
+
382
+ const getSelectedAgency = () => {
383
+ return agencies.find((a) => a.id === selectedAgency);
384
+ };
385
+
386
+ const handleCountryChange = (countryName: string) => {
387
+ const countryData = COUNTRIES.find((c) => c.value === countryName);
388
+ if (countryData) {
389
+ setValue("country" as any, countryName);
390
+ setValue("currency" as any, countryData.currency);
391
+ }
392
+ };
393
+
394
+ const normalizeCountry = (countryValue: string): string => {
395
+ if (!countryValue) return "Japan";
396
+ const byName = COUNTRIES.find((c) => c.value === countryValue);
397
+ if (byName) return byName.value;
398
+ const byCode = COUNTRIES.find(
399
+ (c) => c.code === countryValue || c.code === countryValue.toUpperCase(),
400
+ );
401
+ if (byCode) return byCode.value;
402
+ return "Japan";
403
+ };
404
+
405
+ useEffect(() => {
406
+ if (mode === "PROGRAMMATIC" && dealType && dealType !== "PRIVATE_AUCTION") {
407
+ setValue("auctionType" as any, 3);
408
+ }
409
+ }, [mode, dealType, setValue]);
410
+
411
+ // Auto-populate country from company's primary address when user data is available
412
+ useEffect(() => {
413
+ if (user?.company_country && !dealId) {
414
+ const normalizedCountry = normalizeCountry(user.company_country);
415
+ const countryData = COUNTRIES.find((c) => c.value === normalizedCountry);
416
+ if (countryData) {
417
+ setValue("country" as any, normalizedCountry);
418
+ setValue("currency" as any, countryData.currency);
419
+ console.log(
420
+ "[Deal] Auto-populated country from company address:",
421
+ normalizedCountry,
422
+ );
423
+ }
424
+ }
425
+ }, [user?.company_country, dealId, setValue]);
426
+
427
+ // Debounce brand search - reset accumulated brands and page when search changes
428
+ useEffect(() => {
429
+ const timer = setTimeout(() => {
430
+ setDebouncedBrandSearch(brandSearch);
431
+ setBrandPage(1);
432
+ setAccumulatedBrands([]);
433
+ }, 300);
434
+ return () => clearTimeout(timer);
435
+ }, [brandSearch]);
436
+
437
+ // Debug log for user state and auto-refresh if company_id is missing
438
+ useEffect(() => {
439
+ console.log("[CreateDeal] User state:", {
440
+ hasUser: !!user,
441
+ company_id: user?.company_id,
442
+ name: user?.name,
443
+ });
444
+
445
+ // If user exists but company_id is missing, try to refresh auth after a delay
446
+ // Only do this once per session to avoid infinite reload loops
447
+ const reloadAttemptedKey = "brands_reload_attempted";
448
+ if (
449
+ user &&
450
+ !user.company_id &&
451
+ !sessionStorage.getItem(reloadAttemptedKey)
452
+ ) {
453
+ console.log(
454
+ "[CreateDeal] No company_id, will try to refresh page once...",
455
+ );
456
+ sessionStorage.setItem(reloadAttemptedKey, "true");
457
+ const timer = setTimeout(() => {
458
+ console.log(
459
+ "[CreateDeal] Triggering page reload to refresh user data...",
460
+ );
461
+ window.location.reload();
462
+ }, 2000);
463
+ return () => clearTimeout(timer);
464
+ }
465
+ }, [user]);
466
+
467
+ // Reset accumulated brands on mount to ensure fresh state
468
+ useEffect(() => {
469
+ console.log("[CreateDeal] Component mounted, resetting accumulated brands");
470
+ setAccumulatedBrands([]);
471
+ setBrandPage(1);
472
+ setBrandSearch("");
473
+ setDebouncedBrandSearch("");
474
+ }, []);
475
+
476
+ // Fetch brands from API
477
+ const {
478
+ data: brandsData,
479
+ isLoading: isBrandsLoading,
480
+ isFetching: isBrandsFetching,
481
+ isPending: isBrandsPending,
482
+ refetch: refetchBrands,
483
+ } = useQuery({
484
+ queryKey: [
485
+ "company-brands",
486
+ user?.company_id,
487
+ debouncedBrandSearch,
488
+ brandPage,
489
+ ],
490
+ queryFn: async () => {
491
+ console.log(
492
+ "[CreateDeal] Fetching brands for company_id:",
493
+ user?.company_id,
494
+ );
495
+ if (!user?.company_id)
496
+ return { brands: [], total: 0, page: 1, limit: 20 };
497
+ return fetchCompanyBrands(user.company_id, {
498
+ page: brandPage,
499
+ limit: 20,
500
+ search: debouncedBrandSearch || undefined,
501
+ });
502
+ },
503
+ enabled: !!user?.company_id,
504
+ refetchOnMount: "always",
505
+ staleTime: 0,
506
+ gcTime: 0,
507
+ });
508
+
509
+ // Refetch brands when company_id becomes available or component remounts
510
+ useEffect(() => {
511
+ if (user?.company_id) {
512
+ console.log("[CreateDeal] company_id available, refetching brands");
513
+ refetchBrands();
514
+ }
515
+ }, [user?.company_id, refetchBrands]);
516
+
517
+ // Accumulate brands as pages are loaded
518
+ useEffect(() => {
519
+ console.log("[CreateDeal] brandsData changed:", {
520
+ hasBrandsData: !!brandsData,
521
+ brandsCount: brandsData?.brands?.length ?? 0,
522
+ total: brandsData?.total ?? 0,
523
+ brandPage,
524
+ currentAccumulatedCount: accumulatedBrands.length,
525
+ });
526
+ if (brandsData?.brands) {
527
+ if (brandPage === 1) {
528
+ console.log(
529
+ "[CreateDeal] Setting accumulated brands (page 1):",
530
+ brandsData.brands.length,
531
+ "brands",
532
+ );
533
+ setAccumulatedBrands(brandsData.brands);
534
+ } else {
535
+ setAccumulatedBrands((prev) => {
536
+ const existingIds = new Set(prev.map((b) => b.id));
537
+ const newBrands = brandsData.brands.filter(
538
+ (b) => !existingIds.has(b.id),
539
+ );
540
+ console.log(
541
+ "[CreateDeal] Accumulating brands (page",
542
+ brandPage,
543
+ "):",
544
+ newBrands.length,
545
+ "new brands",
546
+ );
547
+ return [...prev, ...newBrands];
548
+ });
549
+ }
550
+ }
551
+ }, [brandsData, brandPage]);
552
+
553
+ useEffect(() => {
554
+ function handleClickOutside(event: MouseEvent) {
555
+ if (
556
+ brandDropdownRef.current &&
557
+ !brandDropdownRef.current.contains(event.target as Node)
558
+ ) {
559
+ setBrandSearchOpen(false);
560
+ }
561
+ if (
562
+ agencyDropdownRef.current &&
563
+ !agencyDropdownRef.current.contains(event.target as Node)
564
+ ) {
565
+ setAgencySearchOpen(false);
566
+ }
567
+ }
568
+ document.addEventListener("mousedown", handleClickOutside);
569
+ return () => document.removeEventListener("mousedown", handleClickOutside);
570
+ }, []);
571
+
572
+ useEffect(() => {
573
+ const options = {
574
+ root: null,
575
+ rootMargin: "-20% 0px -60% 0px",
576
+ threshold: 0,
577
+ };
578
+
579
+ const observer = new IntersectionObserver((entries) => {
580
+ entries.forEach((entry) => {
581
+ if (entry.isIntersecting) {
582
+ const sectionId = entry.target.getAttribute("data-section");
583
+ if (sectionId) {
584
+ setVisibleSection(sectionId as TipSection);
585
+ }
586
+ }
587
+ });
588
+ }, options);
589
+
590
+ const sections = [basicSectionRef.current, supplySectionRef.current, brandSectionRef.current, budgetSectionRef.current];
591
+ sections.forEach((section) => {
592
+ if (section) observer.observe(section);
593
+ });
594
+
595
+ return () => observer.disconnect();
596
+ }, []);
597
+
598
+ // Populate form with existing deal data in edit mode
599
+ useEffect(() => {
600
+ if (existingDeal && !isDataLoaded) {
601
+ const deal = existingDeal as any;
602
+ const dealMode = (deal.mode || "DIRECT") as "DIRECT" | "PROGRAMMATIC";
603
+ setMode(dealMode);
604
+
605
+ // Extract brand name from direct.brand field
606
+ const brandName =
607
+ deal.direct?.brand ||
608
+ (typeof deal.brand === "object"
609
+ ? deal.brand?.name || ""
610
+ : deal.brand || "");
611
+
612
+ if (dealMode === "DIRECT") {
613
+ const rawCountry =
614
+ deal.direct?.marketSelection?.country || deal.country || "Malaysia";
615
+ const dealCountry = normalizeCountry(rawCountry);
616
+ const existingAgencyId =
617
+ deal.direct?.agencyId || deal.advertiser?.id || "";
618
+
619
+ if (existingAgencyId) {
620
+ setSelectedAgency(existingAgencyId);
621
+ }
622
+
623
+ reset({
624
+ mode: "DIRECT",
625
+ name: deal.name || "",
626
+ brand: brandName,
627
+ clientType: deal.direct?.clientType || "DIRECT_ADVERTISER",
628
+ agencyId: existingAgencyId,
629
+ country: dealCountry,
630
+ currency: deal.currency || "JPY",
631
+ totalBudget:
632
+ deal.direct?.budgetSetup?.budgetAmount ||
633
+ deal.totalBudget ||
634
+ undefined,
635
+ dailyBudgetCap: deal.direct?.dailyBudgetCap || undefined,
636
+ goalType: deal.direct?.campaignGoal?.type || undefined,
637
+ goalValue: deal.direct?.campaignGoal?.targetValue || undefined,
638
+ } as any);
639
+
640
+ if (deal.direct?.approvalEmails) {
641
+ setApprovalEmails(deal.direct.approvalEmails);
642
+ }
643
+
644
+ if (deal.seller?.email) {
645
+ setSellerEmail(deal.seller.email);
646
+ }
647
+ setSellerEmailInitialized(true);
648
+ } else {
649
+ const existingBuyers = deal.programmatic?.buyers || [
650
+ { dsp: "", seatId: "", buyerName: "" },
651
+ ];
652
+ const mappedBuyers = existingBuyers.map((b: any) => {
653
+ let mapped = { ...b };
654
+ if (b.dsp && dspOptions.length > 0) {
655
+ const matchById = dspOptions.find((d: any) => d.id === b.dsp);
656
+ if (!matchById) {
657
+ const matchByName = dspOptions.find((d: any) => d.name.toLowerCase() === b.dsp.toLowerCase());
658
+ if (matchByName) {
659
+ mapped.dsp = matchByName.id;
660
+ }
661
+ }
662
+ }
663
+ if (!mapped.buyerId && buyersForDsp.length > 0 && mapped.buyerName) {
664
+ const matchedBuyer = buyersForDsp.find(
665
+ (ab) => (ab.seatName || ab.name) === mapped.buyerName ||
666
+ (mapped.seatId && ab.seatId === mapped.seatId)
667
+ );
668
+ if (matchedBuyer) {
669
+ mapped.buyerId = matchedBuyer.id;
670
+ }
671
+ }
672
+ return mapped;
673
+ });
674
+ setBuyers(mappedBuyers);
675
+ const firstDspId = mappedBuyers.find((b: any) => b.dsp)?.dsp;
676
+ if (firstDspId) {
677
+ const dsp = dspOptions.find((d: any) => d.id === firstDspId);
678
+ if (dsp) {
679
+ setActiveDspId(dsp.id);
680
+ setActiveDspName(dsp.name);
681
+ } else {
682
+ setActiveDspId(firstDspId);
683
+ setActiveDspName(firstDspId);
684
+ }
685
+ }
686
+ setIsNonBillable(deal.programmatic?.nonBillable || deal.direct?.nonBillable || false);
687
+ setIsHardStop(deal.programmatic?.stopBidRequests || false);
688
+
689
+ const txType = deal.programmatic?.transactionType || "SPOT";
690
+ setSelectedTransactionType(txType);
691
+ setImpMultiplierType(deal.programmatic?.impMultiplierType || null);
692
+ setDescription(deal.description || "");
693
+ setSellerPhone(deal.seller?.phone || "");
694
+ setSellerName(deal.seller?.name || "");
695
+
696
+ const validBuyers = mappedBuyers.filter((b: any) => b.dsp && b.seatId && b.buyerName);
697
+ if (validBuyers.length > 0) {
698
+ setIsDealTypeLocked(true);
699
+ setIsDspLocked(true);
700
+ }
701
+
702
+ reset({
703
+ mode: "PROGRAMMATIC",
704
+ name: deal.name || "",
705
+ dealType: deal.dealType || "GUARANTEED",
706
+ buyers: mappedBuyers,
707
+ auctionType: deal.programmatic?.auctionType || 3,
708
+ transactionType: txType,
709
+ currency: deal.currency || "",
710
+ } as any);
711
+ }
712
+
713
+ setIsDataLoaded(true);
714
+ }
715
+ }, [existingDeal, isDataLoaded, reset, dspOptions, buyersForDsp]);
716
+
717
+ // Initialize seller email from logged-in user when creating new deal (only once)
718
+ useEffect(() => {
719
+ if (!isEditMode && user?.email && !sellerEmailInitialized) {
720
+ setSellerEmail(user.email);
721
+ setSellerEmailInitialized(true);
722
+ }
723
+ }, [user?.email, isEditMode, sellerEmailInitialized]);
724
+
725
+ // Re-validate agency selection when agencies data loads (for edit mode)
726
+ useEffect(() => {
727
+ if (existingDeal && agencies.length > 0 && selectedAgency) {
728
+ const agencyExists = agencies.find((a) => a.id === selectedAgency);
729
+ if (!agencyExists) {
730
+ console.warn(
731
+ `[CreateDeal] Selected agency ${selectedAgency} not found in loaded agencies`,
732
+ );
733
+ }
734
+ }
735
+ }, [agencies, existingDeal, selectedAgency]);
736
+
737
+ const handleModeChange = (newMode: "DIRECT" | "PROGRAMMATIC") => {
738
+ setMode(newMode);
739
+ const currentName = watch("name");
740
+ const currentBrand = watch("brand");
741
+ const currentCurrency = watch("currency" as any);
742
+
743
+ const currentCountry = watch("country" as any);
744
+ if (newMode === "DIRECT") {
745
+ reset({
746
+ mode: "DIRECT",
747
+ name: currentName,
748
+ brand: currentBrand,
749
+ clientType: "DIRECT_ADVERTISER",
750
+ country: currentCountry || "Japan",
751
+ currency: currentCurrency || "JPY",
752
+ totalBudget: undefined,
753
+ dailyBudgetCap: undefined,
754
+ } as any);
755
+ setBuyers([]);
756
+ setActiveDspId("");
757
+ setActiveDspName("");
758
+ setIsDealTypeLocked(false);
759
+ setIsDspLocked(false);
760
+ setSelectedTransactionType("");
761
+ setImpMultiplierType(null);
762
+ setIsHardStop(false);
763
+ } else {
764
+ reset({
765
+ mode: "PROGRAMMATIC",
766
+ name: currentName,
767
+ dealType: "GUARANTEED",
768
+ buyers: [],
769
+ auctionType: 3,
770
+ transactionType: "SPOT",
771
+ currency: "",
772
+ } as any);
773
+ setBuyers([]);
774
+ setActiveDspId("");
775
+ setActiveDspName("");
776
+ setIsDealTypeLocked(false);
777
+ setIsDspLocked(false);
778
+ setSelectedTransactionType("");
779
+ setImpMultiplierType(null);
780
+ setIsHardStop(false);
781
+ }
782
+ };
783
+
784
+ // Use brandsData directly if accumulatedBrands hasn't been populated yet
785
+ // This fixes a timing issue where the dropdown opens before the useEffect runs
786
+ const filteredBrands: CompanyBrand[] =
787
+ accumulatedBrands.length > 0 ? accumulatedBrands : brandsData?.brands || [];
788
+ const hasMoreBrands = brandsData
789
+ ? brandsData.total > filteredBrands.length
790
+ : false;
791
+
792
+ const filteredAgencies = agencies.filter((a) =>
793
+ a.name.toLowerCase().includes(agencySearch.toLowerCase()),
794
+ );
795
+
796
+ const addEmail = () => {
797
+ const email = emailInput.trim();
798
+ if (
799
+ email &&
800
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) &&
801
+ !approvalEmails.includes(email)
802
+ ) {
803
+ const newEmails = [...approvalEmails, email];
804
+ setApprovalEmails(newEmails);
805
+ setValue("approvalEmails" as any, newEmails);
806
+ setEmailInput("");
807
+ }
808
+ };
809
+
810
+ const removeEmail = (email: string) => {
811
+ const newEmails = approvalEmails.filter((e) => e !== email);
812
+ setApprovalEmails(newEmails);
813
+ setValue("approvalEmails" as any, newEmails);
814
+ };
815
+
816
+ // Legacy handlers removed - using centralized handleDspChange/handleAddBuyer/handleRemoveBuyer instead
817
+
818
+ const handleDealTypeChange = (value: string) => {
819
+ setValue("dealType" as any, value);
820
+ setActiveDspId("");
821
+ setActiveDspName("");
822
+ setBuyers([]);
823
+ setValue("buyers" as any, []);
824
+ setValue("transactionType" as any, "");
825
+ setSelectedTransactionType("");
826
+ setValue("currency" as any, "");
827
+ setImpMultiplierType(null);
828
+ setIsDealTypeLocked(false);
829
+ setIsDspLocked(false);
830
+
831
+ if (value !== "PRIVATE_AUCTION") {
832
+ setValue("auctionType" as any, 3);
833
+ } else {
834
+ setValue("auctionType" as any, undefined);
835
+ }
836
+
837
+ if (value !== "GUARANTEED") {
838
+ setIsHardStop(false);
839
+ }
840
+ };
841
+
842
+ const handleDspChange = (dspId: string) => {
843
+ const selectedDsp = dspOptions.find((d) => d.id === dspId);
844
+ setActiveDspId(dspId);
845
+ setActiveDspName(selectedDsp?.name || dspId);
846
+
847
+ setBuyers([]);
848
+ setValue("buyers" as any, []);
849
+ setValue("transactionType" as any, "");
850
+ setSelectedTransactionType("");
851
+ setValue("currency" as any, "");
852
+ setImpMultiplierType(null);
853
+ setIsDspLocked(false);
854
+ };
855
+
856
+ const handleAddBuyer = (buyerValue: string) => {
857
+ const selectedBuyerData = buyersForDsp.find((b) => b.id === buyerValue);
858
+ if (!selectedBuyerData) return;
859
+
860
+ if (buyers.some(b => b.seatId === (selectedBuyerData.seatId || ""))) return;
861
+
862
+ if (!allowMultipleBuyers && buyers.length >= 1) return;
863
+
864
+ const newBuyer = {
865
+ dsp: activeDspName,
866
+ seatId: (selectedBuyerData.seatId || "").trim(),
867
+ buyerName: selectedBuyerData.seatName || selectedBuyerData.name,
868
+ buyerId: selectedBuyerData.id,
869
+ buyerAccountId: (selectedBuyerData as any).buyerAccountId || "",
870
+ };
871
+
872
+ const newBuyers = [...buyers, newBuyer];
873
+ setBuyers(newBuyers);
874
+ setValue("buyers" as any, newBuyers);
875
+
876
+ setIsDspLocked(true);
877
+ setIsDealTypeLocked(true);
878
+ };
879
+
880
+ const handleRemoveBuyer = (index: number) => {
881
+ const newBuyers = buyers.filter((_, i) => i !== index);
882
+ setBuyers(newBuyers);
883
+ setValue("buyers" as any, newBuyers);
884
+
885
+ if (newBuyers.length === 0) {
886
+ setIsDealTypeLocked(false);
887
+ setIsDspLocked(false);
888
+ setValue("dealType" as any, "");
889
+ setActiveDspId("");
890
+ setActiveDspName("");
891
+ setValue("transactionType" as any, "");
892
+ setSelectedTransactionType("");
893
+ setValue("currency" as any, "");
894
+ }
895
+ };
896
+
897
+ const handleTransactionTypeChange = (value: string) => {
898
+ setValue("transactionType" as any, value);
899
+ setSelectedTransactionType(value);
900
+ if (value !== "AUDIENCE") {
901
+ setImpMultiplierType(null);
902
+ }
903
+ };
904
+
905
+ const hasBuyers = buyers.some(b => b.dsp && b.seatId && b.buyerName);
906
+ void hasBuyers;
907
+ const isDspEnabled = !!dealType;
908
+ const isBuyerEnabled = !!activeDspId;
909
+ const isTransactionTypeEnabled = !!activeDspId;
910
+ const isCurrencyEnabled = !!activeDspId;
911
+ const showAuctionType = dealType === "PRIVATE_AUCTION";
912
+ const showHardStop = dealType === "GUARANTEED";
913
+ const showImpMultiplier = selectedTransactionType === "AUDIENCE";
914
+ const allowMultipleBuyers =
915
+ dealType === "PRIVATE_AUCTION" || dealType === "EVERGREEN_PMP";
916
+
917
+ const buildPayload = (data: FormData, isUpdate: boolean = false) => {
918
+ const agency = getSelectedAgency();
919
+
920
+ const payload: any = {
921
+ name: data.name,
922
+ mode: data.mode,
923
+ currency: data.currency,
924
+ advertiser: agency
925
+ ? {
926
+ id: agency.id,
927
+ seatId: agency.external_id,
928
+ name: agency.name,
929
+ }
930
+ : {
931
+ name: data.brand,
932
+ },
933
+ seller: {
934
+ id: user?.id || "",
935
+ name: sellerName || user?.name || "",
936
+ publisherId: user?.company_id || "",
937
+ publisherName: user?.company_name || "",
938
+ email: sellerEmail || user?.email || "",
939
+ phone: sellerPhone || "",
940
+ },
941
+ account: {
942
+ userId: user?.id || "",
943
+ companyName: user?.company_name || "",
944
+ companyId: user?.company_id || "",
945
+ email: sellerEmail || user?.email || "",
946
+ },
947
+ };
948
+
949
+ if (!isUpdate) {
950
+ payload.source = "influence";
951
+ payload.status = "DRAFT";
952
+ }
953
+
954
+ if (data.mode === "DIRECT") {
955
+ const countryData = COUNTRIES.find(
956
+ (c) => c.value === (data as any).country,
957
+ );
958
+ if (!isUpdate) {
959
+ payload.dealType = "DIRECT";
960
+ payload.country = (data as any).country;
961
+ }
962
+ payload.direct = {
963
+ brand: data.brand,
964
+ clientType: data.clientType,
965
+ agencyId: selectedAgency || undefined,
966
+ approvalEmails: approvalEmails,
967
+ marketSelection: {
968
+ country: (data as any).country,
969
+ currency: data.currency,
970
+ region: countryData?.region || "APAC",
971
+ },
972
+ budgetSetup: {
973
+ currency: data.currency,
974
+ budgetAmount: data.totalBudget || 0,
975
+ budgetType: "TOTAL",
976
+ },
977
+ campaignGoal: {
978
+ type: data.goalType || "IMPRESSIONS",
979
+ targetValue: data.goalValue || 0,
980
+ },
981
+ nonBillable: isNonBillable,
982
+ };
983
+ } else {
984
+ if (!isUpdate) {
985
+ payload.dealType = data.dealType;
986
+ }
987
+ payload.advertiser = undefined;
988
+ const programmaticPayload: any = {
989
+ buyers: buyers.filter((b) => b.dsp && b.seatId && b.buyerName).map(b => ({
990
+ dsp: b.dsp,
991
+ seatId: b.seatId,
992
+ name: b.buyerName,
993
+ buyerAccountId: (b as any).buyerAccountId || "",
994
+ })),
995
+ auctionType: dealType === "PRIVATE_AUCTION" ? data.auctionType : 3,
996
+ transactionType: selectedTransactionType || data.transactionType,
997
+ nonBillable: isNonBillable,
998
+ stopBidRequests: isHardStop,
999
+ published: false,
1000
+ reopened: false,
1001
+ };
1002
+ if (showImpMultiplier && impMultiplierType) {
1003
+ programmaticPayload.impMultiplierType = impMultiplierType;
1004
+ }
1005
+ payload.programmatic = programmaticPayload;
1006
+ }
1007
+
1008
+ return payload;
1009
+ };
1010
+
1011
+ const createDealMutation = useMutation({
1012
+ mutationFn: async (data: FormData) => {
1013
+ const payload = buildPayload(data, false);
1014
+ const response = await influenceDealsRequest<Deal>(
1015
+ InfluenceDealsAPI.deals.create(),
1016
+ "POST",
1017
+ payload,
1018
+ );
1019
+ return response;
1020
+ },
1021
+ onSuccess: (deal) => {
1022
+ queryClient.invalidateQueries({ queryKey: ["direct-campaigns"] });
1023
+ toast({
1024
+ title: t("deals:createDeal.successTitle"),
1025
+ description: t("deals:createDeal.successDescription"),
1026
+ });
1027
+ setLocation(`/deals/${deal.id}/line-items`);
1028
+ },
1029
+ onError: (error: unknown) => {
1030
+ toast({
1031
+ title: t("deals:createDeal.errorTitle"),
1032
+ description: formatAPIErrorForToast(error),
1033
+ variant: "destructive",
1034
+ });
1035
+ },
1036
+ });
1037
+
1038
+ const updateDealMutation = useMutation({
1039
+ mutationFn: async (data: FormData) => {
1040
+ const payload = buildPayload(data, true);
1041
+ const response = await influenceDealsRequest<Deal>(
1042
+ InfluenceDealsAPI.deals.update(dealId!),
1043
+ "PUT",
1044
+ payload,
1045
+ );
1046
+ return response;
1047
+ },
1048
+ onSuccess: () => {
1049
+ queryClient.invalidateQueries({ queryKey: ["direct-campaigns"] });
1050
+ toast({
1051
+ title: t("deals:updateDeal.successTitle"),
1052
+ description: t("deals:updateDeal.successDescription"),
1053
+ });
1054
+ setLocation(`/deals/${dealId}/line-items`);
1055
+ },
1056
+ onError: (error: unknown) => {
1057
+ toast({
1058
+ title: t("deals:updateDeal.errorTitle"),
1059
+ description: formatAPIErrorForToast(error),
1060
+ variant: "destructive",
1061
+ });
1062
+ },
1063
+ });
1064
+
1065
+ const onSubmit = (data: FormData) => {
1066
+ if (mode === "PROGRAMMATIC") {
1067
+ (data as any).buyers = buyers.filter(
1068
+ (b) => b.dsp && b.seatId && b.buyerName,
1069
+ );
1070
+ }
1071
+
1072
+ if (mode === "DIRECT" && selectedAgency && !getSelectedAgency()) {
1073
+ toast({
1074
+ title: t("deals:createDeal.pleaseWait"),
1075
+ description: t("deals:createDeal.loadingAgencyData"),
1076
+ variant: "destructive",
1077
+ });
1078
+ return;
1079
+ }
1080
+
1081
+ if (isEditMode) {
1082
+ updateDealMutation.mutate(data);
1083
+ } else {
1084
+ createDealMutation.mutate(data);
1085
+ }
1086
+ };
1087
+
1088
+ const isMutating =
1089
+ createDealMutation.isPending || updateDealMutation.isPending;
1090
+
1091
+ const isAuctionTypeDisabled =
1092
+ mode === "PROGRAMMATIC" &&
1093
+ (dealType === "GUARANTEED" || dealType === "PREFERRED_DEAL");
1094
+ void isAuctionTypeDisabled;
1095
+
1096
+ return (
1097
+ <div className="flex flex-col min-h-[calc(100vh-64px)]">
1098
+ {/* Page Header */}
1099
+ <div className="flex-shrink-0 px-6 py-4 border-b border-mw-neutral-100 dark:border-mw-neutral-800 bg-white dark:bg-mw-neutral-900">
1100
+ <div className="w-full">
1101
+ <div className="flex items-center justify-between">
1102
+ <div className="flex items-center gap-3">
1103
+ <Button
1104
+ type="button"
1105
+ variant="ghost"
1106
+ size="sm"
1107
+ isIconOnly
1108
+ onClick={() => setLocation("/deals")}
1109
+ >
1110
+ <ArrowLeft className="h-5 w-5" />
1111
+ </Button>
1112
+ <div className="h-10 w-10 rounded-lg bg-orange-50 dark:bg-orange-900/20 flex items-center justify-center flex-shrink-0">
1113
+ <FileText className="h-5 w-5 text-orange-500 dark:text-orange-400" />
1114
+ </div>
1115
+ <div>
1116
+ <h1 className="text-lg font-semibold text-mw-neutral-900 dark:text-white">
1117
+ {isEditMode ? "Update Deal" : "New Deal"}
1118
+ </h1>
1119
+ <p className="text-sm text-mw-neutral-500 dark:text-mw-neutral-400">
1120
+ {isEditMode ? "Update your advertising deal" : "Create a new advertising deal"}
1121
+ </p>
1122
+ </div>
1123
+ </div>
1124
+ </div>
1125
+ </div>
1126
+ </div>
1127
+
1128
+ {/* Scrollable Content */}
1129
+ <div className="flex-1 overflow-y-auto" ref={formContainerRef}>
1130
+ <div className="w-full px-6 pt-8 pb-6">
1131
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
1132
+ <div className="lg:col-span-2">
1133
+ <Card className="border-mw-neutral-200 dark:border-mw-neutral-700">
1134
+ <CardContent className="p-6">
1135
+ <div className="flex items-center gap-3 mb-6">
1136
+ <div className="h-10 w-10 rounded-lg bg-mw-neutral-100 dark:bg-mw-neutral-700 flex items-center justify-center flex-shrink-0">
1137
+ <FileText className="h-5 w-5 text-mw-neutral-500" />
1138
+ </div>
1139
+ <div>
1140
+ <h2 className="text-base font-semibold text-mw-neutral-900 dark:text-white">Deal Details</h2>
1141
+ <p className="text-sm text-mw-neutral-500">Set up your deal information</p>
1142
+ </div>
1143
+ </div>
1144
+ <form
1145
+ onSubmit={handleSubmit(onSubmit)}
1146
+ className="space-y-6"
1147
+ id="deal-form"
1148
+ >
1149
+ <div ref={basicSectionRef} data-section="basic" className="space-y-6">
1150
+ <FormItem>
1151
+ <FormLabel required>
1152
+ {t("deals:deals.dealName")}
1153
+ </FormLabel>
1154
+ <FormControl>
1155
+ <Input
1156
+ placeholder={t("deals:createDeal.enterDealName")}
1157
+ maxLength={200}
1158
+ {...register("name")}
1159
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
1160
+ />
1161
+ </FormControl>
1162
+ {errors.name && (
1163
+ <FormMessage>{errors.name.message}</FormMessage>
1164
+ )}
1165
+ </FormItem>
1166
+
1167
+
1168
+ <FormItem>
1169
+ <FormLabel>{t("deals:createDeal.mediaType")}</FormLabel>
1170
+ <SelectRoot
1171
+ value={mediaType}
1172
+ onValueChange={setMediaType}
1173
+ disabled
1174
+ >
1175
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700 bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed">
1176
+ <SelectValue placeholder={t("deals:createDeal.selectMediaType")} />
1177
+ </SelectTrigger>
1178
+ <SelectContent>
1179
+ {MEDIA_TYPE_OPTIONS.map((option) => (
1180
+ <SelectItem key={option.value} value={option.value}>
1181
+ {option.label}
1182
+ </SelectItem>
1183
+ ))}
1184
+ </SelectContent>
1185
+ </SelectRoot>
1186
+ </FormItem>
1187
+
1188
+ <FormItem>
1189
+ <FormLabel>{t("deals:createDeal.sspPartner")}</FormLabel>
1190
+ <SelectRoot
1191
+ value={sspPartner}
1192
+ onValueChange={setSspPartner}
1193
+ disabled
1194
+ >
1195
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700 bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed">
1196
+ <SelectValue placeholder={t("deals:createDeal.selectSspPartner")} />
1197
+ </SelectTrigger>
1198
+ <SelectContent>
1199
+ {SSP_PARTNER_OPTIONS.map((option) => (
1200
+ <SelectItem key={option.value} value={option.value}>
1201
+ {option.label}
1202
+ </SelectItem>
1203
+ ))}
1204
+ </SelectContent>
1205
+ </SelectRoot>
1206
+ </FormItem>
1207
+ </div>
1208
+
1209
+ <div ref={supplySectionRef} data-section="supply" className="space-y-6">
1210
+ <FormItem>
1211
+ <FormLabel required>
1212
+ Deal Type
1213
+ </FormLabel>
1214
+ <div className="flex gap-0">
1215
+ <Button
1216
+ type="button"
1217
+ variant={mode === "DIRECT" ? "primary" : "outline"}
1218
+ size="sm"
1219
+ onClick={() => handleModeChange("DIRECT")}
1220
+ className="rounded-r-none"
1221
+ >
1222
+ Traditional
1223
+ </Button>
1224
+ <Button
1225
+ type="button"
1226
+ variant={mode === "PROGRAMMATIC" ? "primary" : "outline"}
1227
+ size="sm"
1228
+ onClick={() => handleModeChange("PROGRAMMATIC")}
1229
+ className="rounded-l-none border-l-0"
1230
+ >
1231
+ Programmatic
1232
+ </Button>
1233
+ </div>
1234
+ </FormItem>
1235
+
1236
+ {mode === "DIRECT" && (
1237
+ <div ref={brandSectionRef} data-section="brand">
1238
+ <FormItem>
1239
+ <FormLabel>
1240
+ <div className="flex items-center gap-1">
1241
+ <Tag className="h-4 w-4" />
1242
+ {t("deals:deals.brand")}
1243
+ <span className="text-destructive">*</span>
1244
+ </div>
1245
+ </FormLabel>
1246
+ <div className="relative" ref={brandDropdownRef}>
1247
+ <div
1248
+ className={cn(
1249
+ "flex h-10 w-full items-center justify-between rounded-md border border-mw-neutral-200 bg-white px-3 py-2 text-sm cursor-pointer",
1250
+ "hover:border-mw-primary-300 focus-within:border-mw-primary-500 focus-within:ring-2 focus-within:ring-mw-primary-100",
1251
+ "dark:border-mw-neutral-700 dark:bg-mw-neutral-800",
1252
+ )}
1253
+ onClick={() => setBrandSearchOpen(!brandSearchOpen)}
1254
+ >
1255
+ <span
1256
+ className={
1257
+ brand
1258
+ ? "text-mw-neutral-900 dark:text-white"
1259
+ : "text-mw-neutral-400"
1260
+ }
1261
+ >
1262
+ {brand || t("deals:createDeal.selectBrand")}
1263
+ </span>
1264
+ <Search className="h-4 w-4 text-mw-neutral-400" />
1265
+ </div>
1266
+
1267
+ {brandSearchOpen && (
1268
+ <div className="absolute z-50 mt-1 w-full rounded-md border border-mw-neutral-200 bg-white shadow-lg dark:border-mw-neutral-700 dark:bg-mw-neutral-800">
1269
+ <div className="p-2 border-b border-mw-neutral-100 dark:border-mw-neutral-700">
1270
+ <div className="flex items-center gap-2">
1271
+ <div className="relative flex-1">
1272
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-mw-neutral-400" />
1273
+ <Input
1274
+ placeholder={t(
1275
+ "deals:createDeal.searchBrands",
1276
+ )}
1277
+ value={brandSearch}
1278
+ onChange={(e) =>
1279
+ setBrandSearch(e.target.value)
1280
+ }
1281
+ className="pl-8 h-9 border-mw-neutral-200"
1282
+ autoFocus
1283
+ />
1284
+ </div>
1285
+ <Button
1286
+ type="button"
1287
+ variant="ghost"
1288
+ size="sm"
1289
+ isIconOnly
1290
+ onClick={() => {
1291
+ setAccumulatedBrands([]);
1292
+ setBrandPage(1);
1293
+ queryClient.invalidateQueries({
1294
+ queryKey: ["company-brands"],
1295
+ });
1296
+ }}
1297
+ title={t("common:actions.refresh")}
1298
+ >
1299
+ <RefreshCw
1300
+ className={cn(
1301
+ "h-4 w-4",
1302
+ isBrandsFetching && "animate-spin",
1303
+ )}
1304
+ />
1305
+ </Button>
1306
+ </div>
1307
+ </div>
1308
+ <div className="max-h-48 overflow-y-auto">
1309
+ {(() => {
1310
+ console.log(
1311
+ "[CreateDeal] Rendering brands dropdown:",
1312
+ {
1313
+ isBrandsLoading,
1314
+ isBrandsFetching,
1315
+ isBrandsPending,
1316
+ filteredBrandsCount:
1317
+ filteredBrands.length,
1318
+ accumulatedBrandsCount:
1319
+ accumulatedBrands.length,
1320
+ brandSearch,
1321
+ brandsDataExists: !!brandsData,
1322
+ brandsDataBrandsLength:
1323
+ brandsData?.brands?.length ?? 0,
1324
+ userCompanyId: user?.company_id,
1325
+ },
1326
+ );
1327
+ return null;
1328
+ })()}
1329
+ {!user?.company_id ? (
1330
+ <div className="px-3 py-4 text-center text-sm text-mw-neutral-500">
1331
+ {t("deals:createDeal.typeToEnterBrand")}
1332
+ </div>
1333
+ ) : (isBrandsLoading ||
1334
+ isBrandsFetching ||
1335
+ isBrandsPending) &&
1336
+ filteredBrands.length === 0 ? (
1337
+ <div className="px-3 py-4 text-center text-sm text-mw-neutral-500">
1338
+ {t("deals:createDeal.loadingBrands")}
1339
+ </div>
1340
+ ) : filteredBrands.length === 0 ? (
1341
+ <div className="px-3 py-4 text-center text-sm text-mw-neutral-500">
1342
+ {brandSearch
1343
+ ? t("deals:createDeal.noBrandsFound")
1344
+ : t("deals:createDeal.typeToEnterBrand")}
1345
+ </div>
1346
+ ) : (
1347
+ <>
1348
+ {filteredBrands.map((b) => (
1349
+ <div
1350
+ key={b.id}
1351
+ className="px-3 py-2.5 hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-700 cursor-pointer"
1352
+ onClick={() => {
1353
+ setValue("brand", b.name);
1354
+ setBrandSearchOpen(false);
1355
+ setBrandSearch("");
1356
+ }}
1357
+ >
1358
+ <div className="font-medium text-sm text-mw-neutral-900 dark:text-white">
1359
+ {b.name}
1360
+ </div>
1361
+ <div className="text-xs text-mw-neutral-500">
1362
+ {b.code}
1363
+ </div>
1364
+ </div>
1365
+ ))}
1366
+ {hasMoreBrands && (
1367
+ <Button
1368
+ type="button"
1369
+ variant="ghost"
1370
+ size="sm"
1371
+ className="w-full"
1372
+ onClick={() =>
1373
+ setBrandPage((p) => p + 1)
1374
+ }
1375
+ disabled={isBrandsFetching}
1376
+ >
1377
+ {isBrandsFetching
1378
+ ? t("deals:createDeal.loading")
1379
+ : t("deals:createDeal.loadMore")}
1380
+ </Button>
1381
+ )}
1382
+ </>
1383
+ )}
1384
+ </div>
1385
+ <div className="p-2 border-t border-mw-neutral-100 dark:border-mw-neutral-700">
1386
+ <a
1387
+ href={`${API_CONFIG.authFE}/metadata/brands`}
1388
+ target="_blank"
1389
+ rel="noopener noreferrer"
1390
+ className="flex items-center gap-1.5 text-sm text-mw-primary-600 hover:text-mw-primary-700 font-medium"
1391
+ >
1392
+ <Plus className="h-4 w-4" />
1393
+ {t("deals:createDeal.createNewBrand")}
1394
+ <ExternalLink className="h-3 w-3 ml-1" />
1395
+ </a>
1396
+ </div>
1397
+ </div>
1398
+ )}
1399
+ </div>
1400
+ {errors.brand && (
1401
+ <FormMessage>{errors.brand.message}</FormMessage>
1402
+ )}
1403
+ </FormItem>
1404
+ </div>
1405
+ )}
1406
+
1407
+ {mode === "DIRECT" && (
1408
+ <>
1409
+ <FormItem>
1410
+ <FormLabel required>
1411
+ {t("deals:createDeal.clientType")}
1412
+ </FormLabel>
1413
+ <SelectRoot
1414
+ value={watch("clientType" as any)}
1415
+ onValueChange={(value) => {
1416
+ setValue("clientType" as any, value);
1417
+ if (value !== "AGENCY") {
1418
+ setSelectedAgency("");
1419
+ setValue("agencyId" as any, "");
1420
+ }
1421
+ }}
1422
+ >
1423
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1424
+ <SelectValue
1425
+ placeholder={t(
1426
+ "deals:createDeal.selectClientType",
1427
+ )}
1428
+ />
1429
+ </SelectTrigger>
1430
+ <SelectContent>
1431
+ {CLIENT_TYPE_OPTIONS.map((option) => (
1432
+ <SelectItem
1433
+ key={option.value}
1434
+ value={option.value}
1435
+ >
1436
+ {option.label}
1437
+ </SelectItem>
1438
+ ))}
1439
+ </SelectContent>
1440
+ </SelectRoot>
1441
+ {(errors as any).clientType && (
1442
+ <FormMessage>
1443
+ {(errors as any).clientType.message}
1444
+ </FormMessage>
1445
+ )}
1446
+ </FormItem>
1447
+
1448
+ {clientType === "AGENCY" && (
1449
+ <FormItem>
1450
+ <FormLabel required>
1451
+ {t("deals:deals.agency")}
1452
+ </FormLabel>
1453
+ <div className="relative" ref={agencyDropdownRef}>
1454
+ <div
1455
+ className={cn(
1456
+ "flex h-10 w-full items-center justify-between rounded-md border border-mw-neutral-200 bg-white px-3 py-2 text-sm cursor-pointer",
1457
+ "hover:border-mw-primary-300 focus-within:border-mw-primary-500 focus-within:ring-2 focus-within:ring-mw-primary-100",
1458
+ "dark:border-mw-neutral-700 dark:bg-mw-neutral-800",
1459
+ )}
1460
+ onClick={() =>
1461
+ setAgencySearchOpen(!agencySearchOpen)
1462
+ }
1463
+ >
1464
+ <span
1465
+ className={
1466
+ selectedAgency
1467
+ ? "text-mw-neutral-900 dark:text-white"
1468
+ : "text-mw-neutral-400"
1469
+ }
1470
+ >
1471
+ {getSelectedAgency()?.name ||
1472
+ t("deals:createDeal.selectAgency")}
1473
+ </span>
1474
+ <Search className="h-4 w-4 text-mw-neutral-400" />
1475
+ </div>
1476
+
1477
+ {agencySearchOpen && (
1478
+ <div className="absolute z-50 mt-1 w-full rounded-md border border-mw-neutral-200 bg-white shadow-lg dark:border-mw-neutral-700 dark:bg-mw-neutral-800">
1479
+ <div className="p-2 border-b border-mw-neutral-100 dark:border-mw-neutral-700">
1480
+ <div className="relative">
1481
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-mw-neutral-400" />
1482
+ <Input
1483
+ placeholder={t(
1484
+ "deals:createDeal.searchAgencies",
1485
+ )}
1486
+ value={agencySearch}
1487
+ onChange={(e) =>
1488
+ setAgencySearch(e.target.value)
1489
+ }
1490
+ className="pl-8 h-9 border-mw-neutral-200"
1491
+ autoFocus
1492
+ />
1493
+ </div>
1494
+ </div>
1495
+ <div className="max-h-48 overflow-y-auto">
1496
+ {isAgenciesLoading ? (
1497
+ <div className="px-3 py-4 text-center text-sm text-mw-neutral-500">
1498
+ {t(
1499
+ "deals:createDeal.loadingAgencies",
1500
+ )}
1501
+ </div>
1502
+ ) : filteredAgencies.length === 0 ? (
1503
+ <div className="px-3 py-4 text-center text-sm text-mw-neutral-500">
1504
+ {t(
1505
+ "deals:createDeal.noAgenciesFound",
1506
+ )}
1507
+ </div>
1508
+ ) : (
1509
+ filteredAgencies.map((agency) => (
1510
+ <div
1511
+ key={agency.id}
1512
+ className="px-3 py-2.5 hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-700 cursor-pointer"
1513
+ onClick={() => {
1514
+ handleAgencyChange(agency.id);
1515
+ setAgencySearchOpen(false);
1516
+ setAgencySearch("");
1517
+ }}
1518
+ >
1519
+ <div className="font-medium text-sm text-mw-neutral-900 dark:text-white">
1520
+ {agency.name}
1521
+ </div>
1522
+ <div className="text-xs text-mw-neutral-500">
1523
+ {agency.seat_id ? `Seat ID: ${agency.seat_id}` : "Agency"}
1524
+ </div>
1525
+ </div>
1526
+ ))
1527
+ )}
1528
+ </div>
1529
+ </div>
1530
+ )}
1531
+ </div>
1532
+ {selectedAgency && getSelectedAgency() && (
1533
+ <p className="text-xs text-mw-neutral-500 mt-1">
1534
+ {getSelectedAgency()?.seat_id ? `Seat ID: ${getSelectedAgency()?.seat_id}` : "Agency"}
1535
+ </p>
1536
+ )}
1537
+ </FormItem>
1538
+ )}
1539
+
1540
+ <FormItem>
1541
+ <FormLabel required>
1542
+ {t("deals:createDeal.country")}
1543
+ </FormLabel>
1544
+ <SelectRoot
1545
+ value={watch("country" as any)}
1546
+ onValueChange={handleCountryChange}
1547
+ >
1548
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1549
+ <SelectValue
1550
+ placeholder={t("deals:createDeal.selectCountry")}
1551
+ />
1552
+ </SelectTrigger>
1553
+ <SelectContent>
1554
+ {COUNTRIES.map((country) => (
1555
+ <SelectItem
1556
+ key={country.code}
1557
+ value={country.value}
1558
+ >
1559
+ <div className="flex items-center gap-2">
1560
+ <Globe className="h-4 w-4 text-mw-neutral-400" />
1561
+ <span>{country.value}</span>
1562
+ <span className="text-xs text-mw-neutral-500">
1563
+ ({country.code})
1564
+ </span>
1565
+ </div>
1566
+ </SelectItem>
1567
+ ))}
1568
+ </SelectContent>
1569
+ </SelectRoot>
1570
+ {(errors as any).country && (
1571
+ <FormMessage>
1572
+ {(errors as any).country.message}
1573
+ </FormMessage>
1574
+ )}
1575
+ </FormItem>
1576
+
1577
+ <input
1578
+ type="hidden"
1579
+ value={watch("country" as any) || "Japan"}
1580
+ />
1581
+
1582
+ <div ref={budgetSectionRef} data-section="budget" className="space-y-6">
1583
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1584
+ <FormItem>
1585
+ <FormLabel required>
1586
+ {t("deals:deals.currency")}
1587
+ </FormLabel>
1588
+ <SelectRoot
1589
+ value={watch("currency" as any)}
1590
+ onValueChange={(value) =>
1591
+ setValue("currency" as any, value)
1592
+ }
1593
+ >
1594
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1595
+ <SelectValue
1596
+ placeholder={t(
1597
+ "deals:createDeal.selectCurrency",
1598
+ )}
1599
+ />
1600
+ </SelectTrigger>
1601
+ <SelectContent>
1602
+ {CURRENCIES.map((option) => (
1603
+ <SelectItem
1604
+ key={option.value}
1605
+ value={option.value}
1606
+ >
1607
+ {option.label}
1608
+ </SelectItem>
1609
+ ))}
1610
+ </SelectContent>
1611
+ </SelectRoot>
1612
+ </FormItem>
1613
+
1614
+ <FormItem>
1615
+ <FormLabel>
1616
+ {t("deals:createDeal.totalBudget")}
1617
+ </FormLabel>
1618
+ <FormControl>
1619
+ <Input
1620
+ type="number"
1621
+ min={0}
1622
+ placeholder={t(
1623
+ "deals:createDeal.enterTotalBudget",
1624
+ )}
1625
+ {...register("totalBudget" as any, {
1626
+ valueAsNumber: true,
1627
+ })}
1628
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
1629
+ />
1630
+ </FormControl>
1631
+ </FormItem>
1632
+ </div>
1633
+
1634
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1635
+ <FormItem>
1636
+ <FormLabel>
1637
+ {t("deals:createDeal.goalType")}
1638
+ </FormLabel>
1639
+ <SelectRoot
1640
+ value={watch("goalType" as any) || ""}
1641
+ onValueChange={(value) =>
1642
+ setValue("goalType" as any, value)
1643
+ }
1644
+ >
1645
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1646
+ <SelectValue
1647
+ placeholder={t(
1648
+ "deals:createDeal.selectGoalType",
1649
+ )}
1650
+ />
1651
+ </SelectTrigger>
1652
+ <SelectContent>
1653
+ {GOAL_TYPE_OPTIONS.map((option) => (
1654
+ <SelectItem
1655
+ key={option.value}
1656
+ value={option.value}
1657
+ >
1658
+ {option.label}
1659
+ </SelectItem>
1660
+ ))}
1661
+ </SelectContent>
1662
+ </SelectRoot>
1663
+ </FormItem>
1664
+
1665
+ <FormItem>
1666
+ <FormLabel>
1667
+ {t("deals:createDeal.goalValue")}
1668
+ </FormLabel>
1669
+ <FormControl>
1670
+ <Input
1671
+ type="number"
1672
+ min={0}
1673
+ placeholder={t(
1674
+ "deals:createDeal.enterGoalValue",
1675
+ )}
1676
+ {...register("goalValue" as any, {
1677
+ valueAsNumber: true,
1678
+ })}
1679
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
1680
+ />
1681
+ </FormControl>
1682
+ </FormItem>
1683
+ </div>
1684
+ </div>
1685
+
1686
+ {/* Non-Billable for Traditional deals */}
1687
+ <FormItem>
1688
+ <FormLabel>{t("deals:createDeal.nonBillable")}</FormLabel>
1689
+ <div className="flex items-center gap-3 h-10">
1690
+ <Checkbox
1691
+ id="nonBillableDirect"
1692
+ checked={isNonBillable}
1693
+ onChange={(e) => setIsNonBillable(e.target.checked)}
1694
+ />
1695
+ <label
1696
+ htmlFor="nonBillableDirect"
1697
+ className="text-sm text-mw-neutral-700 dark:text-mw-neutral-300 cursor-pointer"
1698
+ >
1699
+ {t("deals:createDeal.nonBillableDescription")}
1700
+ </label>
1701
+ </div>
1702
+ </FormItem>
1703
+ </>
1704
+ )}
1705
+ </div>
1706
+
1707
+ {mode === "PROGRAMMATIC" && (
1708
+ <div className="space-y-6">
1709
+ {/* Row 1: Programmatic Type + Non-Billable */}
1710
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1711
+ <FormItem>
1712
+ <FormLabel required>
1713
+ {t("deals:createDeal.programmaticType")}
1714
+ </FormLabel>
1715
+ <SelectRoot
1716
+ value={watch("dealType" as any) || ""}
1717
+ onValueChange={handleDealTypeChange}
1718
+ disabled={isDealTypeLocked || isEditMode}
1719
+ >
1720
+ <SelectTrigger className={cn(
1721
+ "border-mw-neutral-200 dark:border-mw-neutral-700",
1722
+ (isDealTypeLocked || isEditMode) && "bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed"
1723
+ )}>
1724
+ <SelectValue
1725
+ placeholder={t(
1726
+ "deals:createDeal.selectDealType",
1727
+ )}
1728
+ />
1729
+ </SelectTrigger>
1730
+ <SelectContent>
1731
+ {DEAL_TYPE_OPTIONS.map((option) => (
1732
+ <SelectItem
1733
+ key={option.value}
1734
+ value={option.value}
1735
+ >
1736
+ {option.label}
1737
+ </SelectItem>
1738
+ ))}
1739
+ </SelectContent>
1740
+ </SelectRoot>
1741
+ </FormItem>
1742
+
1743
+ <FormItem>
1744
+ <FormLabel>{t("deals:createDeal.nonBillable")}</FormLabel>
1745
+ <div className="flex items-center gap-3 h-10">
1746
+ <Checkbox
1747
+ id="nonBillable"
1748
+ checked={isNonBillable}
1749
+ onChange={(e) => setIsNonBillable(e.target.checked)}
1750
+ />
1751
+ <label
1752
+ htmlFor="nonBillable"
1753
+ className="text-sm text-mw-neutral-700 dark:text-mw-neutral-300 cursor-pointer"
1754
+ >
1755
+ {t("deals:createDeal.nonBillableDescription")}
1756
+ </label>
1757
+ </div>
1758
+ </FormItem>
1759
+ </div>
1760
+
1761
+ {/* Row 2: Hard Stop (conditional - GUARANTEED only) */}
1762
+ {showHardStop && (
1763
+ <FormItem>
1764
+ <FormLabel>{t("deals:createDeal.hardStop")}</FormLabel>
1765
+ <div className="flex items-center gap-3 h-10">
1766
+ <Checkbox
1767
+ id="hardStop"
1768
+ checked={isHardStop}
1769
+ onChange={(e) => setIsHardStop(e.target.checked)}
1770
+ />
1771
+ <label
1772
+ htmlFor="hardStop"
1773
+ className="text-sm text-mw-neutral-700 dark:text-mw-neutral-300 cursor-pointer"
1774
+ >
1775
+ {t("deals:createDeal.hardStopDescription")}
1776
+ </label>
1777
+ </div>
1778
+ </FormItem>
1779
+ )}
1780
+
1781
+ {/* Row 3: Auction Type (conditional - PRIVATE_AUCTION only) */}
1782
+ {showAuctionType && (
1783
+ <FormItem>
1784
+ <FormLabel required>
1785
+ {t("deals:createDeal.auctionType")}
1786
+ </FormLabel>
1787
+ <SelectRoot
1788
+ value={String(watch("auctionType" as any) || "")}
1789
+ onValueChange={(value) =>
1790
+ setValue("auctionType" as any, parseInt(value))
1791
+ }
1792
+ >
1793
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1794
+ <SelectValue
1795
+ placeholder={t("deals:createDeal.selectAuctionType")}
1796
+ />
1797
+ </SelectTrigger>
1798
+ <SelectContent>
1799
+ {AUCTION_TYPE_OPTIONS.map((option) => (
1800
+ <SelectItem
1801
+ key={option.value}
1802
+ value={option.value}
1803
+ >
1804
+ {option.label}
1805
+ </SelectItem>
1806
+ ))}
1807
+ </SelectContent>
1808
+ </SelectRoot>
1809
+ </FormItem>
1810
+ )}
1811
+
1812
+ {/* Row 4: DSP dropdown (centralized) */}
1813
+ <FormItem>
1814
+ <FormLabel required>
1815
+ {t("deals:createDeal.dsp")}
1816
+ </FormLabel>
1817
+ <SelectRoot
1818
+ value={activeDspId}
1819
+ onValueChange={handleDspChange}
1820
+ disabled={!isDspEnabled || isDspLocked}
1821
+ >
1822
+ <SelectTrigger className={cn(
1823
+ "border-mw-neutral-200 dark:border-mw-neutral-700",
1824
+ (!isDspEnabled || isDspLocked) && "bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed"
1825
+ )}>
1826
+ <SelectValue placeholder="Select DSP" />
1827
+ </SelectTrigger>
1828
+ <SelectContent>
1829
+ {dspOptions.map((option) => (
1830
+ <SelectItem key={option.id} value={option.id}>
1831
+ {option.name}
1832
+ </SelectItem>
1833
+ ))}
1834
+ </SelectContent>
1835
+ </SelectRoot>
1836
+ {!isDspEnabled && (
1837
+ <p className="text-xs text-mw-neutral-500 mt-1">
1838
+ Select a deal type first
1839
+ </p>
1840
+ )}
1841
+ </FormItem>
1842
+
1843
+ {/* Row 5: Buyer Name dropdown + Buyer Panel */}
1844
+ <FormItem>
1845
+ <FormLabel required>
1846
+ {t("deals:createDeal.buyers")}
1847
+ </FormLabel>
1848
+ <div className="space-y-3">
1849
+ <SelectRoot
1850
+ value=""
1851
+ onValueChange={handleAddBuyer}
1852
+ disabled={!isBuyerEnabled || (!allowMultipleBuyers && buyers.filter(b => b.dsp && b.seatId && b.buyerName).length >= 1)}
1853
+ >
1854
+ <SelectTrigger className={cn(
1855
+ "border-mw-neutral-200 dark:border-mw-neutral-700",
1856
+ (!isBuyerEnabled || (!allowMultipleBuyers && buyers.filter(b => b.dsp && b.seatId && b.buyerName).length >= 1)) && "bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed"
1857
+ )}>
1858
+ <SelectValue placeholder={t("deals:createDeal.buyerName")} />
1859
+ </SelectTrigger>
1860
+ <SelectContent>
1861
+ {buyersForDsp.map((b) => (
1862
+ <SelectItem key={b.id} value={b.id}>
1863
+ {b.seatName || b.name} {b.seatId ? `(${b.seatId})` : ""}
1864
+ </SelectItem>
1865
+ ))}
1866
+ </SelectContent>
1867
+ </SelectRoot>
1868
+ {!isBuyerEnabled && (
1869
+ <p className="text-xs text-mw-neutral-500">
1870
+ Select a DSP first
1871
+ </p>
1872
+ )}
1873
+
1874
+ {/* Buyer Panel - chips/badges */}
1875
+ <div className="flex flex-wrap gap-2 min-h-[36px] p-2 rounded-md border border-mw-neutral-200 dark:border-mw-neutral-700 bg-mw-neutral-50 dark:bg-mw-neutral-800/50">
1876
+ {buyers.filter(b => b.dsp && b.seatId && b.buyerName).length === 0 ? (
1877
+ <span className="text-sm text-mw-neutral-400 italic">No buyer selected yet</span>
1878
+ ) : (
1879
+ buyers.filter(b => b.dsp && b.seatId && b.buyerName).map((buyer, index) => (
1880
+ <Badge
1881
+ key={index}
1882
+ variant="secondary"
1883
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm"
1884
+ >
1885
+ <span>{buyer.buyerName} - Seat ID ({buyer.seatId})</span>
1886
+ <Button
1887
+ type="button"
1888
+ variant="ghost"
1889
+ size="sm"
1890
+ isIconOnly
1891
+ onClick={() => handleRemoveBuyer(index)}
1892
+ className="ml-1"
1893
+ >
1894
+ <X className="h-3 w-3" />
1895
+ </Button>
1896
+ </Badge>
1897
+ ))
1898
+ )}
1899
+ </div>
1900
+ </div>
1901
+ </FormItem>
1902
+
1903
+ {/* Row 6: Transaction Type + Currency */}
1904
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1905
+ <FormItem>
1906
+ <FormLabel required>
1907
+ {t("deals:createDeal.transactionType")}
1908
+ </FormLabel>
1909
+ <SelectRoot
1910
+ value={selectedTransactionType || watch("transactionType" as any) || ""}
1911
+ onValueChange={handleTransactionTypeChange}
1912
+ disabled={!isTransactionTypeEnabled}
1913
+ >
1914
+ <SelectTrigger className={cn(
1915
+ "border-mw-neutral-200 dark:border-mw-neutral-700",
1916
+ !isTransactionTypeEnabled && "bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed"
1917
+ )}>
1918
+ <SelectValue
1919
+ placeholder={t("deals:createDeal.selectTransactionType")}
1920
+ />
1921
+ </SelectTrigger>
1922
+ <SelectContent>
1923
+ {TRANSACTION_TYPE_OPTIONS.map((option) => (
1924
+ <SelectItem
1925
+ key={option.value}
1926
+ value={option.value}
1927
+ >
1928
+ {option.label}
1929
+ </SelectItem>
1930
+ ))}
1931
+ </SelectContent>
1932
+ </SelectRoot>
1933
+ </FormItem>
1934
+
1935
+ <FormItem>
1936
+ <FormLabel>
1937
+ {t("deals:deals.currency")}
1938
+ </FormLabel>
1939
+ <SelectRoot
1940
+ value={watch("currency" as any) || ""}
1941
+ onValueChange={(value) =>
1942
+ setValue("currency" as any, value)
1943
+ }
1944
+ disabled={!isCurrencyEnabled}
1945
+ >
1946
+ <SelectTrigger className={cn(
1947
+ "border-mw-neutral-200 dark:border-mw-neutral-700",
1948
+ !isCurrencyEnabled && "bg-mw-neutral-50 dark:bg-mw-neutral-800 cursor-not-allowed"
1949
+ )}>
1950
+ <SelectValue
1951
+ placeholder={t("deals:createDeal.selectCurrency")}
1952
+ />
1953
+ </SelectTrigger>
1954
+ <SelectContent>
1955
+ {CURRENCIES.map((option) => (
1956
+ <SelectItem
1957
+ key={option.value}
1958
+ value={option.value}
1959
+ >
1960
+ {option.label}
1961
+ </SelectItem>
1962
+ ))}
1963
+ </SelectContent>
1964
+ </SelectRoot>
1965
+ </FormItem>
1966
+ </div>
1967
+
1968
+ {/* Row 7: Impression Multiplier Type (conditional - AUDIENCE only) */}
1969
+ {showImpMultiplier && (
1970
+ <FormItem>
1971
+ <FormLabel>Impression Multiplier Type</FormLabel>
1972
+ <SelectRoot
1973
+ value={impMultiplierType || ""}
1974
+ onValueChange={(value) => setImpMultiplierType(value)}
1975
+ >
1976
+ <SelectTrigger className="border-mw-neutral-200 dark:border-mw-neutral-700">
1977
+ <SelectValue placeholder="Select impression multiplier type" />
1978
+ </SelectTrigger>
1979
+ <SelectContent>
1980
+ {IMPRESSION_MULTIPLIER_OPTIONS.map((option) => (
1981
+ <SelectItem
1982
+ key={option.value}
1983
+ value={option.value}
1984
+ >
1985
+ {option.label}
1986
+ </SelectItem>
1987
+ ))}
1988
+ </SelectContent>
1989
+ </SelectRoot>
1990
+ </FormItem>
1991
+ )}
1992
+
1993
+ {/* Row 8: Seller Section */}
1994
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1995
+ <FormItem>
1996
+ <FormLabel>Seller</FormLabel>
1997
+ <Input
1998
+ value={sellerName || user?.name || ""}
1999
+ onChange={(e) => setSellerName(e.target.value)}
2000
+ placeholder="Seller name"
2001
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
2002
+ />
2003
+ </FormItem>
2004
+ <FormItem>
2005
+ <FormLabel>Seller Contact Number</FormLabel>
2006
+ <Input
2007
+ value={sellerPhone}
2008
+ onChange={(e) => setSellerPhone(e.target.value)}
2009
+ placeholder="Enter contact number"
2010
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
2011
+ />
2012
+ </FormItem>
2013
+ <FormItem>
2014
+ <FormLabel>Seller Email</FormLabel>
2015
+ <Input
2016
+ value={sellerEmail || user?.email || ""}
2017
+ onChange={(e) => setSellerEmail(e.target.value)}
2018
+ placeholder="Enter seller email"
2019
+ className="border-mw-neutral-200 dark:border-mw-neutral-700"
2020
+ />
2021
+ </FormItem>
2022
+ </div>
2023
+
2024
+ </div>
2025
+ )}
2026
+ </form>
2027
+ </CardContent>
2028
+ </Card>
2029
+ </div>
2030
+
2031
+ <div className="lg:col-span-1">
2032
+ <div className="sticky top-6 space-y-4">
2033
+ <Card className="border-mw-neutral-200 dark:border-mw-neutral-700">
2034
+ <CardContent className="p-4">
2035
+ <div className="flex items-center gap-2 mb-3">
2036
+ <Lightbulb className="h-4 w-4 text-amber-500" />
2037
+ <h3 className="text-sm font-medium text-mw-neutral-900 dark:text-white">
2038
+ {SECTION_TIPS[visibleSection].title}
2039
+ </h3>
2040
+ </div>
2041
+ <ul className="space-y-2">
2042
+ {SECTION_TIPS[visibleSection].tips.map((tip, index) => (
2043
+ <li key={index} className="flex items-start gap-2 text-sm text-mw-neutral-600 dark:text-mw-neutral-400">
2044
+ <span className="text-amber-500 mt-0.5 flex-shrink-0">•</span>
2045
+ <span>{tip}</span>
2046
+ </li>
2047
+ ))}
2048
+ </ul>
2049
+ </CardContent>
2050
+ </Card>
2051
+
2052
+ <MarketInsightsPanel
2053
+ selectedCountries={watch("country" as any) ? [watch("country" as any)] : []}
2054
+ currency={watch("currency" as any) || "USD"}
2055
+ />
2056
+ </div>
2057
+ </div>
2058
+ </div>
2059
+ </div>
2060
+ </div>
2061
+
2062
+ {/* Sticky Footer */}
2063
+ <div className="sticky bottom-0 z-20 bg-white dark:bg-mw-neutral-900 border-t border-mw-neutral-200 dark:border-mw-neutral-700 px-6 py-4">
2064
+ <div className="w-full flex justify-end gap-3">
2065
+ {isEditMode && (
2066
+ <Button
2067
+ type="button"
2068
+ variant="outline"
2069
+ onClick={() => setLocation(`/deals/${dealId}/line-items`)}
2070
+ className="px-6"
2071
+ >
2072
+ {t("deals:createDeal.cancel")}
2073
+ </Button>
2074
+ )}
2075
+ <Button
2076
+ type="submit"
2077
+ form="deal-form"
2078
+ disabled={isMutating}
2079
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white px-8"
2080
+ >
2081
+ {isMutating
2082
+ ? isEditMode
2083
+ ? t("deals:createDeal.updating")
2084
+ : t("deals:createDeal.creating")
2085
+ : isEditMode
2086
+ ? t("deals:updateDeal.title")
2087
+ : t("deals:deals.createDeal")}
2088
+ </Button>
2089
+ </div>
2090
+ </div>
2091
+ </div>
2092
+ );
2093
+ }