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,1207 @@
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import { useParams, useLocation } from "wouter";
3
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { Button, cn } from "@moving-walls/design-system";
5
+ import { Card, CardContent } from "@moving-walls/design-system";
6
+ import { Badge } from "@moving-walls/design-system";
7
+ import {
8
+ ArrowLeft,
9
+ Check,
10
+ ChevronLeft,
11
+ ChevronRight,
12
+ Loader2,
13
+ } from "lucide-react";
14
+ import { useToast } from "@/hooks/use-toast";
15
+
16
+ const ENABLE_POI_IN_API = true;
17
+ import {
18
+ InfluenceDealsAPI,
19
+ influenceDealsRequest,
20
+ generateExternalId,
21
+ formatAPIErrorForToast,
22
+ mapInventoryForAPI,
23
+ mapLineItemForAPI,
24
+ type Deal,
25
+ type LineItem,
26
+ } from "@/lib/influence-deals-api";
27
+ import { LineItemDetailsStep } from "@/components/line-items/steps/line-item-details-step";
28
+ import { TargetingStep } from "@/components/line-items/steps/targeting-step";
29
+ import { InventoryStep } from "@/components/line-items/steps/inventory-step";
30
+ import { ScheduleStep } from "@/components/line-items/steps/schedule-step";
31
+ import { CreativesStep } from "@/components/line-items/steps/creatives-step";
32
+ import { SummaryStep } from "@/components/line-items/steps/summary-step";
33
+ import { usePageTitle } from "@/hooks/use-page-title";
34
+
35
+ interface LineItemFormData {
36
+ details: {
37
+ name: string;
38
+ startDate: string;
39
+ endDate: string;
40
+ currency: string;
41
+ creativeType?: "DISPLAY" | "VIDEO" | "AUDIO";
42
+ duration?: number;
43
+ impressions?: number;
44
+ netCostPerDay?: number;
45
+ direct?: {
46
+ budgetSetup: {
47
+ budgetType: string;
48
+ budgetAmount?: number;
49
+ currency: string;
50
+ };
51
+ campaignGoal: {
52
+ type: string;
53
+ targetValue: number;
54
+ };
55
+ pacing?: {
56
+ type: string;
57
+ dailyCap?: number;
58
+ hourlyCap?: number;
59
+ };
60
+ };
61
+ programmatic?: {
62
+ bidFloor?: number;
63
+ netCost?: number;
64
+ impressions?: number;
65
+ impMultiplier?: number;
66
+ };
67
+ };
68
+ targeting: {
69
+ targeting?: {
70
+ demographics?: {
71
+ ageGroups?: string[];
72
+ genders?: string[];
73
+ incomeGroups?: string[];
74
+ interests?: string[];
75
+ audienceBehaviour?: string[];
76
+ };
77
+ venueTypes?: string[];
78
+ geofencing?: {
79
+ geometrics?: any[];
80
+ locations?: any[];
81
+ radiusKm?: number;
82
+ pois?: string[];
83
+ };
84
+ };
85
+ deliveryTargeting?: {
86
+ signals?: Record<string, any>;
87
+ };
88
+ recommendationRunId?: string;
89
+ };
90
+ inventory: {
91
+ inventories: string[];
92
+ inventoryObjects?: any[];
93
+ planning?: {
94
+ capacity?: {
95
+ campaignDays?: number;
96
+ available?: { slots?: number; playTimeSec?: number; maxImpressions?: number };
97
+ };
98
+ allocation?: { slots?: number; playTimeSec?: number; sov?: number; sot?: number };
99
+ estimates?: { impressions?: number; reach?: number; frequency?: number };
100
+ pricing?: { cpm?: number; estimatedCost?: number };
101
+ };
102
+ };
103
+ schedule: {
104
+ scheduleGrids?: Record<string, any>;
105
+ schedulePreset?: string;
106
+ scheduleSelection?: string;
107
+ scheduleData?: any;
108
+ apiSchedules?: any[];
109
+ };
110
+ creatives: Array<{
111
+ creativeId: string;
112
+ creativeUri: string;
113
+ creativeType: "DISPLAY" | "VIDEO" | "AUDIO";
114
+ resolution?: string;
115
+ thumbnail?: string;
116
+ mimeType?: string;
117
+ duration?: number;
118
+ inventoryIds?: string[];
119
+ metadata?: Record<string, any>;
120
+ }>;
121
+ }
122
+
123
+ const initialFormData: LineItemFormData = {
124
+ details: {
125
+ name: "",
126
+ startDate: "",
127
+ endDate: "",
128
+ currency: "USD",
129
+ direct: {
130
+ budgetSetup: {
131
+ budgetType: "TOTAL",
132
+ budgetAmount: undefined,
133
+ currency: "USD",
134
+ },
135
+ campaignGoal: {
136
+ type: "IMPRESSIONS",
137
+ targetValue: 0,
138
+ },
139
+ pacing: {
140
+ type: "even",
141
+ },
142
+ },
143
+ programmatic: {
144
+ bidFloor: undefined,
145
+ netCost: undefined,
146
+ impressions: undefined,
147
+ impMultiplier: undefined,
148
+ },
149
+ },
150
+ targeting: {},
151
+ inventory: {
152
+ inventories: [],
153
+ inventoryObjects: [],
154
+ planning: undefined,
155
+ },
156
+ schedule: {
157
+ scheduleGrids: {},
158
+ schedulePreset: "custom",
159
+ },
160
+ creatives: [],
161
+ };
162
+
163
+ interface Step {
164
+ id: number;
165
+ key: keyof LineItemFormData | "creatives" | "summary";
166
+ label: string;
167
+ description: string;
168
+ optional?: boolean;
169
+ formId?: string;
170
+ }
171
+
172
+ const steps: Step[] = [
173
+ {
174
+ id: 1,
175
+ key: "details",
176
+ label: "Line Item Details",
177
+ description: "Basic information and pricing",
178
+ formId: "line-item-details-form",
179
+ },
180
+ {
181
+ id: 2,
182
+ key: "targeting",
183
+ label: "Targeting",
184
+ description: "Audience and location targeting",
185
+ formId: "targeting-step-form",
186
+ },
187
+ {
188
+ id: 3,
189
+ key: "inventory",
190
+ label: "Inventory",
191
+ description: "Select screens and packages",
192
+ formId: "inventory-step-form",
193
+ },
194
+ {
195
+ id: 4,
196
+ key: "schedule",
197
+ label: "Schedule",
198
+ description: "Configure schedule and forecasting",
199
+ formId: "schedule-step-form",
200
+ },
201
+ {
202
+ id: 5,
203
+ key: "creatives",
204
+ label: "Creatives",
205
+ description: "Assign creative assets",
206
+ optional: true,
207
+ formId: "creatives-step-form",
208
+ },
209
+ {
210
+ id: 6,
211
+ key: "summary",
212
+ label: "Summary",
213
+ description: "Review and submit",
214
+ optional: true,
215
+ formId: "summary-step-form",
216
+ },
217
+ ];
218
+
219
+ export default function LineItemWizardPage() {
220
+ usePageTitle("Line Item");
221
+ const { dealId, lineItemId: routeLineItemId } = useParams<{ dealId: string; lineItemId?: string }>();
222
+ const [location, setLocation] = useLocation();
223
+ const queryClient = useQueryClient();
224
+
225
+ // Check if we're on the /new route by looking at the actual path
226
+ const isNewRoute = location.includes('/line-items/new');
227
+ const lineItemId = isNewRoute ? undefined : routeLineItemId;
228
+ const isEditMode = !isNewRoute && !!lineItemId;
229
+
230
+ // Check for plannerRestricted mode (only pacing and creatives are editable)
231
+ // Use window.location.search since wouter's location doesn't include query params
232
+ const isPlannerRestricted = typeof window !== 'undefined' && window.location.search.includes('plannerRestricted=true');
233
+
234
+ console.log('📍 Route check - location:', location, 'search:', window.location.search, 'isPlannerRestricted:', isPlannerRestricted);
235
+
236
+ const [currentStep, setCurrentStep] = useState(1);
237
+ const [completedSteps, setCompletedSteps] = useState<number[]>([]);
238
+ const [formData, setFormData] = useState<LineItemFormData>(initialFormData);
239
+ const [isSaving, setIsSaving] = useState(false);
240
+ const [savedLineItemId, setSavedLineItemId] = useState<string | undefined>(lineItemId);
241
+ const [externalId, setExternalId] = useState<string | undefined>();
242
+ const totalSteps = steps.length;
243
+ const { toast } = useToast();
244
+
245
+ const { data: dealData, isLoading: isLoadingDeal } = useQuery({
246
+ queryKey: ["deal", dealId],
247
+ queryFn: async () => {
248
+ const response = await influenceDealsRequest<Deal>(
249
+ InfluenceDealsAPI.deals.get(dealId!)
250
+ );
251
+ return response;
252
+ },
253
+ enabled: !!dealId,
254
+ });
255
+
256
+ const { data: existingLineItem, isLoading: isLoadingLineItem } = useQuery({
257
+ queryKey: ["lineItem", dealId, lineItemId],
258
+ queryFn: async () => {
259
+ const response = await influenceDealsRequest<any>(
260
+ InfluenceDealsAPI.lineItems.get(dealId!, lineItemId!)
261
+ );
262
+ return response;
263
+ },
264
+ enabled: !!dealId && !!lineItemId,
265
+ });
266
+
267
+ // Helper function to fetch all inventories with pagination
268
+ const fetchAllInventories = async (dealId: string, lineItemId: string) => {
269
+ const allInventories: any[] = [];
270
+ let page = 1;
271
+ const limit = 50;
272
+ let totalPages = 1;
273
+
274
+ do {
275
+ const response = await influenceDealsRequest<any>(
276
+ InfluenceDealsAPI.lineItems.inventories(dealId, lineItemId, { page, limit })
277
+ );
278
+
279
+ // Handle response structure with data array and pagination
280
+ const data = response?.data || response || [];
281
+ const pagination = response?.pagination;
282
+
283
+ if (Array.isArray(data)) {
284
+ allInventories.push(...data);
285
+ }
286
+
287
+ if (pagination?.totalPages) {
288
+ totalPages = pagination.totalPages;
289
+ } else {
290
+ // If no pagination info, assume single page
291
+ totalPages = 1;
292
+ }
293
+
294
+ page++;
295
+ } while (page <= totalPages);
296
+
297
+ console.log('📦 Fetched all inventories (paginated):', allInventories.length, 'items');
298
+ return allInventories;
299
+ };
300
+
301
+ // Fetch line item inventories from dedicated endpoint with pagination
302
+ const { data: lineItemInventories } = useQuery({
303
+ queryKey: ["lineItemInventories", dealId, lineItemId],
304
+ queryFn: async () => {
305
+ const inventories = await fetchAllInventories(dealId!, lineItemId!);
306
+ console.log('📦 Fetched line item inventories:', inventories);
307
+ return inventories;
308
+ },
309
+ enabled: !!dealId && !!lineItemId,
310
+ });
311
+
312
+ // Reset form when creating a new line item (not edit mode) or when deal changes
313
+ useEffect(() => {
314
+ console.log('🔄 Reset check - lineItemId:', lineItemId, 'isEditMode:', isEditMode);
315
+ if (!lineItemId) {
316
+ console.log('🔄 Resetting form for new line item');
317
+ setFormData(initialFormData);
318
+ setCurrentStep(1);
319
+ setCompletedSteps([]);
320
+ setSavedLineItemId(undefined);
321
+ }
322
+ }, [dealId, lineItemId, isEditMode]);
323
+
324
+ useEffect(() => {
325
+ // Only load existing line item data if we have a lineItemId (edit mode)
326
+ if (!lineItemId) {
327
+ return;
328
+ }
329
+ if (existingLineItem && isEditMode) {
330
+ const li = existingLineItem as any;
331
+ console.log('📦 Loading existing line item:', li);
332
+ // Store the external ID for use in payload building during updates
333
+ if (li.externalId) {
334
+ setExternalId(li.externalId);
335
+ }
336
+ // Pacing can be at root level or in direct object
337
+ const pacingData = li.pacing || li.direct?.pacing || { type: "asap" };
338
+ console.log('📦 Pacing from API:', pacingData);
339
+
340
+ // Get inventories from the dedicated endpoint response (already fetched with pagination)
341
+ const inventories = lineItemInventories || [];
342
+ console.log('📦 Inventories from dedicated endpoint:', inventories);
343
+
344
+ // Map inventories with all required fields for edit mode
345
+ const inventoryObjects = Array.isArray(inventories)
346
+ ? inventories.map((inv: any) => ({
347
+ id: inv.id || inv.inventoryId,
348
+ inventoryId: inv.inventoryId || inv.id,
349
+ name: inv.name || inv.screen?.name || 'Unknown',
350
+ resolution: inv.resolution || inv.screen?.resolution || '1920x1080',
351
+ width: inv.width || inv.screen?.width,
352
+ height: inv.height || inv.screen?.height,
353
+ location: inv.location || inv.screen?.location,
354
+ publisher: inv.publisher,
355
+ publisherName: inv.publisherName || inv.publisher?.name,
356
+ tags: inv.tags || [],
357
+ operatingHours: inv.operatingHours,
358
+ estimatedCostPerDay: inv.estimatedCostPerDay || inv.planning?.estimatedCostPerDay,
359
+ currency: inv.currency || li.currency || 'USD',
360
+ screen: inv.screen,
361
+ ...inv,
362
+ }))
363
+ : [];
364
+
365
+ // Extract inventory IDs
366
+ const inventoryIds = inventoryObjects.map((inv: any) => inv.id || inv.inventoryId);
367
+
368
+ setFormData((prev) => ({
369
+ details: {
370
+ name: li.name || "",
371
+ startDate: li.startDate || "",
372
+ endDate: li.endDate || "",
373
+ currency: li.currency || "USD",
374
+ creativeType: li.creativeType || "VIDEO",
375
+ impressions: li.impressions,
376
+ netCostPerDay: li.netCostPerDay,
377
+ direct: {
378
+ budgetSetup: li.direct?.budgetSetup || {
379
+ budgetType: "TOTAL",
380
+ budgetAmount: undefined,
381
+ currency: "USD",
382
+ },
383
+ campaignGoal: li.direct?.campaignGoal || {
384
+ type: "IMPRESSIONS",
385
+ targetValue: 0,
386
+ },
387
+ pacing: pacingData,
388
+ },
389
+ },
390
+ targeting: {
391
+ targeting: li.targeting || undefined,
392
+ deliveryTargeting: li.deliveryTargeting || undefined,
393
+ // Preserve recommendationRunId from current session if API doesn't have it
394
+ recommendationRunId: li.recommendationRunId || prev.targeting.recommendationRunId || undefined,
395
+ },
396
+ inventory: {
397
+ inventories: inventoryIds,
398
+ inventoryObjects: inventoryObjects,
399
+ },
400
+ schedule: {
401
+ scheduleGrids: li.scheduleGrids || {},
402
+ schedulePreset: li.schedulePreset || "custom",
403
+ // Convert existing schedule from API to scheduleData format
404
+ scheduleData: li.schedule && Array.isArray(li.schedule) && li.schedule.length > 0
405
+ ? {
406
+ schedules: li.schedule.map((s: any, idx: number) => ({
407
+ id: s.id || `schedule-${Date.now()}-${idx}`,
408
+ type: s.type || "DEFAULT",
409
+ validity: {
410
+ startDate: s.validity?.startDate || li.startDate,
411
+ endDate: s.validity?.endDate || li.endDate,
412
+ },
413
+ hours: s.hours || [{ start: 7, end: 23 }],
414
+ priority: s.priority ?? 10,
415
+ daysOfWeek: s.daysOfWeek || [1, 2, 3, 4, 5, 6, 7],
416
+ date: s.date,
417
+ name: s.name || `Schedule ${idx + 1}`,
418
+ }))
419
+ }
420
+ : undefined,
421
+ apiSchedules: li.schedule || [],
422
+ },
423
+ creatives: li.creatives || [],
424
+ }));
425
+ }
426
+ }, [existingLineItem, lineItemInventories, isEditMode, lineItemId]);
427
+
428
+ const handlePrevious = useCallback(() => {
429
+ if (currentStep > 1) {
430
+ setCurrentStep((prev) => prev - 1);
431
+ }
432
+ }, [currentStep]);
433
+
434
+ const handleNext = useCallback(() => {
435
+ if (currentStep < totalSteps) {
436
+ setCompletedSteps((prev) =>
437
+ prev.includes(currentStep) ? prev : [...prev, currentStep]
438
+ );
439
+ setCurrentStep((prev) => prev + 1);
440
+ }
441
+ }, [currentStep, totalSteps]);
442
+
443
+ const handleStepClick = (stepId: number) => {
444
+ if (stepId <= currentStep || completedSteps.includes(stepId - 1)) {
445
+ setCurrentStep(stepId);
446
+ }
447
+ };
448
+
449
+ const buildLineItemPayload = useCallback((isUpdate: boolean = false) => {
450
+ const mode = dealData?.mode || "DIRECT";
451
+ const dealType = dealData?.dealType || "DIRECT";
452
+
453
+ // Prepare targeting data with geofencing cleanup
454
+ let targeting = undefined;
455
+ if (formData.targeting?.targeting) {
456
+ targeting = { ...formData.targeting.targeting };
457
+ if (targeting.geofencing) {
458
+ // Remove radiusKm and pois from top level - only geometrics and locations are allowed
459
+ const { radiusKm, pois, ...cleanGeofencing } = targeting.geofencing;
460
+ // Also strip pois from each geometry unless the API supports it
461
+ if (!ENABLE_POI_IN_API && cleanGeofencing.geometrics) {
462
+ cleanGeofencing.geometrics = cleanGeofencing.geometrics.map((geom: any) => {
463
+ const { pois: geomPois, ...cleanGeom } = geom;
464
+ return cleanGeom;
465
+ });
466
+ }
467
+ targeting.geofencing = cleanGeofencing;
468
+ }
469
+ }
470
+
471
+ // Prepare deliveryTargeting (DIRECT mode only)
472
+ // Note: timeOfDay is not yet supported by the backend API, so we filter it out
473
+ let deliveryTargeting = undefined;
474
+ if (mode === "DIRECT" && formData.targeting?.deliveryTargeting && Object.keys(formData.targeting.deliveryTargeting).length > 0) {
475
+ deliveryTargeting = { ...formData.targeting.deliveryTargeting };
476
+ if (deliveryTargeting.signals) {
477
+ const { timeOfDay, ...supportedSignals } = deliveryTargeting.signals;
478
+ if (Object.keys(supportedSignals).length > 0) {
479
+ deliveryTargeting.signals = supportedSignals;
480
+ } else {
481
+ deliveryTargeting = undefined;
482
+ }
483
+ }
484
+ }
485
+
486
+ // Prepare pacing with lowercase type
487
+ let pacing = undefined;
488
+ if (formData.details.direct?.pacing) {
489
+ pacing = {
490
+ ...formData.details.direct.pacing,
491
+ type: formData.details.direct.pacing.type?.toLowerCase() || 'asap',
492
+ };
493
+ }
494
+
495
+ // Prepare the line item data for mapping
496
+ const lineItemData: any = {
497
+ name: formData.details.name,
498
+ status: "GENERATED",
499
+ startDate: formData.details.startDate,
500
+ endDate: formData.details.endDate,
501
+ currency: formData.details.currency,
502
+ pacing: pacing,
503
+ targeting: targeting,
504
+ deliveryTargeting: deliveryTargeting,
505
+ direct: formData.details.direct,
506
+ programmatic: formData.details.programmatic,
507
+ planning: formData.inventory.planning,
508
+ creativeType: formData.details.creativeType || "VIDEO",
509
+ duration: formData.details.duration || 10,
510
+ resolutions: ["1920x1080", "1280x720", "3840x2160", "4096x2160"],
511
+ inventories: formData.inventory.inventories || [],
512
+ };
513
+
514
+ // Add schedule data if available
515
+ if (formData.schedule.apiSchedules && formData.schedule.apiSchedules.length > 0) {
516
+ lineItemData.schedule = formData.schedule.apiSchedules;
517
+ }
518
+
519
+ // For programmatic mode, add impressions and netCostPerDay
520
+ if (mode === "PROGRAMMATIC" || mode?.toUpperCase() === "PROGRAMMATIC") {
521
+ if (formData.details.impressions) {
522
+ lineItemData.impressions = formData.details.impressions;
523
+ }
524
+ if (formData.details.netCostPerDay) {
525
+ lineItemData.netCostPerDay = formData.details.netCostPerDay;
526
+ }
527
+ }
528
+
529
+ // Use the mode-aware mapping function
530
+ const mappedLineItem = mapLineItemForAPI(
531
+ lineItemData,
532
+ mode,
533
+ dealType,
534
+ );
535
+
536
+ return mappedLineItem;
537
+ }, [formData, dealData]);
538
+
539
+ const handleSave = useCallback(async (creativesToSave?: any[]) => {
540
+ setIsSaving(true);
541
+ try {
542
+ // If line item was already saved during inventory step, just navigate away
543
+ // Creatives are already assigned via drag-drop in edit mode
544
+ const effectiveLineItemId = savedLineItemId || lineItemId;
545
+
546
+ let finalLineItemId = effectiveLineItemId;
547
+
548
+ if (effectiveLineItemId) {
549
+ // Line item already exists, update it if needed
550
+ const payload = buildLineItemPayload(true);
551
+ await influenceDealsRequest(
552
+ InfluenceDealsAPI.lineItems.update(dealId!, effectiveLineItemId),
553
+ "PUT",
554
+ payload
555
+ );
556
+ } else {
557
+ // Create new line item (shouldn't happen as we save after inventory step)
558
+ const payload = buildLineItemPayload(false);
559
+ const response = await influenceDealsRequest<any>(
560
+ InfluenceDealsAPI.lineItems.create(dealId!),
561
+ "POST",
562
+ payload
563
+ );
564
+ finalLineItemId = response?.id || response?.data?.id;
565
+ }
566
+
567
+ // Update inventories via separate endpoint
568
+ if (finalLineItemId && formData.inventory.inventoryObjects && formData.inventory.inventoryObjects.length > 0) {
569
+ try {
570
+ // Format inventory objects for the API - include all available fields from JAD API
571
+ const inventoryPayload = formData.inventory.inventoryObjects.map((inv: any) => mapInventoryForAPI(inv));
572
+
573
+ await influenceDealsRequest(
574
+ InfluenceDealsAPI.lineItems.updateInventories(dealId!, finalLineItemId),
575
+ "PUT",
576
+ { inventories: inventoryPayload }
577
+ );
578
+ console.log('📦 Successfully updated inventories via separate endpoint');
579
+ } catch (inventoryError: unknown) {
580
+ console.error("Failed to update inventories:", inventoryError);
581
+ // Re-throw to prevent save completion - toast handled by caller
582
+ throw inventoryError;
583
+ }
584
+ }
585
+
586
+ toast({
587
+ title: "Success",
588
+ description: `Successfully ${effectiveLineItemId ? 'updated' : 'created'} the line item "${formData.details.name}"`,
589
+ });
590
+
591
+ queryClient.invalidateQueries({ queryKey: ["lineItems", dealId] });
592
+ queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
593
+ // Invalidate individual line item cache
594
+ if (effectiveLineItemId) {
595
+ queryClient.invalidateQueries({ queryKey: ["lineItem", dealId, effectiveLineItemId] });
596
+ queryClient.invalidateQueries({ queryKey: ["lineItemInventories", dealId, effectiveLineItemId] });
597
+ }
598
+ // Clear form data after successful save
599
+ setFormData(initialFormData);
600
+ setCurrentStep(1);
601
+ setCompletedSteps([]);
602
+ setSavedLineItemId(undefined);
603
+ setLocation(`/deals/${dealId}/line-items`);
604
+ } catch (error) {
605
+ console.error("Failed to save line item:", error);
606
+ toast({
607
+ title: "Error",
608
+ description: formatAPIErrorForToast(error),
609
+ variant: "destructive",
610
+ });
611
+ } finally {
612
+ setIsSaving(false);
613
+ }
614
+ }, [buildLineItemPayload, dealId, lineItemId, savedLineItemId, dealData, queryClient, setLocation, toast, formData.details.name]);
615
+
616
+ // Save line item without creatives (used after schedule step to ensure resolutions are saved)
617
+ const saveLineItemOnly = useCallback(async (scheduleData: any) => {
618
+ setIsSaving(true);
619
+ try {
620
+ // Build payload with the new schedule data
621
+ const updatedFormData = {
622
+ ...formData,
623
+ schedule: scheduleData,
624
+ };
625
+
626
+ const isDirectDeal = dealData?.mode === "DIRECT";
627
+ const isGuaranteed = dealData?.dealType === "GUARANTEED";
628
+ const payload: any = {
629
+ name: updatedFormData.details.name,
630
+ status: "GENERATED",
631
+ };
632
+
633
+ const isCreatingNew = !isEditMode && !savedLineItemId;
634
+ if (isCreatingNew) {
635
+ payload.externalId = generateExternalId();
636
+ payload.startDate = updatedFormData.details.startDate;
637
+ payload.endDate = updatedFormData.details.endDate;
638
+ payload.currency = updatedFormData.details.currency;
639
+ }
640
+
641
+ // Add thresholdCountPerDay for GUARANTEED deals (required by API)
642
+ if (isGuaranteed) {
643
+ payload.thresholdCountPerDay = updatedFormData.details.thresholdCountPerDay || 1;
644
+ }
645
+
646
+ // Pacing is always at root level for both direct and programmatic
647
+ // Ensure pacing type is lowercase as required by API
648
+ if (updatedFormData.details.direct?.pacing) {
649
+ payload.pacing = {
650
+ ...updatedFormData.details.direct.pacing,
651
+ type: updatedFormData.details.direct.pacing.type?.toLowerCase() || 'asap',
652
+ };
653
+ }
654
+
655
+ if (isDirectDeal) {
656
+ payload.direct = {
657
+ budgetSetup: updatedFormData.details.direct?.budgetSetup,
658
+ campaignGoal: updatedFormData.details.direct?.campaignGoal,
659
+ };
660
+ } else {
661
+ if (updatedFormData.details.impressions) {
662
+ payload.impressions = updatedFormData.details.impressions;
663
+ }
664
+ if (updatedFormData.details.netCostPerDay) {
665
+ payload.netCostPerDay = updatedFormData.details.netCostPerDay;
666
+ }
667
+ }
668
+
669
+ if (updatedFormData.targeting?.targeting) {
670
+ const targeting = { ...updatedFormData.targeting.targeting };
671
+ if (targeting.geofencing) {
672
+ // Remove radiusKm and pois from top level - only geometrics and locations are allowed
673
+ const { radiusKm, pois, ...cleanGeofencing } = targeting.geofencing;
674
+ // Also strip pois from each geometry unless the API supports it
675
+ if (!ENABLE_POI_IN_API && cleanGeofencing.geometrics) {
676
+ cleanGeofencing.geometrics = cleanGeofencing.geometrics.map((geom: any) => {
677
+ const { pois: geomPois, ...cleanGeom } = geom;
678
+ return cleanGeom;
679
+ });
680
+ }
681
+ targeting.geofencing = cleanGeofencing;
682
+ }
683
+ payload.targeting = targeting;
684
+ }
685
+
686
+ // Include deliveryTargeting for signals (weather, traffic, aqi)
687
+ // Note: timeOfDay is not yet supported by the backend API, so we filter it out
688
+ if (updatedFormData.targeting?.deliveryTargeting && Object.keys(updatedFormData.targeting.deliveryTargeting).length > 0) {
689
+ const deliveryTargeting = { ...updatedFormData.targeting.deliveryTargeting };
690
+ if (deliveryTargeting.signals) {
691
+ const { timeOfDay, ...supportedSignals } = deliveryTargeting.signals;
692
+ if (Object.keys(supportedSignals).length > 0) {
693
+ deliveryTargeting.signals = supportedSignals;
694
+ payload.deliveryTargeting = deliveryTargeting;
695
+ }
696
+ } else {
697
+ payload.deliveryTargeting = deliveryTargeting;
698
+ }
699
+ }
700
+
701
+ // Use hardcoded standard resolutions for line item payload
702
+ payload.resolutions = ["1920x1080", "1280x720", "3840x2160", "4096x2160"];
703
+
704
+ // Set creativeType from form data (default to VIDEO)
705
+ payload.creativeType = updatedFormData.details.creativeType || "VIDEO";
706
+
707
+ // Set duration from form data (default to 10 seconds)
708
+ payload.duration = updatedFormData.details.duration || 10;
709
+
710
+ // Add schedule data if available
711
+ if (scheduleData.apiSchedules && scheduleData.apiSchedules.length > 0) {
712
+ payload.schedule = scheduleData.apiSchedules;
713
+ }
714
+
715
+ let newLineItemId = savedLineItemId || lineItemId;
716
+
717
+ if (isEditMode && lineItemId) {
718
+ await influenceDealsRequest(
719
+ InfluenceDealsAPI.lineItems.update(dealId!, lineItemId),
720
+ "PUT",
721
+ payload
722
+ );
723
+ newLineItemId = lineItemId;
724
+ } else if (savedLineItemId) {
725
+ // Update existing saved line item
726
+ await influenceDealsRequest(
727
+ InfluenceDealsAPI.lineItems.update(dealId!, savedLineItemId),
728
+ "PUT",
729
+ payload
730
+ );
731
+ newLineItemId = savedLineItemId;
732
+ } else {
733
+ // Create new line item
734
+ const response = await influenceDealsRequest<any>(
735
+ InfluenceDealsAPI.lineItems.create(dealId!),
736
+ "POST",
737
+ payload
738
+ );
739
+ newLineItemId = response?.id || response?.data?.id;
740
+ setSavedLineItemId(newLineItemId);
741
+ }
742
+
743
+ // Update inventories via separate endpoint
744
+ if (newLineItemId && formData.inventory.inventoryObjects && formData.inventory.inventoryObjects.length > 0) {
745
+ try {
746
+ // Format inventory objects for the API - include all available fields from JAD API
747
+ const inventoryPayload = formData.inventory.inventoryObjects.map((inv: any) => mapInventoryForAPI(inv));
748
+
749
+ await influenceDealsRequest(
750
+ InfluenceDealsAPI.lineItems.updateInventories(dealId!, newLineItemId),
751
+ "PUT",
752
+ { inventories: inventoryPayload }
753
+ );
754
+ console.log('📦 Successfully updated inventories via separate endpoint');
755
+ } catch (inventoryError: unknown) {
756
+ console.error("Failed to update inventories:", inventoryError);
757
+ // Re-throw to prevent step advancement - toast handled by caller
758
+ throw inventoryError;
759
+ }
760
+ }
761
+
762
+ toast({
763
+ title: "Success",
764
+ description: `Successfully ${isEditMode || savedLineItemId ? 'updated' : 'created'} the line item "${updatedFormData.details.name}"`,
765
+ });
766
+
767
+ queryClient.invalidateQueries({ queryKey: ["lineItems", dealId] });
768
+ queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
769
+ queryClient.invalidateQueries({ queryKey: ["lineItem", dealId, newLineItemId] });
770
+ queryClient.invalidateQueries({ queryKey: ["lineItemInventories", dealId, newLineItemId] });
771
+
772
+ // Update deal status to GENERATED when completing the first line item
773
+ // Only do this for new line items (not edit mode) and when deal is still in DRAFT status
774
+ // Note: dealData?.status === "DRAFT" check prevents duplicate updates
775
+ if (!isEditMode && dealData?.status === "DRAFT") {
776
+ try {
777
+ console.log('[LineItemWizard] Line item completed, updating deal status to GENERATED');
778
+ await influenceDealsRequest(
779
+ InfluenceDealsAPI.deals.update(dealId!),
780
+ "PUT",
781
+ { status: "GENERATED" }
782
+ );
783
+ queryClient.invalidateQueries({ queryKey: ["deal", dealId] });
784
+ } catch (statusError) {
785
+ console.error("Failed to update deal status to GENERATED:", statusError);
786
+ }
787
+ }
788
+
789
+ return newLineItemId;
790
+ } catch (error) {
791
+ console.error("Failed to save line item:", error);
792
+ throw error;
793
+ } finally {
794
+ setIsSaving(false);
795
+ }
796
+ }, [formData, dealData, dealId, lineItemId, isEditMode, savedLineItemId, queryClient, toast]);
797
+
798
+ // Create initial line item after details step (for new line items only)
799
+ // This ensures we have a lineItemId for the recommendation engine in the targeting step
800
+ const createInitialLineItem = useCallback(async (detailsData: any) => {
801
+ if (isEditMode || savedLineItemId || lineItemId) {
802
+ // Already have a line item, no need to create
803
+ return;
804
+ }
805
+
806
+ setIsSaving(true);
807
+ try {
808
+ const isDirectDeal = dealData?.mode === "DIRECT";
809
+ const isGuaranteed = dealData?.dealType === "GUARANTEED";
810
+ const payload: any = {
811
+ name: detailsData.name,
812
+ status: "GENERATED",
813
+ externalId: generateExternalId(),
814
+ startDate: detailsData.startDate,
815
+ endDate: detailsData.endDate,
816
+ currency: detailsData.currency,
817
+ creativeType: detailsData.creativeType || "VIDEO",
818
+ duration: detailsData.duration || 10,
819
+ resolutions: ["1920x1080", "1280x720", "3840x2160", "4096x2160"],
820
+ };
821
+
822
+ // Add thresholdCountPerDay for GUARANTEED deals (required by API)
823
+ if (isGuaranteed) {
824
+ payload.thresholdCountPerDay = detailsData.thresholdCountPerDay || 1;
825
+ }
826
+
827
+ // Pacing at root level
828
+ if (detailsData.direct?.pacing) {
829
+ payload.pacing = {
830
+ ...detailsData.direct.pacing,
831
+ type: detailsData.direct.pacing.type?.toLowerCase() || 'asap',
832
+ };
833
+ }
834
+
835
+ if (isDirectDeal) {
836
+ payload.direct = {
837
+ budgetSetup: detailsData.direct?.budgetSetup,
838
+ campaignGoal: detailsData.direct?.campaignGoal,
839
+ };
840
+ } else {
841
+ if (detailsData.impressions) {
842
+ payload.impressions = detailsData.impressions;
843
+ }
844
+ if (detailsData.netCostPerDay) {
845
+ payload.netCostPerDay = detailsData.netCostPerDay;
846
+ }
847
+ }
848
+
849
+ console.log("🆕 Creating initial line item for recommendation engine:", payload);
850
+ const response = await influenceDealsRequest<any>(
851
+ InfluenceDealsAPI.lineItems.create(dealId!),
852
+ "POST",
853
+ payload
854
+ );
855
+ const newLineItemId = response?.id || response?.data?.id;
856
+ console.log("✅ Initial line item created:", newLineItemId);
857
+ setSavedLineItemId(newLineItemId);
858
+ } catch (error) {
859
+ console.error("Failed to create initial line item:", error);
860
+ // Don't block the user - they can still proceed, just without recommendations
861
+ toast({
862
+ title: "Note",
863
+ description: "Could not initialize line item for AI recommendations. You can still select inventory manually.",
864
+ variant: "default",
865
+ });
866
+ } finally {
867
+ setIsSaving(false);
868
+ }
869
+ }, [isEditMode, savedLineItemId, lineItemId, dealData, dealId, toast]);
870
+
871
+ const handleStepComplete = useCallback(async (stepKey: string, data: any) => {
872
+ let shouldProceed = true;
873
+
874
+ if (stepKey === "details") {
875
+ setFormData((prev) => ({
876
+ ...prev,
877
+ details: data,
878
+ }));
879
+
880
+ // Create initial line item for new line items so targeting step has an ID for recommendations
881
+ await createInitialLineItem(data);
882
+ } else if (stepKey === "targeting") {
883
+ console.log('[Wizard] Targeting step completed with data:', data);
884
+ console.log('[Wizard] recommendationRunId:', data?.recommendationRunId);
885
+ setFormData((prev) => ({
886
+ ...prev,
887
+ targeting: data,
888
+ }));
889
+ } else if (stepKey === "inventory") {
890
+ setFormData((prev) => ({
891
+ ...prev,
892
+ inventory: data,
893
+ }));
894
+ } else if (stepKey === "schedule") {
895
+ setFormData((prev) => ({
896
+ ...prev,
897
+ schedule: data,
898
+ }));
899
+
900
+ // Save line item when completing schedule step to ensure resolutions are saved
901
+ try {
902
+ await saveLineItemOnly(data);
903
+ } catch (error: unknown) {
904
+ console.error("Failed to save line item after schedule step:", error);
905
+ toast({
906
+ title: "Error",
907
+ description: formatAPIErrorForToast(error),
908
+ variant: "destructive",
909
+ });
910
+ // Stay on current step - don't proceed
911
+ shouldProceed = false;
912
+ }
913
+ } else if (stepKey === "creatives") {
914
+ setFormData((prev) => ({
915
+ ...prev,
916
+ creatives: data,
917
+ }));
918
+ }
919
+
920
+ // Only proceed to next step if no errors occurred
921
+ if (!shouldProceed) {
922
+ return;
923
+ }
924
+
925
+ if (currentStep < totalSteps) {
926
+ setCompletedSteps((prev) =>
927
+ prev.includes(currentStep) ? prev : [...prev, currentStep]
928
+ );
929
+ setCurrentStep((prev) => prev + 1);
930
+ } else {
931
+ // Pass creatives data directly to handleSave to avoid React state async issue
932
+ handleSave(stepKey === "creatives" ? data : undefined);
933
+ }
934
+ }, [currentStep, totalSteps, handleSave, saveLineItemOnly, createInitialLineItem, toast]);
935
+
936
+ const handleCancel = () => {
937
+ setLocation(`/deals/${dealId}/line-items`);
938
+ };
939
+
940
+ const renderStepContent = () => {
941
+ const currentStepData = steps[currentStep - 1];
942
+
943
+ switch (currentStepData.key) {
944
+ case "details":
945
+ return (
946
+ <LineItemDetailsStep
947
+ data={formData.details}
948
+ dealData={dealData}
949
+ onComplete={(data) => handleStepComplete("details", data)}
950
+ isLoading={isSaving}
951
+ formId={currentStepData.formId}
952
+ isPlannerRestricted={isPlannerRestricted}
953
+ dealMode={dealData?.mode}
954
+ dealType={dealData?.dealType}
955
+ />
956
+ );
957
+ case "targeting":
958
+ return (
959
+ <TargetingStep
960
+ data={formData.targeting}
961
+ campaignData={dealData}
962
+ lineItemId={savedLineItemId || lineItemId}
963
+ lineItemData={formData.details}
964
+ onComplete={(data) => handleStepComplete("targeting", data)}
965
+ onBack={handlePrevious}
966
+ isLoading={isSaving}
967
+ formId={currentStepData.formId}
968
+ isPlannerRestricted={isPlannerRestricted}
969
+ dealMode={dealData?.mode}
970
+ />
971
+ );
972
+ case "inventory":
973
+ return (
974
+ <InventoryStep
975
+ data={formData.inventory}
976
+ lineItemData={formData.details}
977
+ lineItemId={savedLineItemId || lineItemId}
978
+ targeting={formData.targeting}
979
+ recommendationRunId={formData.targeting.recommendationRunId}
980
+ country={dealData?.country || "JP"}
981
+ onComplete={(data) => handleStepComplete("inventory", data)}
982
+ onBack={handlePrevious}
983
+ isLoading={isSaving}
984
+ formId={currentStepData.formId}
985
+ isPlannerRestricted={isPlannerRestricted}
986
+ dealMode={dealData?.mode}
987
+ />
988
+ );
989
+ case "schedule":
990
+ return (
991
+ <ScheduleStep
992
+ data={formData.inventory}
993
+ lineItemData={formData.details}
994
+ scheduleData={formData.schedule.scheduleData}
995
+ onComplete={(data) => handleStepComplete("schedule", data)}
996
+ onBack={handlePrevious}
997
+ isLoading={isSaving}
998
+ formId={currentStepData.formId}
999
+ isPlannerRestricted={isPlannerRestricted}
1000
+ />
1001
+ );
1002
+ case "creatives":
1003
+ return (
1004
+ <CreativesStep
1005
+ data={formData.creatives}
1006
+ lineItemId={savedLineItemId || lineItemId}
1007
+ lineItemName={formData.details.name || "Line Item 01"}
1008
+ dealId={dealId}
1009
+ dealDealId={dealData?.dealId}
1010
+ allowedResolutions={["1920x1080", "1280x720", "3840x2160", "4096x2160"]}
1011
+ creativeType={formData.details.creativeType || "VIDEO"}
1012
+ isEditMode={!!(savedLineItemId || lineItemId)}
1013
+ onComplete={(data) => handleStepComplete("creatives", data)}
1014
+ onBack={handlePrevious}
1015
+ onSkip={() => handleSave()}
1016
+ isLoading={isSaving}
1017
+ formId={currentStepData.formId}
1018
+ />
1019
+ );
1020
+ case "summary":
1021
+ return (
1022
+ <SummaryStep
1023
+ formData={formData}
1024
+ dealData={dealData}
1025
+ dealId={dealId}
1026
+ dealDealId={dealData?.dealId}
1027
+ isEditMode={!!(savedLineItemId || lineItemId)}
1028
+ />
1029
+ );
1030
+ default:
1031
+ return null;
1032
+ }
1033
+ };
1034
+
1035
+ const isLoading = isLoadingDeal || (isEditMode && isLoadingLineItem);
1036
+
1037
+ if (isLoading) {
1038
+ return (
1039
+ <div className="min-h-screen flex items-center justify-center">
1040
+ <Loader2 className="h-8 w-8 animate-spin text-mw-primary-600" />
1041
+ </div>
1042
+ );
1043
+ }
1044
+
1045
+ const dealName = typeof dealData?.name === "string" ? dealData.name : (dealData?.name as any)?.name || "Deal";
1046
+
1047
+ return (
1048
+ <div className="h-[calc(100vh-64px)] bg-mw-gray-50 dark:bg-mw-gray-900 flex flex-col overflow-hidden">
1049
+ {/* Title Header */}
1050
+ <div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-b border-mw-gray-200 dark:border-mw-gray-700 px-6 py-3">
1051
+ <div className="flex items-center gap-2">
1052
+ <Button
1053
+ variant="ghost"
1054
+ size="sm"
1055
+ isIconOnly
1056
+ onClick={handleCancel}
1057
+ >
1058
+ <ArrowLeft className="h-4 w-4" />
1059
+ </Button>
1060
+ <div>
1061
+ <div className="flex items-center gap-2">
1062
+ <h1 className="text-lg font-semibold text-mw-neutral-900 dark:text-white">
1063
+ {isEditMode ? "Edit Line Item" : "Add Line Item"}
1064
+ </h1>
1065
+ {isPlannerRestricted && (
1066
+ <span className="text-xs px-2 py-0.5 bg-purple-100 text-purple-700 rounded-full font-medium">
1067
+ Pacing & Creatives Only
1068
+ </span>
1069
+ )}
1070
+ </div>
1071
+ <p className="text-sm font-normal leading-normal text-mw-neutral-500 dark:text-mw-neutral-400">
1072
+ {isPlannerRestricted
1073
+ ? "Only pacing and creatives can be modified for this Planner deal."
1074
+ : "Follow the steps below to set up your line item."}
1075
+ </p>
1076
+ </div>
1077
+ </div>
1078
+ </div>
1079
+
1080
+ {/* Stepper - fixed at top of content area */}
1081
+ <div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-b border-mw-gray-100 dark:border-mw-gray-800">
1082
+ <div className="w-full px-6 py-4">
1083
+ <div className="flex items-start justify-between">
1084
+ {steps.map((step, index) => {
1085
+ const isActive = currentStep === step.id;
1086
+ const isCompleted = completedSteps.includes(step.id);
1087
+ const isAccessible = step.id <= currentStep || completedSteps.includes(step.id - 1);
1088
+
1089
+ return (
1090
+ <div key={step.id} className="flex items-center flex-1">
1091
+ <Button
1092
+ variant="ghost"
1093
+ size="sm"
1094
+ onClick={() => handleStepClick(step.id)}
1095
+ disabled={!isAccessible}
1096
+ className={cn(
1097
+ "-mx-2 -my-1",
1098
+ isAccessible && "hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-800",
1099
+ !isAccessible && "opacity-50"
1100
+ )}
1101
+ >
1102
+ {isCompleted ? (
1103
+ <div className="flex items-center justify-center w-8 h-8 rounded-full shrink-0 bg-mw-success-600 text-white">
1104
+ <Check className="h-4 w-4" />
1105
+ </div>
1106
+ ) : isActive ? (
1107
+ <div className="flex items-center justify-center w-8 h-8 rounded-full shrink-0 bg-mw-primary-500 text-white text-sm font-medium">
1108
+ {step.id}
1109
+ </div>
1110
+ ) : (
1111
+ <span
1112
+ className={cn(
1113
+ "flex items-center justify-center w-8 h-8 text-sm font-medium shrink-0 text-mw-neutral-500 dark:text-mw-neutral-400"
1114
+ )}
1115
+ >
1116
+ {step.id}
1117
+ </span>
1118
+ )}
1119
+ <div className="text-left">
1120
+ <p
1121
+ className={cn(
1122
+ "text-sm font-semibold whitespace-nowrap",
1123
+ isCompleted && "text-mw-success-800 dark:text-mw-success-400",
1124
+ isActive && !isCompleted && "text-mw-neutral-900 dark:text-mw-neutral-100",
1125
+ !isActive && !isCompleted && "text-mw-neutral-500 dark:text-mw-neutral-400"
1126
+ )}
1127
+ >
1128
+ {step.label}
1129
+ </p>
1130
+ {step.optional && (
1131
+ <p className="text-xs text-mw-neutral-400 dark:text-mw-neutral-500">
1132
+ Optional
1133
+ </p>
1134
+ )}
1135
+ </div>
1136
+ </Button>
1137
+ {index < steps.length - 1 && (
1138
+ <div className="flex-1 mx-4 border-t-2 border-dashed border-mw-gray-200 dark:border-mw-gray-700" />
1139
+ )}
1140
+ </div>
1141
+ );
1142
+ })}
1143
+ </div>
1144
+ </div>
1145
+ </div>
1146
+
1147
+ {/* Content Area - scrollable */}
1148
+ <div className="flex-1 overflow-y-auto">
1149
+ <div className="w-full px-6 py-6">
1150
+ <div className="bg-white dark:bg-mw-gray-800 rounded-xl border border-mw-gray-200 dark:border-mw-gray-700 p-6">
1151
+ {renderStepContent()}
1152
+ </div>
1153
+ </div>
1154
+ </div>
1155
+
1156
+ {/* Fixed Footer with Navigation Buttons */}
1157
+ <div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-t border-mw-gray-200 dark:border-mw-gray-700 px-6 py-4">
1158
+ <div className="flex justify-between items-center">
1159
+ <div>
1160
+ {currentStep > 1 && (
1161
+ <Button
1162
+ type="button"
1163
+ variant="outline"
1164
+ onClick={handlePrevious}
1165
+ className="border border-mw-neutral-200 dark:border-mw-neutral-700 text-mw-neutral-700 dark:text-mw-neutral-300 hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-800 px-5 py-2"
1166
+ >
1167
+ <ChevronLeft className="mr-1.5 h-4 w-4" />
1168
+ Previous Step
1169
+ </Button>
1170
+ )}
1171
+ </div>
1172
+ <div className="flex items-center gap-3">
1173
+ {currentStep === totalSteps ? (
1174
+ <Button
1175
+ type="button"
1176
+ onClick={() => setLocation(`/deals/${dealId}/line-items`)}
1177
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white px-5 py-2"
1178
+ >
1179
+ Finish
1180
+ </Button>
1181
+ ) : (
1182
+ <Button
1183
+ type="submit"
1184
+ form={steps[currentStep - 1].formId}
1185
+ variant="outline"
1186
+ disabled={isSaving}
1187
+ className="border border-mw-primary-300 dark:border-mw-primary-600 text-mw-primary-600 dark:text-mw-primary-400 hover:bg-mw-primary-50 dark:hover:bg-mw-primary-900/20 px-5 py-2"
1188
+ >
1189
+ {isSaving ? (
1190
+ <>
1191
+ <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
1192
+ Saving...
1193
+ </>
1194
+ ) : (
1195
+ <>
1196
+ Next Step
1197
+ <ChevronRight className="ml-1.5 h-4 w-4" />
1198
+ </>
1199
+ )}
1200
+ </Button>
1201
+ )}
1202
+ </div>
1203
+ </div>
1204
+ </div>
1205
+ </div>
1206
+ );
1207
+ }