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,1062 @@
1
+ import { useState, useMemo } from "react";
2
+ import { useQuery, useMutation } from "@tanstack/react-query";
3
+ import { useLocation, useParams } from "wouter";
4
+ import {
5
+ ArrowLeft,
6
+ Plus,
7
+ Search,
8
+ Filter,
9
+ MoreHorizontal,
10
+ Trash2,
11
+ Edit,
12
+ FileText,
13
+ PlayCircle,
14
+ Eye,
15
+ DollarSign,
16
+ Wallet,
17
+ AlertTriangle,
18
+ History,
19
+ Copy,
20
+ Send,
21
+ RotateCcw,
22
+ Lock,
23
+ Info,
24
+ Sparkles,
25
+ Loader2,
26
+ Link2,
27
+ Download,
28
+ FileCode,
29
+ Package,
30
+ Check,
31
+ ClipboardCopy,
32
+ } from "lucide-react";
33
+ import { Button } from "@/components/ui/button";
34
+ import { Input } from "@/components/ui/input";
35
+ import { Badge } from "@/components/ui/badge";
36
+ import { Card, CardContent, CardHeader } from "@/components/ui/card";
37
+ import {
38
+ DropdownMenu,
39
+ DropdownMenuContent,
40
+ DropdownMenuItem,
41
+ DropdownMenuTrigger,
42
+ } from "@/components/ui/dropdown-menu";
43
+ import {
44
+ AlertDialog,
45
+ AlertDialogAction,
46
+ AlertDialogCancel,
47
+ AlertDialogContent,
48
+ AlertDialogDescription,
49
+ AlertDialogFooter,
50
+ AlertDialogHeader,
51
+ AlertDialogTitle,
52
+ } from "@/components/ui/alert-dialog";
53
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
54
+ import { PageHeader } from "@/components/page-header";
55
+ import { DataTable } from "@/components/data-table";
56
+ import { MetricCard } from "@/components/metric-card";
57
+ import { StatusBadge } from "@/components/status-badge";
58
+ import { FilterDrawer, FilterField, FilterValues } from "@/components/filter-drawer";
59
+ import { HistoryDrawer } from "@/components/history-drawer";
60
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
61
+ import { useToast } from "@/hooks/use-toast";
62
+ import { apiRequest, queryClient } from "@/lib/queryClient";
63
+ import { formatDate, formatCurrency } from "@/lib/utils";
64
+ import type { Deal, SspPartner, MediaOwner, LineItem } from "@shared/schema";
65
+
66
+ const STATUS_OPTIONS = [
67
+ { value: "active", label: "Active" },
68
+ { value: "paused", label: "Paused" },
69
+ ];
70
+
71
+ const initialFilterValues: FilterValues = {
72
+ status: [],
73
+ mediaOwner: [],
74
+ dateRange: { start: undefined, end: undefined },
75
+ budgetRange: { min: undefined, max: undefined },
76
+ };
77
+
78
+ export default function DealDetail() {
79
+ const { id: dealId } = useParams<{ id: string }>();
80
+ const [, setLocation] = useLocation();
81
+ const [searchQuery, setSearchQuery] = useState("");
82
+ const [sortBy, setSortBy] = useState<string>("name");
83
+ const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
84
+ const [filterOpen, setFilterOpen] = useState(false);
85
+ const [filterValues, setFilterValues] = useState<FilterValues>(initialFilterValues);
86
+ const [appliedFilters, setAppliedFilters] = useState<FilterValues>(initialFilterValues);
87
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
88
+ const [lineItemToDelete, setLineItemToDelete] = useState<LineItem | null>(null);
89
+ const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
90
+ const [optimizeDialogOpen, setOptimizeDialogOpen] = useState(false);
91
+ const [lineItemToOptimize, setLineItemToOptimize] = useState<LineItem | null>(null);
92
+ const [copiedId, setCopiedId] = useState<string | null>(null);
93
+ const { toast } = useToast();
94
+
95
+ const { data: deal, isLoading: dealLoading } = useQuery<Deal>({
96
+ queryKey: ["/api/deals", dealId],
97
+ enabled: !!dealId,
98
+ });
99
+
100
+ const { data: lineItems, isLoading: lineItemsLoading } = useQuery<LineItem[]>({
101
+ queryKey: ["/api/deals", dealId, "line-items"],
102
+ enabled: !!dealId,
103
+ });
104
+
105
+ const { data: sspPartners } = useQuery<SspPartner[]>({
106
+ queryKey: ["/api/ssp-partners"],
107
+ });
108
+
109
+ const { data: mediaOwners } = useQuery<MediaOwner[]>({
110
+ queryKey: ["/api/media-owners"],
111
+ });
112
+
113
+ interface DistributionLineItem {
114
+ lineItemId: string;
115
+ lineItemName: string;
116
+ status: string;
117
+ creativeType: string | null;
118
+ vastUrl: string;
119
+ }
120
+
121
+ interface DistributionData {
122
+ dealId: string;
123
+ dealName: string;
124
+ dealType: string;
125
+ lineItems: DistributionLineItem[];
126
+ }
127
+
128
+ const { data: distributionData, isLoading: distributionLoading } = useQuery<DistributionData>({
129
+ queryKey: ["/api/deals", dealId, "distribution"],
130
+ enabled: !!dealId && !!deal?.acceptanceSent,
131
+ });
132
+
133
+ const handleCopyVastUrl = (url: string, lineItemId: string) => {
134
+ navigator.clipboard.writeText(url).then(() => {
135
+ setCopiedId(lineItemId);
136
+ setTimeout(() => setCopiedId(null), 2000);
137
+ toast({ title: "Copied", description: "VAST URL copied to clipboard" });
138
+ });
139
+ };
140
+
141
+ const handleDownloadHtml = (lineItemId: string) => {
142
+ window.open(`/api/deals/${dealId}/line-items/${lineItemId}/vast-tag/html`, "_blank");
143
+ };
144
+
145
+ const handleDownloadZip = (lineItemId: string) => {
146
+ window.open(`/api/deals/${dealId}/line-items/${lineItemId}/vast-tag/zip`, "_blank");
147
+ };
148
+
149
+ const handleDownloadAll = () => {
150
+ window.open(`/api/deals/${dealId}/distribution/download-all`, "_blank");
151
+ };
152
+
153
+ const isProgrammatic = useMemo(() => {
154
+ if (!deal) return false;
155
+ return ['pg', 'pd', 'pmp', 'always_on'].includes(deal.dealType || '');
156
+ }, [deal]);
157
+
158
+ const mediaOwnerOptions = useMemo(() =>
159
+ (mediaOwners ?? []).map(mo => ({ value: mo.id, label: mo.name })),
160
+ [mediaOwners]
161
+ );
162
+
163
+ const filterFields: FilterField[] = [
164
+ {
165
+ key: "status",
166
+ label: "Status",
167
+ type: "multi-select",
168
+ options: STATUS_OPTIONS,
169
+ },
170
+ {
171
+ key: "mediaOwner",
172
+ label: "Media Owner",
173
+ type: "multi-select",
174
+ options: mediaOwnerOptions,
175
+ },
176
+ {
177
+ key: "dateRange",
178
+ label: "Flight Date Range",
179
+ type: "date-range",
180
+ },
181
+ {
182
+ key: "budgetRange",
183
+ label: "Budget Range",
184
+ type: "number-range",
185
+ minPlaceholder: "Min budget",
186
+ maxPlaceholder: "Max budget",
187
+ },
188
+ ];
189
+
190
+ const deleteMutation = useMutation({
191
+ mutationFn: async (id: string) => {
192
+ return apiRequest("DELETE", `/api/line-items/${id}`);
193
+ },
194
+ onSuccess: () => {
195
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId, "line-items"] });
196
+ queryClient.invalidateQueries({ queryKey: ["/api/line-items"] });
197
+ toast({
198
+ title: "Line item deleted",
199
+ description: "The line item has been deleted.",
200
+ });
201
+ setDeleteDialogOpen(false);
202
+ setLineItemToDelete(null);
203
+ },
204
+ });
205
+
206
+ const duplicateLineItemMutation = useMutation({
207
+ mutationFn: async (id: string) => {
208
+ return apiRequest("POST", `/api/line-items/${id}/duplicate`);
209
+ },
210
+ onSuccess: () => {
211
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId, "line-items"] });
212
+ queryClient.invalidateQueries({ queryKey: ["/api/line-items"] });
213
+ toast({
214
+ title: "Line item duplicated",
215
+ description: "A copy of the line item has been created.",
216
+ });
217
+ },
218
+ });
219
+
220
+ const autoOptimizeMutation = useMutation({
221
+ mutationFn: async (id: string) => {
222
+ return apiRequest("POST", `/api/line-items/${id}/auto-optimize`, {
223
+ maxInventories: 5,
224
+ });
225
+ },
226
+ onSuccess: async (response) => {
227
+ const result = await response.json();
228
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId, "line-items"] });
229
+ queryClient.invalidateQueries({ queryKey: ["/api/line-items"] });
230
+ setOptimizeDialogOpen(false);
231
+ setLineItemToOptimize(null);
232
+ toast({
233
+ title: "Optimization Complete",
234
+ description: `Added ${result.summary?.inventoriesAdded || 0} recommended inventories`,
235
+ });
236
+ },
237
+ onError: () => {
238
+ setOptimizeDialogOpen(false);
239
+ setLineItemToOptimize(null);
240
+ toast({
241
+ title: "Optimization Failed",
242
+ description: "Failed to auto-optimize line item. Please try again.",
243
+ variant: "destructive",
244
+ });
245
+ },
246
+ });
247
+
248
+ const handleOptimizeClick = (lineItem: LineItem) => {
249
+ setLineItemToOptimize(lineItem);
250
+ setOptimizeDialogOpen(true);
251
+ };
252
+
253
+ const handleConfirmOptimize = () => {
254
+ if (lineItemToOptimize) {
255
+ autoOptimizeMutation.mutate(lineItemToOptimize.id);
256
+ }
257
+ };
258
+
259
+ const handleDeleteClick = (lineItem: LineItem) => {
260
+ setLineItemToDelete(lineItem);
261
+ setDeleteDialogOpen(true);
262
+ };
263
+
264
+ const handleConfirmDelete = () => {
265
+ if (lineItemToDelete) {
266
+ deleteMutation.mutate(lineItemToDelete.id);
267
+ }
268
+ };
269
+
270
+ const handleApplyFilters = () => {
271
+ setAppliedFilters(filterValues);
272
+ setFilterOpen(false);
273
+ };
274
+
275
+ const handleClearFilters = () => {
276
+ setFilterValues(initialFilterValues);
277
+ setAppliedFilters(initialFilterValues);
278
+ setFilterOpen(false);
279
+ };
280
+
281
+ const requestAcceptanceMutation = useMutation({
282
+ mutationFn: async () => {
283
+ return apiRequest("PATCH", `/api/deals/${dealId}`, { acceptanceSent: true });
284
+ },
285
+ onSuccess: () => {
286
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId] });
287
+ toast({
288
+ title: "Request Sent",
289
+ description: "Acceptance request has been sent to DSP partners.",
290
+ });
291
+ },
292
+ });
293
+
294
+ const activateDealMutation = useMutation({
295
+ mutationFn: async () => {
296
+ return apiRequest("PATCH", `/api/deals/${dealId}`, { acceptanceSent: true });
297
+ },
298
+ onSuccess: () => {
299
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId] });
300
+ toast({
301
+ title: "Deal Activated",
302
+ description: "Deal has been sent to CMS for activation.",
303
+ });
304
+ },
305
+ });
306
+
307
+ const reopenDealMutation = useMutation({
308
+ mutationFn: async () => {
309
+ return apiRequest("PATCH", `/api/deals/${dealId}`, { reopened: true });
310
+ },
311
+ onSuccess: () => {
312
+ queryClient.invalidateQueries({ queryKey: ["/api/deals", dealId] });
313
+ toast({
314
+ title: "Deal Reopened",
315
+ description: "You can now make changes. Limited editing is available after reopening.",
316
+ });
317
+ },
318
+ });
319
+
320
+ const handleRequestAcceptance = () => {
321
+ requestAcceptanceMutation.mutate();
322
+ };
323
+
324
+ const handleActivateDeal = () => {
325
+ activateDealMutation.mutate();
326
+ };
327
+
328
+ const handleReopenDeal = () => {
329
+ reopenDealMutation.mutate();
330
+ };
331
+
332
+ // Check if deal is locked (sent but not reopened)
333
+ const isDealLocked = deal?.acceptanceSent && !deal?.reopened;
334
+
335
+ // Check if deal can be edited (not sent, or sent and reopened)
336
+ const canEditDeal = !deal?.acceptanceSent || deal?.reopened;
337
+
338
+ const activeFilterCount = useMemo(() => {
339
+ let count = 0;
340
+ const statuses = appliedFilters.status as string[];
341
+ const mediaOwnerIds = appliedFilters.mediaOwner as string[];
342
+ const dateRange = appliedFilters.dateRange as { start?: Date; end?: Date };
343
+ const budgetRange = appliedFilters.budgetRange as { min?: number; max?: number };
344
+
345
+ if (statuses?.length > 0) count++;
346
+ if (mediaOwnerIds?.length > 0) count++;
347
+ if (dateRange?.start || dateRange?.end) count++;
348
+ if (budgetRange?.min !== undefined || budgetRange?.max !== undefined) count++;
349
+
350
+ return count;
351
+ }, [appliedFilters]);
352
+
353
+ const handleSort = (key: string) => {
354
+ if (sortBy === key) {
355
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
356
+ } else {
357
+ setSortBy(key);
358
+ setSortOrder("asc");
359
+ }
360
+ };
361
+
362
+ const filteredLineItems = useMemo(() => {
363
+ if (!lineItems) return [];
364
+
365
+ let result = lineItems.filter((lineItem) => {
366
+ const matchesSearch = lineItem.name.toLowerCase().includes(searchQuery.toLowerCase());
367
+
368
+ const statuses = appliedFilters.status as string[];
369
+ const matchesStatus = statuses.length === 0 || statuses.includes(lineItem.status);
370
+
371
+ const mediaOwnerIds = appliedFilters.mediaOwner as string[];
372
+ const matchesMediaOwner = mediaOwnerIds.length === 0 || (lineItem.mediaOwnerId && mediaOwnerIds.includes(lineItem.mediaOwnerId));
373
+
374
+ const dateRange = appliedFilters.dateRange as { start?: Date; end?: Date };
375
+ let matchesDateRange = true;
376
+ if (dateRange?.start && lineItem.startDate) {
377
+ const itemStart = new Date(lineItem.startDate);
378
+ matchesDateRange = matchesDateRange && itemStart >= dateRange.start;
379
+ }
380
+ if (dateRange?.end && lineItem.endDate) {
381
+ const itemEnd = new Date(lineItem.endDate);
382
+ matchesDateRange = matchesDateRange && itemEnd <= dateRange.end;
383
+ }
384
+
385
+ const budgetRange = appliedFilters.budgetRange as { min?: number; max?: number };
386
+ let matchesBudget = true;
387
+ if (lineItem.budget) {
388
+ const budget = parseFloat(lineItem.budget);
389
+ if (budgetRange?.min !== undefined) {
390
+ matchesBudget = matchesBudget && budget >= budgetRange.min;
391
+ }
392
+ if (budgetRange?.max !== undefined) {
393
+ matchesBudget = matchesBudget && budget <= budgetRange.max;
394
+ }
395
+ }
396
+
397
+ return matchesSearch && matchesStatus && matchesMediaOwner && matchesDateRange && matchesBudget;
398
+ });
399
+
400
+ result = [...result].sort((a, b) => {
401
+ let comparison = 0;
402
+ switch (sortBy) {
403
+ case "name":
404
+ comparison = a.name.localeCompare(b.name);
405
+ break;
406
+ case "status":
407
+ comparison = a.status.localeCompare(b.status);
408
+ break;
409
+ case "budget":
410
+ const budgetA = a.budget ? parseFloat(a.budget) : 0;
411
+ const budgetB = b.budget ? parseFloat(b.budget) : 0;
412
+ comparison = budgetA - budgetB;
413
+ break;
414
+ case "dates":
415
+ const dateA = a.startDate ? new Date(a.startDate).getTime() : 0;
416
+ const dateB = b.startDate ? new Date(b.startDate).getTime() : 0;
417
+ comparison = dateA - dateB;
418
+ break;
419
+ case "trafficAllocation":
420
+ comparison = (a.trafficAllocation ?? 100) - (b.trafficAllocation ?? 100);
421
+ break;
422
+ case "priority":
423
+ comparison = (a.priority ?? 5) - (b.priority ?? 5);
424
+ break;
425
+ default:
426
+ comparison = 0;
427
+ }
428
+ return sortOrder === "asc" ? comparison : -comparison;
429
+ });
430
+
431
+ return result;
432
+ }, [lineItems, searchQuery, appliedFilters, sortBy, sortOrder]);
433
+
434
+ const summaryStats = useMemo(() => {
435
+ if (!lineItems || !deal) {
436
+ return {
437
+ totalLineItems: 0,
438
+ totalAdPlays: 0,
439
+ totalImpressions: 0,
440
+ totalRevenue: 0,
441
+ budget: 0,
442
+ deliveryProgress: 0,
443
+ };
444
+ }
445
+
446
+ const totalLineItems = lineItems.length;
447
+ const totalRevenue = lineItems.reduce((sum, item) => sum + parseFloat(item.spent || "0"), 0);
448
+ const budget = parseFloat(deal.budget || "0");
449
+ const totalImpressions = Math.round(totalRevenue / 10 * 1000);
450
+ const totalAdPlays = Math.round(totalImpressions * 0.85);
451
+ const deliveryProgress = budget > 0 ? Math.round((totalRevenue / budget) * 100) : 0;
452
+
453
+ return {
454
+ totalLineItems,
455
+ totalAdPlays,
456
+ totalImpressions,
457
+ totalRevenue,
458
+ budget,
459
+ deliveryProgress,
460
+ };
461
+ }, [lineItems, deal]);
462
+
463
+ const getMediaOwnerName = (id: string | null | undefined) => {
464
+ if (!id) return "-";
465
+ const owner = mediaOwners?.find(o => o.id === id);
466
+ return owner?.name ?? "-";
467
+ };
468
+
469
+ const getSspName = (id: string | null | undefined) => {
470
+ if (!id) return "-";
471
+ const ssp = sspPartners?.find(s => s.id === id);
472
+ return ssp?.name ?? "-";
473
+ };
474
+
475
+ const getMissingLineItemFields = (lineItem: LineItem): string[] => {
476
+ const missingFields: string[] = [];
477
+ if (!lineItem.name) missingFields.push("Name");
478
+ if (!lineItem.startDate) missingFields.push("Start Date");
479
+ if (!lineItem.endDate) missingFields.push("End Date");
480
+ return missingFields;
481
+ };
482
+
483
+ const columns = [
484
+ {
485
+ key: "name",
486
+ header: "Name",
487
+ sortable: true,
488
+ render: (lineItem: LineItem) => {
489
+ const missingFields = getMissingLineItemFields(lineItem);
490
+ return (
491
+ <div className="flex items-center gap-2">
492
+ <span className="font-medium text-primary hover:underline cursor-pointer">{lineItem.name}</span>
493
+ {missingFields.length > 0 && (
494
+ <Tooltip>
495
+ <TooltipTrigger asChild>
496
+ <AlertTriangle className="h-4 w-4 text-amber-500 flex-shrink-0" data-testid={`warning-line-item-${lineItem.id}`} />
497
+ </TooltipTrigger>
498
+ <TooltipContent>
499
+ <p className="font-medium">Missing required fields:</p>
500
+ <ul className="list-disc list-inside">
501
+ {missingFields.map((field) => (
502
+ <li key={field}>{field}</li>
503
+ ))}
504
+ </ul>
505
+ </TooltipContent>
506
+ </Tooltip>
507
+ )}
508
+ </div>
509
+ );
510
+ },
511
+ },
512
+ {
513
+ key: "status",
514
+ header: "Status",
515
+ sortable: true,
516
+ render: (lineItem: LineItem) => <StatusBadge status={lineItem.status} />,
517
+ },
518
+ {
519
+ key: "priority",
520
+ header: "Priority",
521
+ sortable: true,
522
+ render: (lineItem: LineItem) => (
523
+ <Badge variant="outline" data-testid={`text-priority-${lineItem.id}`}>
524
+ {lineItem.priority ?? 5}
525
+ </Badge>
526
+ ),
527
+ },
528
+ {
529
+ key: "creativeType",
530
+ header: "Creative Type",
531
+ sortable: false,
532
+ render: (lineItem: LineItem) => (
533
+ <Badge variant="secondary" className="capitalize">
534
+ {lineItem.creativeType || "-"}
535
+ </Badge>
536
+ ),
537
+ },
538
+ {
539
+ key: "dates",
540
+ header: "Flight Dates",
541
+ sortable: true,
542
+ render: (lineItem: LineItem) =>
543
+ lineItem.startDate && lineItem.endDate
544
+ ? `${formatDate(lineItem.startDate)} - ${formatDate(lineItem.endDate)}`
545
+ : "-",
546
+ },
547
+ {
548
+ key: "budget",
549
+ header: "Budget",
550
+ sortable: true,
551
+ render: (lineItem: LineItem) => formatCurrency(lineItem.budget),
552
+ },
553
+ {
554
+ key: "trafficAllocation",
555
+ header: "Traffic Allocation",
556
+ sortable: true,
557
+ render: (lineItem: LineItem) => `${lineItem.trafficAllocation ?? 100}%`,
558
+ },
559
+ {
560
+ key: "impressions",
561
+ header: "Impressions",
562
+ sortable: false,
563
+ render: (lineItem: LineItem) => {
564
+ const spent = parseFloat(lineItem.spent || "0");
565
+ const impressions = Math.round(spent / 10 * 1000);
566
+ return impressions.toLocaleString();
567
+ },
568
+ },
569
+ {
570
+ key: "revenue",
571
+ header: "Revenue",
572
+ sortable: true,
573
+ render: (lineItem: LineItem) => formatCurrency(lineItem.spent ?? 0),
574
+ },
575
+ {
576
+ key: "actions",
577
+ header: "",
578
+ className: "w-12",
579
+ sortable: false,
580
+ render: (lineItem: LineItem) => (
581
+ <DropdownMenu>
582
+ <DropdownMenuTrigger asChild>
583
+ <Button
584
+ variant="ghost"
585
+ size="icon"
586
+ data-testid={`line-item-actions-${lineItem.id}`}
587
+ onClick={(e) => e.stopPropagation()}
588
+ >
589
+ <MoreHorizontal className="h-4 w-4" />
590
+ </Button>
591
+ </DropdownMenuTrigger>
592
+ <DropdownMenuContent align="end">
593
+ <DropdownMenuItem
594
+ onClick={(e) => {
595
+ e.stopPropagation();
596
+ setLocation(`/deals/${dealId}/line-items/${lineItem.id}/creatives`);
597
+ }}
598
+ data-testid={`view-line-item-${lineItem.id}`}
599
+ >
600
+ <Eye className="h-4 w-4 mr-2" />
601
+ View Detail
602
+ </DropdownMenuItem>
603
+ <DropdownMenuItem
604
+ onClick={(e) => {
605
+ e.stopPropagation();
606
+ setLocation(`/deals/${dealId}/line-items/${lineItem.id}/edit`);
607
+ }}
608
+ disabled={isDealLocked ?? false}
609
+ data-testid={`edit-line-item-${lineItem.id}`}
610
+ >
611
+ <Edit className="h-4 w-4 mr-2" />
612
+ Edit
613
+ </DropdownMenuItem>
614
+ <DropdownMenuItem
615
+ onClick={(e) => {
616
+ e.stopPropagation();
617
+ duplicateLineItemMutation.mutate(lineItem.id);
618
+ }}
619
+ disabled={isDealLocked ?? false}
620
+ data-testid={`duplicate-line-item-${lineItem.id}`}
621
+ >
622
+ <Copy className="h-4 w-4 mr-2" />
623
+ Duplicate
624
+ </DropdownMenuItem>
625
+ <DropdownMenuItem
626
+ onClick={(e) => {
627
+ e.stopPropagation();
628
+ handleOptimizeClick(lineItem);
629
+ }}
630
+ disabled={isDealLocked ?? false}
631
+ data-testid={`auto-optimize-line-item-${lineItem.id}`}
632
+ >
633
+ <Sparkles className="h-4 w-4 mr-2" />
634
+ Auto-Optimize
635
+ </DropdownMenuItem>
636
+ <DropdownMenuItem
637
+ className="text-destructive"
638
+ onClick={(e) => {
639
+ e.stopPropagation();
640
+ handleDeleteClick(lineItem);
641
+ }}
642
+ disabled={isDealLocked ?? false}
643
+ data-testid={`delete-line-item-${lineItem.id}`}
644
+ >
645
+ <Trash2 className="h-4 w-4 mr-2" />
646
+ Delete
647
+ </DropdownMenuItem>
648
+ </DropdownMenuContent>
649
+ </DropdownMenu>
650
+ ),
651
+ },
652
+ ];
653
+
654
+ const handleRowClick = (lineItem: LineItem) => {
655
+ setLocation(`/deals/${dealId}/line-items/${lineItem.id}/creatives`);
656
+ };
657
+
658
+ const isLoading = dealLoading || lineItemsLoading;
659
+
660
+ if (dealLoading) {
661
+ return (
662
+ <div className="flex flex-col gap-6 p-6">
663
+ <div className="animate-pulse">
664
+ <div className="h-8 bg-muted rounded w-64 mb-2" />
665
+ <div className="h-4 bg-muted rounded w-96" />
666
+ </div>
667
+ </div>
668
+ );
669
+ }
670
+
671
+ if (!deal) {
672
+ return (
673
+ <div className="flex flex-col gap-6 p-6">
674
+ <div className="text-center py-12">
675
+ <h2 className="text-xl font-semibold">Deal not found</h2>
676
+ <p className="text-muted-foreground mt-2">The deal you're looking for doesn't exist.</p>
677
+ <Button onClick={() => setLocation("/deals")} className="mt-4">
678
+ Back to Deals
679
+ </Button>
680
+ </div>
681
+ </div>
682
+ );
683
+ }
684
+
685
+ return (
686
+ <div className="flex flex-col gap-6 p-6">
687
+ <PageHeader
688
+ title={
689
+ <div className="flex items-center gap-4">
690
+ <div className="flex flex-col">
691
+ <span>{deal.name}</span>
692
+ {deal.externalDealId && (
693
+ <span className="text-sm font-normal text-muted-foreground">ID: {deal.externalDealId}</span>
694
+ )}
695
+ </div>
696
+ <div className="flex flex-col gap-1">
697
+ <Badge variant="outline" className="text-xs capitalize w-fit">
698
+ {deal.source || "influence"}
699
+ </Badge>
700
+ {deal.isRfp && (
701
+ <Badge variant="outline" className="bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200 border-amber-200 dark:border-amber-800 text-xs w-fit">
702
+ RFP
703
+ </Badge>
704
+ )}
705
+ </div>
706
+ </div>
707
+ }
708
+ description={`Deal details and line items`}
709
+ actions={
710
+ <div className="flex items-center gap-2">
711
+ <Button
712
+ variant="outline"
713
+ onClick={() => setLocation("/deals")}
714
+ data-testid="button-back-deals"
715
+ >
716
+ <ArrowLeft className="h-4 w-4 mr-2" />
717
+ Back
718
+ </Button>
719
+ <Button
720
+ onClick={() => setLocation(`/deals/${dealId}/line-items/new`)}
721
+ disabled={isDealLocked ?? false}
722
+ data-testid="button-new-line-item"
723
+ >
724
+ <Plus className="h-4 w-4 mr-2" />
725
+ New Line Item
726
+ </Button>
727
+ {lineItems && lineItems.length > 0 && (
728
+ deal?.acceptanceSent ? (
729
+ <Button
730
+ onClick={() => handleReopenDeal()}
731
+ variant="outline"
732
+ disabled={deal?.reopened ?? false}
733
+ data-testid="button-reopen-deal"
734
+ >
735
+ <RotateCcw className="h-4 w-4 mr-2" />
736
+ {deal?.reopened ? "Reopened" : "Reopen"}
737
+ </Button>
738
+ ) : isProgrammatic ? (
739
+ <Button
740
+ onClick={() => handleRequestAcceptance()}
741
+ variant="default"
742
+ data-testid="button-request-acceptance"
743
+ >
744
+ <Send className="h-4 w-4 mr-2" />
745
+ Request Acceptance
746
+ </Button>
747
+ ) : (
748
+ <Button
749
+ onClick={() => handleActivateDeal()}
750
+ variant="default"
751
+ data-testid="button-activate-deal"
752
+ >
753
+ <PlayCircle className="h-4 w-4 mr-2" />
754
+ Activate Deal
755
+ </Button>
756
+ )
757
+ )}
758
+ </div>
759
+ }
760
+ />
761
+
762
+ {isDealLocked && (
763
+ <Card className="border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950">
764
+ <CardContent className="py-4">
765
+ <div className="flex items-center gap-3">
766
+ <Lock className="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
767
+ <div>
768
+ <p className="font-medium text-amber-800 dark:text-amber-200">Deal and Line Items are locked for editing</p>
769
+ <p className="text-sm text-amber-700 dark:text-amber-300">
770
+ This deal has been sent to stakeholders for approval. All deal and line item editing, duplication, and deletion is disabled to maintain data integrity with external systems. Click "Reopen" to make changes. Note: After reopening, only the deal name and ad-play verification settings can be modified. Deal type cannot be changed.
771
+ </p>
772
+ </div>
773
+ </div>
774
+ </CardContent>
775
+ </Card>
776
+ )}
777
+
778
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5">
779
+ <MetricCard
780
+ title="Total Line Items"
781
+ value={summaryStats.totalLineItems}
782
+ icon={FileText}
783
+ />
784
+ <MetricCard
785
+ title="Total Ad Plays"
786
+ value={summaryStats.totalAdPlays.toLocaleString()}
787
+ icon={PlayCircle}
788
+ />
789
+ <MetricCard
790
+ title="Total Impressions"
791
+ value={summaryStats.totalImpressions.toLocaleString()}
792
+ icon={Eye}
793
+ />
794
+ <MetricCard
795
+ title="Revenue"
796
+ value={formatCurrency(summaryStats.totalRevenue)}
797
+ icon={DollarSign}
798
+ />
799
+ <MetricCard
800
+ title="Budget"
801
+ value={formatCurrency(summaryStats.budget)}
802
+ icon={Wallet}
803
+ />
804
+ </div>
805
+
806
+ <Tabs defaultValue="line-items" data-testid="deal-detail-tabs">
807
+ <TabsList>
808
+ <TabsTrigger value="line-items" data-testid="tab-line-items">
809
+ <FileText className="h-4 w-4 mr-2" />
810
+ Line Items
811
+ </TabsTrigger>
812
+ {deal?.acceptanceSent && (
813
+ <TabsTrigger value="distribution" data-testid="tab-distribution">
814
+ <Link2 className="h-4 w-4 mr-2" />
815
+ Distribution
816
+ </TabsTrigger>
817
+ )}
818
+ </TabsList>
819
+
820
+ <TabsContent value="line-items">
821
+ <div className="flex items-center gap-3 mb-4">
822
+ <div className="relative flex-1">
823
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
824
+ <Input
825
+ placeholder="Search line items..."
826
+ value={searchQuery}
827
+ onChange={(e) => setSearchQuery(e.target.value)}
828
+ className="pl-9 bg-white dark:bg-background border"
829
+ data-testid="input-search"
830
+ />
831
+ </div>
832
+ <Button
833
+ variant="outline"
834
+ onClick={() => setFilterOpen(true)}
835
+ className="bg-white dark:bg-background"
836
+ data-testid="button-open-filters"
837
+ >
838
+ <Filter className="h-4 w-4 mr-2" />
839
+ Filters
840
+ {activeFilterCount > 0 && (
841
+ <Badge variant="secondary" className="ml-2">{activeFilterCount}</Badge>
842
+ )}
843
+ </Button>
844
+ <Button
845
+ variant="outline"
846
+ size="icon"
847
+ onClick={() => setHistoryDrawerOpen(true)}
848
+ data-testid="button-view-history"
849
+ >
850
+ <History className="h-4 w-4" />
851
+ </Button>
852
+ </div>
853
+
854
+ <Card>
855
+ <CardContent className="pt-6">
856
+ <DataTable
857
+ columns={columns}
858
+ data={filteredLineItems}
859
+ isLoading={lineItemsLoading}
860
+ emptyMessage="No line items found. Create your first line item to get started."
861
+ onRowClick={handleRowClick}
862
+ sortKey={sortBy}
863
+ sortOrder={sortOrder}
864
+ onSort={handleSort}
865
+ />
866
+ </CardContent>
867
+ </Card>
868
+ </TabsContent>
869
+
870
+ {deal?.acceptanceSent && (
871
+ <TabsContent value="distribution">
872
+ <Card>
873
+ <CardHeader className="flex flex-row items-center justify-between gap-2">
874
+ <div>
875
+ <h3 className="text-lg font-semibold" data-testid="text-distribution-title">VAST Tag Distribution</h3>
876
+ <p className="text-sm text-muted-foreground">
877
+ Distribute VAST endpoints to CMS platforms for ad delivery. Choose the format that matches your CMS capabilities.
878
+ </p>
879
+ </div>
880
+ {distributionData && distributionData.lineItems.length > 0 && (
881
+ <Button
882
+ variant="outline"
883
+ onClick={handleDownloadAll}
884
+ data-testid="button-download-all"
885
+ >
886
+ <Download className="h-4 w-4 mr-2" />
887
+ Download All
888
+ </Button>
889
+ )}
890
+ </CardHeader>
891
+ <CardContent>
892
+ {distributionLoading ? (
893
+ <div className="flex items-center justify-center py-12">
894
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
895
+ </div>
896
+ ) : !distributionData || distributionData.lineItems.length === 0 ? (
897
+ <div className="text-center py-12 text-muted-foreground" data-testid="text-no-distribution">
898
+ <Link2 className="h-10 w-10 mx-auto mb-3 opacity-50" />
899
+ <p>No line items available for distribution.</p>
900
+ </div>
901
+ ) : (
902
+ <div className="space-y-4">
903
+ {distributionData.lineItems.map((item) => (
904
+ <Card key={item.lineItemId} data-testid={`distribution-card-${item.lineItemId}`}>
905
+ <CardContent className="py-4">
906
+ <div className="flex flex-col gap-3">
907
+ <div className="flex items-center justify-between gap-2 flex-wrap">
908
+ <div className="flex items-center gap-2">
909
+ <span className="font-medium" data-testid={`text-distribution-name-${item.lineItemId}`}>{item.lineItemName}</span>
910
+ <StatusBadge status={item.status} />
911
+ {item.creativeType && (
912
+ <Badge variant="secondary" className="capitalize text-xs">
913
+ {item.creativeType}
914
+ </Badge>
915
+ )}
916
+ </div>
917
+ <div className="flex items-center gap-2">
918
+ <Tooltip>
919
+ <TooltipTrigger asChild>
920
+ <Button
921
+ variant="outline"
922
+ size="sm"
923
+ onClick={() => handleDownloadHtml(item.lineItemId)}
924
+ data-testid={`button-download-html-${item.lineItemId}`}
925
+ >
926
+ <FileCode className="h-4 w-4 mr-1" />
927
+ HTML
928
+ </Button>
929
+ </TooltipTrigger>
930
+ <TooltipContent>
931
+ <p>Download HTML player file for CMS platforms that support web content scheduling</p>
932
+ </TooltipContent>
933
+ </Tooltip>
934
+ <Tooltip>
935
+ <TooltipTrigger asChild>
936
+ <Button
937
+ variant="outline"
938
+ size="sm"
939
+ onClick={() => handleDownloadZip(item.lineItemId)}
940
+ data-testid={`button-download-zip-${item.lineItemId}`}
941
+ >
942
+ <Package className="h-4 w-4 mr-1" />
943
+ ZIP
944
+ </Button>
945
+ </TooltipTrigger>
946
+ <TooltipContent>
947
+ <p>Download ZIP package for CMS platforms that only accept offline file uploads</p>
948
+ </TooltipContent>
949
+ </Tooltip>
950
+ </div>
951
+ </div>
952
+ <div className="flex items-center gap-2">
953
+ <code className="flex-1 text-xs bg-muted px-3 py-2 rounded-md font-mono break-all" data-testid={`text-vast-url-${item.lineItemId}`}>
954
+ {item.vastUrl}
955
+ </code>
956
+ <Tooltip>
957
+ <TooltipTrigger asChild>
958
+ <Button
959
+ variant="outline"
960
+ size="icon"
961
+ onClick={() => handleCopyVastUrl(item.vastUrl, item.lineItemId)}
962
+ data-testid={`button-copy-vast-${item.lineItemId}`}
963
+ >
964
+ {copiedId === item.lineItemId ? (
965
+ <Check className="h-4 w-4 text-green-600" />
966
+ ) : (
967
+ <ClipboardCopy className="h-4 w-4" />
968
+ )}
969
+ </Button>
970
+ </TooltipTrigger>
971
+ <TooltipContent>
972
+ <p>Copy VAST endpoint URL for CMS platforms with native VAST support</p>
973
+ </TooltipContent>
974
+ </Tooltip>
975
+ </div>
976
+ </div>
977
+ </CardContent>
978
+ </Card>
979
+ ))}
980
+ </div>
981
+ )}
982
+ </CardContent>
983
+ </Card>
984
+ </TabsContent>
985
+ )}
986
+ </Tabs>
987
+
988
+ <FilterDrawer
989
+ open={filterOpen}
990
+ onOpenChange={setFilterOpen}
991
+ fields={filterFields}
992
+ values={filterValues}
993
+ onChange={setFilterValues}
994
+ onApply={handleApplyFilters}
995
+ onClear={handleClearFilters}
996
+ title="Filter Insertion Orders"
997
+ />
998
+
999
+ <HistoryDrawer
1000
+ open={historyDrawerOpen}
1001
+ onOpenChange={setHistoryDrawerOpen}
1002
+ entityId={dealId || ""}
1003
+ entityType="deal"
1004
+ entityName={deal?.name}
1005
+ hierarchical={true}
1006
+ />
1007
+
1008
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1009
+ <AlertDialogContent>
1010
+ <AlertDialogHeader>
1011
+ <AlertDialogTitle>Delete Line Item</AlertDialogTitle>
1012
+ <AlertDialogDescription>
1013
+ Are you sure you want to delete "{lineItemToDelete?.name}"? This action cannot be undone.
1014
+ </AlertDialogDescription>
1015
+ </AlertDialogHeader>
1016
+ <AlertDialogFooter>
1017
+ <AlertDialogCancel data-testid="button-cancel-delete">Cancel</AlertDialogCancel>
1018
+ <AlertDialogAction
1019
+ onClick={handleConfirmDelete}
1020
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
1021
+ data-testid="button-confirm-delete"
1022
+ >
1023
+ Delete
1024
+ </AlertDialogAction>
1025
+ </AlertDialogFooter>
1026
+ </AlertDialogContent>
1027
+ </AlertDialog>
1028
+
1029
+ <AlertDialog open={optimizeDialogOpen} onOpenChange={setOptimizeDialogOpen}>
1030
+ <AlertDialogContent>
1031
+ <AlertDialogHeader>
1032
+ <AlertDialogTitle>Auto-Optimize Line Item</AlertDialogTitle>
1033
+ <AlertDialogDescription>
1034
+ This will use AI recommendations to add up to 5 optimal inventories to "{lineItemToOptimize?.name}".
1035
+ Existing inventory selections will be preserved.
1036
+ </AlertDialogDescription>
1037
+ </AlertDialogHeader>
1038
+ <AlertDialogFooter>
1039
+ <AlertDialogCancel data-testid="button-cancel-optimize">Cancel</AlertDialogCancel>
1040
+ <AlertDialogAction
1041
+ onClick={handleConfirmOptimize}
1042
+ disabled={autoOptimizeMutation.isPending}
1043
+ data-testid="button-confirm-optimize"
1044
+ >
1045
+ {autoOptimizeMutation.isPending ? (
1046
+ <>
1047
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
1048
+ Optimizing...
1049
+ </>
1050
+ ) : (
1051
+ <>
1052
+ <Sparkles className="h-4 w-4 mr-2" />
1053
+ Optimize
1054
+ </>
1055
+ )}
1056
+ </AlertDialogAction>
1057
+ </AlertDialogFooter>
1058
+ </AlertDialogContent>
1059
+ </AlertDialog>
1060
+ </div>
1061
+ );
1062
+ }