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,1703 @@
1
+ import { useParams, useLocation } from "wouter";
2
+ import { useTranslation } from "@/lib/i18n";
3
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useToast } from "@/hooks/use-toast";
5
+ import { useState } from "react";
6
+ import { Button, cn } from "@moving-walls/design-system";
7
+ import { Card, CardContent, CardHeader } from "@moving-walls/design-system";
8
+ import { Badge } from "@moving-walls/design-system";
9
+ import { PageHeader } from "@moving-walls/design-system";
10
+ import { AgGridTable } from "@moving-walls/design-system";
11
+ import type { AgGridTableServerSideConfig, ColDef } from "@moving-walls/design-system";
12
+
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogDescription,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "@moving-walls/design-system";
21
+ import {
22
+ Sheet,
23
+ SheetContent,
24
+ SheetHeader,
25
+ SheetTitle,
26
+ } from "@moving-walls/design-system";
27
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@moving-walls/design-system";
28
+ import { Input } from "@moving-walls/design-system";
29
+ import {
30
+ ArrowLeft,
31
+ Plus,
32
+ Package,
33
+ Pencil,
34
+ Copy,
35
+ Trash2,
36
+ Loader2,
37
+ Calendar,
38
+ DollarSign,
39
+ BarChart3,
40
+ Monitor,
41
+ MoreHorizontal,
42
+ FileText,
43
+ History,
44
+ Search,
45
+ X,
46
+ Image,
47
+ Send,
48
+ RotateCcw,
49
+ Archive,
50
+ Filter,
51
+ Eye,
52
+ PlayCircle,
53
+ Wallet,
54
+ } from "lucide-react";
55
+ import { RequestApprovalSheet } from "@/components/deals/request-approval-sheet";
56
+ import { ReopenDealSheet } from "@/components/deals/reopen-deal-sheet";
57
+ import { ResendApprovalSheet } from "@/components/deals/resend-approval-sheet";
58
+ import {
59
+ DropdownMenu,
60
+ DropdownMenuContent,
61
+ DropdownMenuItem,
62
+ DropdownMenuTrigger,
63
+ Tooltip,
64
+ } from "@moving-walls/design-system";
65
+ import {
66
+ InfluenceDealsAPI,
67
+ influenceDealsRequest,
68
+ generateExternalId,
69
+ isDealEditable,
70
+ type Deal,
71
+ type LineItem,
72
+ } from "@/lib/influence-deals-api";
73
+ import { DealStatusBadge } from "@/components/deals/deal-status-badge";
74
+ import { usePageTitle } from "@/hooks/use-page-title";
75
+
76
+ export default function DealLineItemsPage() {
77
+ const { t } = useTranslation(['deals', 'common', 'creatives']);
78
+ const { dealId } = useParams<{ dealId: string }>();
79
+ const [, setLocation] = useLocation();
80
+ const queryClient = useQueryClient();
81
+
82
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
83
+ const [lineItemToDelete, setLineItemToDelete] = useState<LineItem | null>(
84
+ null,
85
+ );
86
+ const [isDeleting, setIsDeleting] = useState(false);
87
+ const [isCopying, setIsCopying] = useState<string | null>(null);
88
+ const [isActivating, setIsActivating] = useState(false);
89
+ const [currentPage, setCurrentPage] = useState(1);
90
+ const [pageSize, setPageSize] = useState(10);
91
+ const [selectedLineItem, setSelectedLineItem] = useState<LineItem | null>(null);
92
+ const [lineItemPopoverOpen, setLineItemPopoverOpen] = useState(false);
93
+ const [inventorySearch, setInventorySearch] = useState("");
94
+ const [inventoryTab, setInventoryTab] = useState("screens");
95
+ const [popoverTab, setPopoverTab] = useState<"inventories" | "creatives">("inventories");
96
+ const [isApprovalSheetOpen, setIsApprovalSheetOpen] = useState(false);
97
+ const [isReopenSheetOpen, setIsReopenSheetOpen] = useState(false);
98
+ const [isResendApprovalSheetOpen, setIsResendApprovalSheetOpen] = useState(false);
99
+ const [activateDialogOpen, setActivateDialogOpen] = useState(false);
100
+ const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
101
+ const [isArchiving, setIsArchiving] = useState(false);
102
+ const { toast } = useToast();
103
+ const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
104
+ const [lineItemSearch, setLineItemSearch] = useState("");
105
+
106
+ const { data: deal, isLoading: isLoadingDeal } = useQuery({
107
+ queryKey: ["deal", dealId],
108
+ queryFn: async () => {
109
+ const response = await influenceDealsRequest<Deal>(
110
+ InfluenceDealsAPI.deals.get(dealId!),
111
+ );
112
+ return response;
113
+ },
114
+ enabled: !!dealId,
115
+ refetchOnMount: 'always',
116
+ });
117
+
118
+ usePageTitle("Line Items", deal?.name);
119
+
120
+ const { data: lineItemsResponse, isLoading: isLoadingLineItems } = useQuery({
121
+ queryKey: ["deal-line-items", dealId, currentPage, pageSize],
122
+ queryFn: async () => {
123
+ const response = await influenceDealsRequest<{
124
+ data: LineItem[];
125
+ pagination?: { page: number; limit: number; total: number; totalPages: number }
126
+ }>(
127
+ InfluenceDealsAPI.lineItems.list(dealId!, { page: currentPage, limit: pageSize }),
128
+ );
129
+ return {
130
+ data: response?.data || response || [],
131
+ pagination: (response as any)?.pagination || null
132
+ };
133
+ },
134
+ enabled: !!dealId,
135
+ refetchOnMount: 'always',
136
+ });
137
+
138
+ const lineItems = Array.isArray(lineItemsResponse?.data) ? lineItemsResponse.data : [];
139
+ const paginationInfo = lineItemsResponse?.pagination;
140
+ const isLoading = isLoadingDeal || isLoadingLineItems;
141
+
142
+ // Fetch full line item details when sheet is open (includes targeting data)
143
+ const { data: lineItemDetails, isLoading: isLoadingDetails } = useQuery({
144
+ queryKey: ["line-item-details", dealId, selectedLineItem?.id],
145
+ queryFn: async () => {
146
+ const response = await influenceDealsRequest<any>(
147
+ InfluenceDealsAPI.lineItems.get(dealId!, selectedLineItem!.id),
148
+ );
149
+ return response;
150
+ },
151
+ enabled: !!dealId && !!selectedLineItem?.id && lineItemPopoverOpen,
152
+ });
153
+
154
+ // Fetch inventories for selected line item when sheet is open
155
+ const { data: lineItemInventoriesResponse, isLoading: isLoadingInventories } = useQuery({
156
+ queryKey: ["line-item-inventories", dealId, selectedLineItem?.id],
157
+ queryFn: async () => {
158
+ const response = await influenceDealsRequest<{
159
+ data: any[];
160
+ pagination?: { page: number; limit: number; total: number; totalPages: number }
161
+ }>(
162
+ InfluenceDealsAPI.lineItems.inventories(dealId!, selectedLineItem!.id, { page: 1, limit: 100 }),
163
+ );
164
+ return {
165
+ data: response?.data || response || [],
166
+ pagination: (response as any)?.pagination || null
167
+ };
168
+ },
169
+ enabled: !!dealId && !!selectedLineItem?.id && lineItemPopoverOpen,
170
+ });
171
+
172
+ // Fetch creatives for selected line item when sheet is open
173
+ const { data: lineItemCreativesResponse, isLoading: isLoadingCreatives } = useQuery({
174
+ queryKey: ["line-item-creatives", dealId, selectedLineItem?.id],
175
+ queryFn: async () => {
176
+ const response = await influenceDealsRequest<{
177
+ data: any[];
178
+ pagination?: { page: number; limit: number; total: number; totalPages: number }
179
+ }>(
180
+ InfluenceDealsAPI.creatives.listByLineItem(dealId!, selectedLineItem!.id, { page: 1, limit: 100 }),
181
+ );
182
+ return {
183
+ data: response?.data || response || [],
184
+ pagination: (response as any)?.pagination || null
185
+ };
186
+ },
187
+ enabled: !!dealId && !!selectedLineItem?.id && lineItemPopoverOpen,
188
+ });
189
+
190
+ const { data: historyData, isLoading: isLoadingHistory } = useQuery({
191
+ queryKey: ["deal-history", dealId],
192
+ queryFn: async () => {
193
+ const response = await influenceDealsRequest<{
194
+ dealId: string;
195
+ total: number;
196
+ items: Array<{
197
+ id: string;
198
+ action: string;
199
+ entityType: string;
200
+ entityId: string;
201
+ field?: string;
202
+ fieldLabel?: string;
203
+ summary?: string;
204
+ previousValue?: any;
205
+ currentValue?: any;
206
+ changedBy?: string | null;
207
+ changedByEmail?: string | null;
208
+ metadata?: {
209
+ entityName?: string;
210
+ totalChanges?: number;
211
+ changedFields?: string[];
212
+ approvalAction?: string;
213
+ decision?: string;
214
+ approverEmail?: string;
215
+ approverEmails?: string[];
216
+ };
217
+ timestamp: string;
218
+ }>;
219
+ }>(
220
+ InfluenceDealsAPI.deals.history(dealId!),
221
+ "GET",
222
+ );
223
+ return response;
224
+ },
225
+ enabled: !!dealId && historyDrawerOpen,
226
+ });
227
+
228
+ const totalPages = paginationInfo?.totalPages || 1;
229
+ const totalItems = paginationInfo?.total || lineItems.length;
230
+ const filteredLineItems = lineItems.filter((li) => {
231
+ if (!lineItemSearch) return true;
232
+ const search = lineItemSearch.toLowerCase();
233
+ return (li.name || "").toLowerCase().includes(search);
234
+ });
235
+ const paginatedLineItems = filteredLineItems;
236
+
237
+ const formatCurrency = (
238
+ amount: number | undefined,
239
+ currency: string = "MYR",
240
+ ): string => {
241
+ if (!amount) return `${currency} 0`;
242
+ return `${currency} ${amount.toLocaleString()}`;
243
+ };
244
+
245
+ const formatDate = (dateStr?: string | null): string => {
246
+ if (!dateStr) return "--";
247
+ const date = new Date(dateStr);
248
+ return date.toLocaleDateString("en-US", {
249
+ month: "short",
250
+ day: "2-digit",
251
+ year: "numeric",
252
+ });
253
+ };
254
+
255
+ const formatDateRange = (
256
+ startDate?: string | null,
257
+ endDate?: string | null,
258
+ ): string => {
259
+ if (!startDate && !endDate) return "--";
260
+ const start = startDate ? formatDate(startDate) : "--";
261
+ const end = endDate ? formatDate(endDate) : "--";
262
+ return `${start} - ${end}`;
263
+ };
264
+
265
+ const formatPacingType = (type?: string | null): string => {
266
+ if (!type) return "--";
267
+ const normalizedType = type.toLowerCase().replace(/[_-]/g, '');
268
+ switch (normalizedType) {
269
+ case 'even': return 'Even (Default)';
270
+ case 'asap': return 'ASAP';
271
+ case 'frontloaded': return 'Front-Loaded';
272
+ case 'backloaded': return 'Back-Loaded';
273
+ default: return type.charAt(0).toUpperCase() + type.slice(1);
274
+ }
275
+ };
276
+
277
+ const calculateDaysRemaining = (endDate?: string | null): string | null => {
278
+ if (!endDate) return null;
279
+ const end = new Date(endDate);
280
+ const now = new Date();
281
+ const days = Math.ceil(
282
+ (end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
283
+ );
284
+ if (days < 0) return "Ended";
285
+ if (days === 0) return "Ends today";
286
+ return `${days} days remaining`;
287
+ };
288
+
289
+ // Deal-level budget from deal configuration
290
+ const dealBudget =
291
+ (deal as any)?.totalBudget ||
292
+ (deal as any)?.direct?.budgetSetup?.budgetAmount ||
293
+ (deal as any)?.programmatic?.dealSetup?.dealFloor ||
294
+ 0;
295
+
296
+ // Use deal-level allocated budget if available from API
297
+ // If not available, don't show allocated vs total comparison
298
+ const hasAllocatedBudgetData = typeof (deal as any)?.allocatedBudget === 'number';
299
+ const allocatedBudget = hasAllocatedBudgetData ? (deal as any).allocatedBudget : 0;
300
+
301
+ // For backward compatibility
302
+ const totalBudget = dealBudget;
303
+
304
+ // Remaining budget from deal level - only calculate if we have allocated data
305
+ const remainingBudget = hasAllocatedBudgetData
306
+ ? (deal as any)?.remainingBudget || (dealBudget - allocatedBudget)
307
+ : Infinity;
308
+ const isBudgetExceeded = hasAllocatedBudgetData && dealBudget > 0 && allocatedBudget >= dealBudget;
309
+
310
+ // Use deal-level total impressions if available
311
+ const totalImpressions =
312
+ (deal as any)?.totalImpressions ||
313
+ (deal as any)?.targetQuantity ||
314
+ (deal as any)?.impressionsGoal ||
315
+ 0;
316
+
317
+ const formatNumber = (num: number): string => {
318
+ if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`;
319
+ if (num >= 1000) return `${(num / 1000).toFixed(0)}k`;
320
+ return num.toString();
321
+ };
322
+
323
+ const generateCopyName = (originalName: string): string => {
324
+ const copyPattern = /\s*-\s*Copy\s*(\d+)?$/i;
325
+ const match = originalName.match(copyPattern);
326
+
327
+ if (match) {
328
+ const currentNum = match[1] ? parseInt(match[1], 10) : 1;
329
+ const baseName = originalName.replace(copyPattern, "");
330
+ return `${baseName} - Copy ${currentNum + 1}`;
331
+ }
332
+
333
+ const existingCopies = lineItems.filter((li) => {
334
+ const liName = li.name || "";
335
+ return liName.startsWith(originalName) && liName.includes("Copy");
336
+ });
337
+
338
+ if (existingCopies.length === 0) {
339
+ return `${originalName} - Copy 1`;
340
+ }
341
+
342
+ let maxCopyNum = 0;
343
+ existingCopies.forEach((li) => {
344
+ const liMatch = (li.name || "").match(/Copy\s*(\d+)/i);
345
+ if (liMatch) {
346
+ const num = parseInt(liMatch[1], 10);
347
+ if (num > maxCopyNum) maxCopyNum = num;
348
+ }
349
+ });
350
+
351
+ return `${originalName} - Copy ${maxCopyNum + 1}`;
352
+ };
353
+
354
+ const handleDeleteClick = (lineItem: LineItem) => {
355
+ setLineItemToDelete(lineItem);
356
+ setDeleteDialogOpen(true);
357
+ };
358
+
359
+ const handleConfirmDelete = async () => {
360
+ if (!lineItemToDelete || !dealId) return;
361
+
362
+ setIsDeleting(true);
363
+ try {
364
+ await influenceDealsRequest(
365
+ InfluenceDealsAPI.lineItems.delete(dealId, lineItemToDelete.id),
366
+ "DELETE",
367
+ );
368
+ queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
369
+ setDeleteDialogOpen(false);
370
+ setLineItemToDelete(null);
371
+ } catch (error) {
372
+ console.error("Failed to delete line item:", error);
373
+ } finally {
374
+ setIsDeleting(false);
375
+ }
376
+ };
377
+
378
+ const handleCopyLineItem = async (lineItem: LineItem) => {
379
+ if (!dealId) return;
380
+
381
+ setIsCopying(lineItem.id);
382
+ try {
383
+ const li = lineItem as any;
384
+ const copyName = generateCopyName(lineItem.name);
385
+
386
+ const payload: any = {
387
+ externalId: generateExternalId(),
388
+ name: copyName,
389
+ status: "DRAFT",
390
+ startDate: li.startDate,
391
+ endDate: li.endDate,
392
+ currency: li.currency || deal?.currency || "USD",
393
+ };
394
+
395
+ if (li.direct) {
396
+ payload.direct = {
397
+ budgetSetup: li.direct.budgetSetup,
398
+ campaignGoal: li.direct.campaignGoal,
399
+ };
400
+ }
401
+
402
+ if (li.impressions) {
403
+ payload.impressions = li.impressions;
404
+ }
405
+ if (li.netCostPerDay) {
406
+ payload.netCostPerDay = li.netCostPerDay;
407
+ }
408
+
409
+ if (li.targeting) {
410
+ payload.targeting = li.targeting;
411
+ }
412
+
413
+ await influenceDealsRequest(
414
+ InfluenceDealsAPI.lineItems.create(dealId),
415
+ "POST",
416
+ payload,
417
+ );
418
+
419
+ queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
420
+ } catch (error) {
421
+ console.error("Failed to copy line item:", error);
422
+ } finally {
423
+ setIsCopying(null);
424
+ }
425
+ };
426
+
427
+ const handleActivateCampaign = async () => {
428
+ if (!dealId || !deal) return;
429
+
430
+ setIsActivating(true);
431
+ try {
432
+ const startDate = (deal as any)?.startDate;
433
+ const today = new Date();
434
+ today.setHours(0, 0, 0, 0);
435
+
436
+ let newStatus: string;
437
+ if (startDate) {
438
+ const dealStartDate = new Date(startDate);
439
+ if (isNaN(dealStartDate.getTime())) {
440
+ newStatus = "LIVE";
441
+ } else {
442
+ dealStartDate.setHours(0, 0, 0, 0);
443
+ newStatus = dealStartDate > today ? "APPROVED" : "LIVE";
444
+ }
445
+ } else {
446
+ newStatus = "LIVE";
447
+ }
448
+
449
+ await influenceDealsRequest(
450
+ InfluenceDealsAPI.deals.update(dealId),
451
+ "PUT",
452
+ { status: newStatus }
453
+ );
454
+
455
+ queryClient.invalidateQueries({ queryKey: ["deal", dealId] });
456
+ queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
457
+ } catch (error) {
458
+ console.error("Failed to activate campaign:", error);
459
+ } finally {
460
+ setIsActivating(false);
461
+ }
462
+ };
463
+
464
+ const handleArchiveDeal = async () => {
465
+ if (!dealId) return;
466
+ setIsArchiving(true);
467
+ try {
468
+ await influenceDealsRequest(
469
+ InfluenceDealsAPI.deals.update(dealId),
470
+ "PUT",
471
+ { status: "ARCHIVED" }
472
+ );
473
+ queryClient.invalidateQueries({ queryKey: ["deal", dealId] });
474
+ queryClient.invalidateQueries({ queryKey: ["deals"] });
475
+ toast({ title: "Deal archived", description: "The deal has been moved to archive.", variant: "success" });
476
+ setLocation("/deals");
477
+ } catch (error) {
478
+ console.error("Failed to archive deal:", error);
479
+ toast({ title: "Error", description: "Failed to archive deal. Please try again.", variant: "destructive" });
480
+ } finally {
481
+ setIsArchiving(false);
482
+ setArchiveDialogOpen(false);
483
+ }
484
+ };
485
+
486
+ const handleLineItemClick = (lineItem: LineItem) => {
487
+ setLocation(`/deals/${dealId}/line-items/${lineItem.id}`);
488
+ };
489
+
490
+ const getInventoryCount = (li: any): number => {
491
+ return li.totalInventories || li.inventoryIds?.length || li.inventories?.length || 0;
492
+ };
493
+
494
+ interface InventoryItem {
495
+ id: string;
496
+ name: string;
497
+ address?: string;
498
+ venueType?: string;
499
+ publisherName?: string;
500
+ mediaType?: string;
501
+ estimatedCost?: number;
502
+ tags?: string[];
503
+ thumbnail?: string;
504
+ }
505
+
506
+ interface CreativeItem {
507
+ id: string;
508
+ name: string;
509
+ type?: string;
510
+ status?: string;
511
+ thumbnailUrl?: string;
512
+ dimensions?: string;
513
+ mimeType?: string;
514
+ mediaUrl?: string;
515
+ }
516
+
517
+ const getLineItemInventories = (lineItem: any): InventoryItem[] => {
518
+ if (!lineItem.inventories || !Array.isArray(lineItem.inventories) || lineItem.inventories.length === 0) {
519
+ return [];
520
+ }
521
+
522
+ return lineItem.inventories.map((inv: any, idx: number) => ({
523
+ id: inv.id || inv.inventoryId || `inv-${idx}`,
524
+ name: inv.name || inv.inventoryName || "Unnamed inventory",
525
+ address: inv.address || inv.location,
526
+ venueType: inv.venueType,
527
+ publisherName: inv.publisherName,
528
+ mediaType: inv.mediaType,
529
+ estimatedCost: inv.estimatedCost || inv.dailyCost,
530
+ tags: inv.tags,
531
+ }));
532
+ };
533
+
534
+ const getLineItemCreatives = (lineItem: any): CreativeItem[] => {
535
+ if (!lineItem.creatives || !Array.isArray(lineItem.creatives) || lineItem.creatives.length === 0) {
536
+ return [];
537
+ }
538
+
539
+ return lineItem.creatives.map((cr: any, idx: number) => ({
540
+ id: cr.id || cr.creativeId || `cr-${idx}`,
541
+ name: cr.name || cr.creativeName || "Unnamed creative",
542
+ type: cr.type || cr.creativeType || cr.mediaType,
543
+ status: cr.status,
544
+ thumbnailUrl: cr.thumbnailUrl || cr.thumbnail || cr.creativeUri || cr.previewUrl,
545
+ dimensions: cr.dimensions || cr.resolution || (cr.width && cr.height ? `${cr.width}x${cr.height}` : undefined),
546
+ }));
547
+ };
548
+
549
+ // Use fetched inventories from API, fallback to embedded inventories
550
+ const fetchedInventories = lineItemInventoriesResponse?.data || [];
551
+ const selectedInventories = fetchedInventories.length > 0
552
+ ? fetchedInventories.map((inv: any, idx: number) => ({
553
+ id: inv.id || inv.inventoryId || `inv-${idx}`,
554
+ name: inv.name || inv.inventoryName || "Unnamed inventory",
555
+ address: inv.address || inv.location,
556
+ venueType: inv.venueType,
557
+ publisherName: inv.publisherName || inv.publisher?.name,
558
+ mediaType: inv.mediaType,
559
+ estimatedCost: inv.estimatedCost || inv.dailyCost,
560
+ tags: inv.tags,
561
+ thumbnail: inv.thumbnail || inv.thumbnailUrl || inv.imageUrl,
562
+ }))
563
+ : (selectedLineItem ? getLineItemInventories(selectedLineItem) : []);
564
+
565
+ // Use fetched creatives from API, fallback to embedded creatives
566
+ const fetchedCreatives = lineItemCreativesResponse?.data || [];
567
+ const selectedCreatives = fetchedCreatives.length > 0
568
+ ? fetchedCreatives.map((cr: any, idx: number) => ({
569
+ id: cr.id || cr.creativeId || `cr-${idx}`,
570
+ name: cr.name || cr.creativeName || "Unnamed creative",
571
+ type: cr.type || cr.creativeType,
572
+ status: cr.status,
573
+ thumbnailUrl: cr.thumbnail || cr.thumbnailUrl,
574
+ dimensions: cr.dimensions || cr.resolution,
575
+ mimeType: cr.mimeType,
576
+ mediaUrl: cr.creativeUri || cr.mediaUrl || cr.url,
577
+ }))
578
+ : (selectedLineItem ? getLineItemCreatives(selectedLineItem) : []);
579
+ const filteredInventories = selectedInventories.filter((inv: InventoryItem) =>
580
+ inv.name.toLowerCase().includes(inventorySearch.toLowerCase()) ||
581
+ inv.address?.toLowerCase().includes(inventorySearch.toLowerCase())
582
+ );
583
+ const filteredCreatives = selectedCreatives.filter(cr =>
584
+ cr.name.toLowerCase().includes(inventorySearch.toLowerCase())
585
+ );
586
+
587
+ const dealMode =
588
+ (deal as any)?.mode?.toUpperCase() === "PROGRAMMATIC"
589
+ ? "PROGRAMMATIC"
590
+ : "DIRECT";
591
+
592
+ // Check if this is a Planner deal that can be edited in restricted mode
593
+ const isPlannerDeal = deal?.source === 'mw-planner';
594
+ const PLANNER_RESTRICTED_EDIT_STATUSES = ['APPROVED', 'LIVE', 'PENDING'];
595
+ const isPlannerRestrictedEdit = isPlannerDeal &&
596
+ deal?.status &&
597
+ PLANNER_RESTRICTED_EDIT_STATUSES.includes(deal.status.toUpperCase());
598
+
599
+ const showRequestApprovalButton = ['GENERATED', 'REOPENED'].includes(deal?.status?.toUpperCase() || '');
600
+
601
+ const showActivateButton = ['DRAFT', 'GENERATED'].includes(deal?.status?.toUpperCase() || '') && totalItems >= 1;
602
+
603
+ const REOPEN_ALLOWED_STATUSES = ['APPROVED', 'LIVE', 'COMPLETED', 'EXPIRED', 'REJECTED', 'ARCHIVED'];
604
+ const showReopenButton = dealMode === 'DIRECT' &&
605
+ !isPlannerDeal &&
606
+ deal?.status &&
607
+ REOPEN_ALLOWED_STATUSES.includes(deal.status.toUpperCase());
608
+
609
+ // Show resend approval email button for PENDING deals
610
+ const showResendApprovalButton = deal?.status?.toUpperCase() === 'PENDING';
611
+
612
+ // Only allow editing in DRAFT, GENERATED, or REOPENED stages (or restricted mode for Planner)
613
+ const canEdit = isDealEditable(deal?.status);
614
+ const canEditOrPlannerRestricted = canEdit || isPlannerRestrictedEdit;
615
+
616
+ const lineItemColumnDefs: ColDef<LineItem>[] = [
617
+ {
618
+ colId: "name",
619
+ headerName: "Name",
620
+ field: "name",
621
+ sortable: true,
622
+ minWidth: 200,
623
+ flex: 1,
624
+ cellRenderer: (params: any) => {
625
+ const lineItem = params.data as LineItem;
626
+ if (!lineItem) return null;
627
+ return (
628
+ <p
629
+ className="font-medium text-mw-primary-600 hover:text-mw-primary-700 hover:underline cursor-pointer"
630
+ onClick={() => handleLineItemClick(lineItem)}
631
+ >
632
+ {lineItem.name}
633
+ </p>
634
+ );
635
+ },
636
+ },
637
+ {
638
+ colId: "status",
639
+ headerName: "Status",
640
+ sortable: false,
641
+ cellRenderer: (params: any) => {
642
+ const li = params.data as any;
643
+ if (!li) return null;
644
+ return <DealStatusBadge status={li.status || "DRAFT"} />;
645
+ },
646
+ },
647
+ {
648
+ colId: "priority",
649
+ headerName: "Priority",
650
+ sortable: false,
651
+ cellRenderer: (params: any) => {
652
+ const li = params.data as any;
653
+ if (!li) return null;
654
+ return (
655
+ <span className="text-sm text-mw-neutral-900 dark:text-white">
656
+ {li.priority || "-"}
657
+ </span>
658
+ );
659
+ },
660
+ },
661
+ {
662
+ colId: "creativeType",
663
+ headerName: "Creative Type",
664
+ sortable: false,
665
+ cellRenderer: (params: any) => {
666
+ const li = params.data as any;
667
+ if (!li) return null;
668
+ return (
669
+ <span className="text-sm text-mw-neutral-900 dark:text-white">
670
+ {li.creativeType || "-"}
671
+ </span>
672
+ );
673
+ },
674
+ },
675
+ {
676
+ colId: "flightDates",
677
+ headerName: "Flight Dates",
678
+ sortable: false,
679
+ minWidth: 180,
680
+ cellRenderer: (params: any) => {
681
+ const li = params.data as any;
682
+ if (!li) return null;
683
+ const daysRemaining = calculateDaysRemaining(li.endDate);
684
+ return (
685
+ <div>
686
+ <p className="text-sm text-mw-neutral-900 dark:text-white">
687
+ {formatDateRange(li.startDate, li.endDate)}
688
+ </p>
689
+ {daysRemaining && (
690
+ <p className={cn("text-xs", daysRemaining === "Ended" ? "text-mw-error-600" : "text-mw-success-600")}>
691
+ {daysRemaining}
692
+ </p>
693
+ )}
694
+ </div>
695
+ );
696
+ },
697
+ },
698
+ {
699
+ colId: "budget",
700
+ headerName: "Budget",
701
+ sortable: false,
702
+ cellRenderer: (params: any) => {
703
+ const li = params.data as any;
704
+ if (!li) return null;
705
+ return (
706
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
707
+ {formatCurrency(
708
+ li.budget || li.direct?.budgetSetup?.budgetAmount,
709
+ deal?.currency || "MYR",
710
+ )}
711
+ </p>
712
+ );
713
+ },
714
+ },
715
+ {
716
+ colId: "trafficAllocation",
717
+ headerName: "Traffic Allocation",
718
+ sortable: false,
719
+ cellRenderer: (params: any) => {
720
+ const li = params.data as any;
721
+ if (!li) return null;
722
+ return (
723
+ <span className="text-sm text-mw-neutral-900 dark:text-white">
724
+ {li.trafficAllocation ? `${li.trafficAllocation}%` : "-"}
725
+ </span>
726
+ );
727
+ },
728
+ },
729
+ {
730
+ colId: "impressions",
731
+ headerName: "Impressions",
732
+ sortable: false,
733
+ cellRenderer: (params: any) => {
734
+ const li = params.data as any;
735
+ if (!li) return null;
736
+ return (
737
+ <span className="text-sm font-medium text-mw-neutral-900 dark:text-white">
738
+ {(li.targetValue || li.direct?.campaignGoal?.targetValue || li.direct?.goalSetup?.targetValue || li.targetQuantity || li.impressions || 0).toLocaleString()}
739
+ </span>
740
+ );
741
+ },
742
+ },
743
+ {
744
+ colId: "revenue",
745
+ headerName: "Revenue",
746
+ sortable: false,
747
+ cellRenderer: (params: any) => {
748
+ const li = params.data as any;
749
+ if (!li) return null;
750
+ return (
751
+ <span className="text-sm font-medium text-mw-neutral-900 dark:text-white">
752
+ {formatCurrency(li.revenue || 0, deal?.currency || "MYR")}
753
+ </span>
754
+ );
755
+ },
756
+ },
757
+ {
758
+ colId: "actions",
759
+ headerName: "",
760
+ sortable: false,
761
+ width: 60,
762
+ cellRenderer: (params: any) => {
763
+ const lineItem = params.data as LineItem;
764
+ if (!lineItem) return null;
765
+ return (
766
+ <DropdownMenu>
767
+ <DropdownMenuTrigger asChild>
768
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0" isIconOnly>
769
+ <MoreHorizontal className="h-4 w-4" />
770
+ </Button>
771
+ </DropdownMenuTrigger>
772
+ <DropdownMenuContent align="end">
773
+ {canEditOrPlannerRestricted && (
774
+ <DropdownMenuItem onClick={() => setLocation(`/deals/${dealId}/line-items/${lineItem.id}/edit${isPlannerRestrictedEdit ? '?plannerRestricted=true' : ''}`)}>
775
+ <Pencil className="h-4 w-4 mr-2" />
776
+ {isPlannerRestrictedEdit ? 'Edit Pacing & Creatives' : 'Edit'}
777
+ </DropdownMenuItem>
778
+ )}
779
+ <DropdownMenuItem onClick={() => setLocation(`/deals/${dealId}/line-items/${lineItem.id}/creatives`)}>
780
+ <Image className="h-4 w-4 mr-2" />
781
+ Creatives
782
+ </DropdownMenuItem>
783
+ {canEdit && (
784
+ <>
785
+ <DropdownMenuItem
786
+ onClick={() => handleCopyLineItem(lineItem)}
787
+ disabled={isCopying === lineItem.id}
788
+ >
789
+ {isCopying === lineItem.id ? (
790
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
791
+ ) : (
792
+ <Copy className="h-4 w-4 mr-2" />
793
+ )}
794
+ Copy
795
+ </DropdownMenuItem>
796
+ <DropdownMenuItem
797
+ onClick={() => handleDeleteClick(lineItem)}
798
+ className="text-red-600 focus:text-red-600"
799
+ >
800
+ <Trash2 className="h-4 w-4 mr-2" />
801
+ Delete
802
+ </DropdownMenuItem>
803
+ </>
804
+ )}
805
+ </DropdownMenuContent>
806
+ </DropdownMenu>
807
+ );
808
+ },
809
+ },
810
+ ];
811
+
812
+ return (
813
+ <div className="h-[calc(100vh-72px)] w-full flex flex-col bg-mw-neutral-50 dark:bg-mw-neutral-900 overflow-hidden">
814
+ <div className="flex-shrink-0 px-6 py-5 bg-white dark:bg-mw-neutral-800">
815
+ <div className="flex items-start justify-between">
816
+ <div className="flex items-start gap-3">
817
+ <Button variant="ghost" size="sm" className="mt-1 h-8 w-8 !p-0" isIconOnly onClick={() => setLocation("/deals")}>
818
+ <ArrowLeft className="h-4 w-4" />
819
+ </Button>
820
+ <div>
821
+ <div className="flex items-center gap-3">
822
+ <h1 className="text-2xl font-semibold text-mw-neutral-900 dark:text-white">
823
+ {deal?.name || "Loading..."}
824
+ </h1>
825
+ <Badge className="bg-blue-50 text-blue-600 border-blue-200 text-xs">
826
+ {(deal as any)?.source || "Activate"}
827
+ </Badge>
828
+ <Badge className="bg-amber-50 text-amber-700 border-amber-200 text-xs">
829
+ {dealMode === 'PROGRAMMATIC' ? 'Programmatic' : (deal as any)?.dealType || 'RFP'}
830
+ </Badge>
831
+ </div>
832
+ <div className="flex items-center gap-2 mt-1">
833
+ <p className="text-sm text-mw-neutral-500">
834
+ ID: {deal?.dealId || deal?.id?.slice(0, 10) || "--"}
835
+ </p>
836
+ </div>
837
+ <p className="text-sm text-mw-neutral-500 mt-0.5">
838
+ Deal details and line items
839
+ </p>
840
+ </div>
841
+ </div>
842
+ <div className="flex items-center gap-2">
843
+ {canEdit && (
844
+ <Button
845
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white"
846
+ disabled={isBudgetExceeded}
847
+ onClick={() => setLocation(`/deals/${dealId}/line-items/new`)}
848
+ >
849
+ <Plus className="h-4 w-4 mr-2" />
850
+ New Line Item
851
+ </Button>
852
+ )}
853
+ {showActivateButton && (
854
+ <Button
855
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white"
856
+ onClick={() => setActivateDialogOpen(true)}
857
+ disabled={isActivating}
858
+ >
859
+ {isActivating ? (
860
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
861
+ ) : (
862
+ <PlayCircle className="h-4 w-4 mr-2" />
863
+ )}
864
+ Activate Deal
865
+ </Button>
866
+ )}
867
+ {showRequestApprovalButton && (
868
+ <Button
869
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white"
870
+ onClick={() => setIsApprovalSheetOpen(true)}
871
+ >
872
+ <Send className="h-4 w-4 mr-2" />
873
+ Request Acceptance
874
+ </Button>
875
+ )}
876
+ {showResendApprovalButton && (
877
+ <Button
878
+ onClick={() => setIsResendApprovalSheetOpen(true)}
879
+ variant="outline"
880
+ className="border-mw-info-500 text-mw-info-600 hover:bg-mw-info-50"
881
+ >
882
+ <Send className="h-4 w-4 mr-2" />
883
+ Resend Approval
884
+ </Button>
885
+ )}
886
+ {showReopenButton && (
887
+ <Button
888
+ onClick={() => setIsReopenSheetOpen(true)}
889
+ variant="outline"
890
+ className="border-amber-500 text-amber-600 hover:bg-amber-50"
891
+ >
892
+ <RotateCcw className="h-4 w-4 mr-2" />
893
+ Reopen Deal
894
+ </Button>
895
+ )}
896
+ <DropdownMenu>
897
+ <DropdownMenuTrigger asChild>
898
+ <Button variant="outline" isIconOnly className="h-9 w-9 !p-0">
899
+ <MoreHorizontal className="h-4 w-4" />
900
+ </Button>
901
+ </DropdownMenuTrigger>
902
+ <DropdownMenuContent align="end">
903
+ {canEditOrPlannerRestricted && (
904
+ <DropdownMenuItem onClick={() => setLocation(`/deals/edit/${dealId}${isPlannerRestrictedEdit ? '?plannerRestricted=true' : ''}`)}>
905
+ <Pencil className="h-4 w-4 mr-2" />
906
+ {isPlannerRestrictedEdit ? 'Edit Pacing & Creatives' : 'Edit Deal'}
907
+ </DropdownMenuItem>
908
+ )}
909
+ <DropdownMenuItem
910
+ onClick={() => setArchiveDialogOpen(true)}
911
+ className="text-red-600 focus:text-red-600"
912
+ >
913
+ <Archive className="h-4 w-4 mr-2" />
914
+ Move to Archive
915
+ </DropdownMenuItem>
916
+ </DropdownMenuContent>
917
+ </DropdownMenu>
918
+ </div>
919
+ </div>
920
+ </div>
921
+
922
+ {/* Main content with padding */}
923
+ <div className="flex-1 w-full overflow-auto px-6 py-6">
924
+ <div className="grid grid-cols-5 gap-4 mb-6">
925
+ {[
926
+ { label: "Total Line Items", value: totalItems.toLocaleString(), icon: FileText, bgColor: "bg-blue-50", iconColor: "text-blue-500" },
927
+ { label: "Total Ad Plays", value: ((deal as any)?.totalAdPlays || 0).toLocaleString(), icon: PlayCircle, bgColor: "bg-green-50", iconColor: "text-green-500" },
928
+ { label: "Total Impressions", value: totalImpressions.toLocaleString(), icon: Eye, bgColor: "bg-purple-50", iconColor: "text-purple-500" },
929
+ { label: "Revenue", value: formatCurrency((deal as any)?.revenue || 0, deal?.currency || "MYR"), icon: DollarSign, bgColor: "bg-amber-50", iconColor: "text-amber-500" },
930
+ { label: "Budget", value: formatCurrency(dealBudget || totalBudget, deal?.currency || "MYR"), icon: Wallet, bgColor: "bg-red-50", iconColor: "text-red-500" },
931
+ ].map((metric) => (
932
+ <Card key={metric.label} className="border-blue-100 dark:border-mw-neutral-700">
933
+ <CardContent className="p-5">
934
+ <div className="flex items-start justify-between">
935
+ <div>
936
+ <p className="text-sm text-mw-neutral-500 dark:text-mw-neutral-400">{metric.label}</p>
937
+ <p className="text-2xl font-semibold text-mw-neutral-900 dark:text-white mt-2">{metric.value}</p>
938
+ </div>
939
+ <div className={cn("p-2 rounded-lg", metric.bgColor)}>
940
+ <metric.icon className={cn("h-5 w-5", metric.iconColor)} />
941
+ </div>
942
+ </div>
943
+ </CardContent>
944
+ </Card>
945
+ ))}
946
+ </div>
947
+
948
+ <Card className="flex flex-col h-auto border-mw-neutral-100">
949
+ <CardHeader className="p-4 bg-white dark:bg-mw-neutral-800">
950
+ <div className="flex items-center gap-3">
951
+ <div className="flex items-center gap-2 text-sm font-medium text-mw-neutral-700 dark:text-mw-neutral-300">
952
+ <FileText className="h-4 w-4" />
953
+ Line Items
954
+ </div>
955
+ <div className="flex-1 relative">
956
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-mw-neutral-400" />
957
+ <Input
958
+ placeholder="Search line items..."
959
+ className="pl-9 h-9"
960
+ value={lineItemSearch}
961
+ onChange={(e) => setLineItemSearch(e.target.value)}
962
+ />
963
+ </div>
964
+ <Button variant="outline" size="sm" className="h-9">
965
+ <Filter className="h-4 w-4 mr-2" />
966
+ Filters
967
+ </Button>
968
+ <Tooltip content="Deal History">
969
+ <Button
970
+ variant="outline"
971
+ size="sm"
972
+ className="h-9 w-9 !p-0"
973
+ isIconOnly
974
+ onClick={() => setHistoryDrawerOpen(true)}
975
+ >
976
+ <History className="h-4 w-4" />
977
+ </Button>
978
+ </Tooltip>
979
+ </div>
980
+ </CardHeader>
981
+
982
+ <CardContent className="p-0 flex-1 flex flex-col overflow-hidden">
983
+ {lineItems.length === 0 && !isLoading ? (
984
+ <div className="flex flex-col items-center justify-center py-16 text-center">
985
+ <div className="w-20 h-20 mb-4 flex items-center justify-center bg-mw-neutral-100 dark:bg-mw-neutral-800 rounded-lg">
986
+ <Package className="w-10 h-10 text-mw-neutral-400" />
987
+ </div>
988
+ <h3 className="text-lg font-medium text-mw-neutral-900 dark:text-white mb-2">
989
+ No Line Items Configured yet
990
+ </h3>
991
+ <p className="text-sm text-mw-neutral-500 dark:text-mw-neutral-400 mb-6 max-w-md">
992
+ Create line items to define inventory, schedule, and budget
993
+ for your campaign.
994
+ </p>
995
+ <Button
996
+ onClick={() => setLocation(`/deals/${dealId}/line-items/new`)}
997
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white"
998
+ >
999
+ <Plus className="h-4 w-4 mr-2" />
1000
+ New Line Item
1001
+ </Button>
1002
+ </div>
1003
+ ) : (
1004
+ <div className="flex-1 overflow-hidden">
1005
+ <AgGridTable
1006
+ rowData={paginatedLineItems}
1007
+ columnDefs={lineItemColumnDefs}
1008
+ getRowId={(row) => row.id}
1009
+ loading={isLoading}
1010
+ emptyMessage="No line items found."
1011
+ domLayout="autoHeight"
1012
+ serverSidePagination={true}
1013
+ serverSideConfig={{
1014
+ totalCount: totalItems,
1015
+ currentPage,
1016
+ pageSize,
1017
+ onPageChange: setCurrentPage,
1018
+ onPageSizeChange: (newSize) => {
1019
+ setPageSize(newSize);
1020
+ setCurrentPage(1);
1021
+ },
1022
+ pageSizeSelector: [10, 25, 50, 100],
1023
+ }}
1024
+ />
1025
+ </div>
1026
+ )}
1027
+ </CardContent>
1028
+ </Card>
1029
+ </div>
1030
+
1031
+ <Sheet open={lineItemPopoverOpen} onOpenChange={setLineItemPopoverOpen}>
1032
+ <SheetContent className="w-[640px] sm:max-w-[640px] p-0 flex flex-col">
1033
+ <SheetHeader className="px-6 py-4 border-b border-mw-neutral-200 dark:border-mw-neutral-700 flex-shrink-0">
1034
+ <div className="flex items-center justify-between">
1035
+ <SheetTitle className="text-lg font-semibold">Line Item Details</SheetTitle>
1036
+ <Button
1037
+ variant="ghost"
1038
+ size="sm"
1039
+ onClick={() => setLineItemPopoverOpen(false)}
1040
+ className="h-8 w-8 p-0"
1041
+ >
1042
+ <X className="h-4 w-4" />
1043
+ </Button>
1044
+ </div>
1045
+ </SheetHeader>
1046
+
1047
+ {selectedLineItem && (() => {
1048
+ const displayLineItem = lineItemDetails ? { ...selectedLineItem, ...lineItemDetails } : selectedLineItem;
1049
+ return (
1050
+ <div className="flex flex-col flex-1 overflow-hidden">
1051
+ <div className="px-6 py-4 border-b border-mw-neutral-200 dark:border-mw-neutral-700 flex-shrink-0">
1052
+ <div className="flex items-start justify-between mb-3">
1053
+ <div>
1054
+ <h3 className="text-base font-semibold text-mw-neutral-900 dark:text-white">
1055
+ {displayLineItem.name}
1056
+ </h3>
1057
+ <p className="text-xs text-mw-neutral-500 mt-0.5">
1058
+ LID: {displayLineItem.id?.slice(0, 8)}
1059
+ </p>
1060
+ </div>
1061
+ <DealStatusBadge status={(displayLineItem as any).status || "DRAFT"} />
1062
+ </div>
1063
+
1064
+ <div className="space-y-4 mt-4">
1065
+ <div className="grid grid-cols-3 gap-3">
1066
+ <div>
1067
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Flight Dates</p>
1068
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1069
+ {formatDateRange((displayLineItem as any).startDate, (displayLineItem as any).endDate)}
1070
+ </p>
1071
+ </div>
1072
+ <div>
1073
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Creative Type</p>
1074
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1075
+ {(displayLineItem as any).creativeType || "--"}
1076
+ </p>
1077
+ </div>
1078
+ <div>
1079
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Duration</p>
1080
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1081
+ {(displayLineItem as any).duration ? `${(displayLineItem as any).duration}s` : "--"}
1082
+ </p>
1083
+ </div>
1084
+ </div>
1085
+
1086
+ <div className="border-t border-mw-neutral-100 dark:border-mw-neutral-700 pt-3">
1087
+ <p className="text-xs font-medium text-mw-neutral-600 dark:text-mw-neutral-300 mb-2">Budget & Goals</p>
1088
+ <div className="grid grid-cols-3 gap-3">
1089
+ <div>
1090
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Budget</p>
1091
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1092
+ {formatCurrency(
1093
+ (displayLineItem as any).budget || (displayLineItem as any).direct?.budgetSetup?.budgetAmount,
1094
+ deal?.currency || "MYR"
1095
+ )}
1096
+ </p>
1097
+ </div>
1098
+ <div>
1099
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Goal Type → Target</p>
1100
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1101
+ {(displayLineItem as any).goalType || (displayLineItem as any).direct?.campaignGoal?.type || (displayLineItem as any).direct?.goalSetup?.goalType || "Impressions"} → {formatNumber((displayLineItem as any).targetValue || (displayLineItem as any).direct?.campaignGoal?.targetValue || (displayLineItem as any).direct?.goalSetup?.targetValue || (displayLineItem as any).targetQuantity || (displayLineItem as any).impressions || 0)}
1102
+ </p>
1103
+ </div>
1104
+ <div>
1105
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Pacing</p>
1106
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1107
+ {formatPacingType((displayLineItem as any).pacingType || (displayLineItem as any).pacing?.type || (displayLineItem as any).direct?.pacingSetup?.pacingType)}
1108
+ {((displayLineItem as any).pacing?.dailyCap || (displayLineItem as any).direct?.pacingSetup?.pacingLimit) &&
1109
+ ` (Daily: ${formatNumber((displayLineItem as any).pacing?.dailyCap || (displayLineItem as any).direct?.pacingSetup?.pacingLimit || 0)})`}
1110
+ </p>
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+
1115
+ <div className="border-t border-mw-neutral-100 dark:border-mw-neutral-700 pt-3">
1116
+ <p className="text-xs font-medium text-mw-neutral-600 dark:text-mw-neutral-300 mb-2">Specifications</p>
1117
+ <div className="grid grid-cols-2 gap-3">
1118
+ <div>
1119
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Resolutions</p>
1120
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1121
+ {(displayLineItem as any).resolutions?.length > 0
1122
+ ? (displayLineItem as any).resolutions.map((r: any) => typeof r === 'string' ? r : `${r.width}x${r.height}`).join(", ")
1123
+ : "--"}
1124
+ </p>
1125
+ </div>
1126
+ <div>
1127
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Inventories</p>
1128
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white">
1129
+ {getInventoryCount(displayLineItem)}
1130
+ </p>
1131
+ </div>
1132
+ </div>
1133
+ </div>
1134
+
1135
+ <div className="border-t border-mw-neutral-100 dark:border-mw-neutral-700 pt-3">
1136
+ <p className="text-xs font-medium text-mw-neutral-600 dark:text-mw-neutral-300 mb-2">Targeting</p>
1137
+ <div className="grid grid-cols-2 gap-3">
1138
+ <div>
1139
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Demographics</p>
1140
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1141
+ {(() => {
1142
+ const li = displayLineItem as any;
1143
+ const demo = li.targeting?.demographics || li.demographics || li.deliveryTargeting?.demographics;
1144
+ if (!demo) return "--";
1145
+ if (Array.isArray(demo)) return demo.join(", ") || "--";
1146
+ if (typeof demo === 'object') {
1147
+ const parts = [];
1148
+ if (demo.ageGroups?.length) parts.push(`Age: ${demo.ageGroups.join(", ")}`);
1149
+ if (demo.genders?.length) parts.push(`Gender: ${demo.genders.join(", ")}`);
1150
+ if (demo.incomeLevel?.length) parts.push(`Income: ${demo.incomeLevel.join(", ")}`);
1151
+ return parts.join(" | ") || "--";
1152
+ }
1153
+ return String(demo);
1154
+ })()}
1155
+ </p>
1156
+ </div>
1157
+ <div>
1158
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">GeoFencing</p>
1159
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1160
+ {(() => {
1161
+ const li = displayLineItem as any;
1162
+ const geo = li.targeting?.geoFencing || li.geoZones || li.geoFencing || li.deliveryTargeting?.geoFencing;
1163
+ if (!geo) return "--";
1164
+ if (Array.isArray(geo)) return `${geo.length} zone${geo.length !== 1 ? 's' : ''}`;
1165
+ if (typeof geo === 'object') {
1166
+ const geometrics = geo.geometrics || [];
1167
+ const locations = geo.locations || [];
1168
+ const pois = geo.pois || [];
1169
+ const total = geometrics.length + locations.length + pois.length;
1170
+ if (total > 0) return `${total} zone${total !== 1 ? 's' : ''}`;
1171
+ }
1172
+ return String(geo);
1173
+ })()}
1174
+ </p>
1175
+ </div>
1176
+ <div>
1177
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Signals</p>
1178
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1179
+ {(() => {
1180
+ const li = displayLineItem as any;
1181
+ const signals = li.targeting?.signals || li.signals || li.deliveryTargeting?.signals;
1182
+ if (!signals) return "--";
1183
+ if (Array.isArray(signals)) return signals.length > 0 ? signals.join(", ") : "--";
1184
+ if (typeof signals === 'object') {
1185
+ const types = Object.keys(signals).filter(k => signals[k] && Object.keys(signals[k]).length > 0);
1186
+ if (types.length > 0) return types.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(", ");
1187
+ }
1188
+ return "--";
1189
+ })()}
1190
+ </p>
1191
+ </div>
1192
+ <div>
1193
+ <p className="text-xs text-mw-neutral-500 dark:text-mw-neutral-400">Schedule</p>
1194
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1195
+ {(() => {
1196
+ const li = displayLineItem as any;
1197
+ const schedule = li.scheduleRules || li.schedule || li.scheduleGrids;
1198
+ if (!schedule) return "--";
1199
+ if (Array.isArray(schedule)) return `${schedule.length} rule${schedule.length !== 1 ? 's' : ''}`;
1200
+ if (typeof schedule === 'object') {
1201
+ if (schedule.rules) return `${schedule.rules.length} rule${schedule.rules.length !== 1 ? 's' : ''}`;
1202
+ const keys = Object.keys(schedule);
1203
+ if (keys.length > 0) return `${keys.length} day${keys.length !== 1 ? 's' : ''} configured`;
1204
+ }
1205
+ return String(schedule);
1206
+ })()}
1207
+ </p>
1208
+ </div>
1209
+ </div>
1210
+ </div>
1211
+ </div>
1212
+ </div>
1213
+
1214
+ <div className="flex-1 flex flex-col overflow-hidden">
1215
+ <div className="px-6 pt-4">
1216
+ <div className="flex border-b border-mw-neutral-200 dark:border-mw-neutral-700">
1217
+ <Button
1218
+ variant={popoverTab === "inventories" ? "primary" : "ghost"}
1219
+ size="sm"
1220
+ className={cn(
1221
+ "rounded-none border-b-2 -mb-px",
1222
+ popoverTab === "inventories"
1223
+ ? "border-mw-primary-500"
1224
+ : "border-transparent"
1225
+ )}
1226
+ onClick={() => setPopoverTab("inventories")}
1227
+ >
1228
+ Inventories ({getInventoryCount(displayLineItem)})
1229
+ </Button>
1230
+ <Button
1231
+ variant={popoverTab === "creatives" ? "primary" : "ghost"}
1232
+ size="sm"
1233
+ className={cn(
1234
+ "rounded-none border-b-2 -mb-px",
1235
+ popoverTab === "creatives"
1236
+ ? "border-mw-primary-500"
1237
+ : "border-transparent"
1238
+ )}
1239
+ onClick={() => setPopoverTab("creatives")}
1240
+ >
1241
+ Creatives ({selectedCreatives.length})
1242
+ </Button>
1243
+ </div>
1244
+ </div>
1245
+
1246
+ {popoverTab === "inventories" && (
1247
+ <div className="flex-1 flex flex-col overflow-hidden px-6 pt-4">
1248
+ <Tabs value={inventoryTab} onValueChange={setInventoryTab} className="flex-1 flex flex-col overflow-hidden">
1249
+ <TabsList className="grid grid-cols-3 mb-4 flex-shrink-0">
1250
+ <TabsTrigger value="screens" className="data-[state=active]:bg-mw-primary-500 data-[state=active]:text-white">
1251
+ Screens
1252
+ </TabsTrigger>
1253
+ <TabsTrigger value="networks">Networks</TabsTrigger>
1254
+ <TabsTrigger value="packages">Packages</TabsTrigger>
1255
+ </TabsList>
1256
+
1257
+ <div className="grid grid-cols-4 gap-4 mb-4 text-center flex-shrink-0">
1258
+ <div>
1259
+ <p className="text-sm text-mw-primary-500 font-medium">
1260
+ {String(filteredInventories.length).padStart(2, "0")}
1261
+ </p>
1262
+ <p className="text-xs text-mw-neutral-500">Total selected</p>
1263
+ </div>
1264
+ <div>
1265
+ <p className="text-sm text-mw-neutral-900 dark:text-white font-medium">
1266
+ {String(filteredInventories.length).padStart(2, "0")}
1267
+ </p>
1268
+ <p className="text-xs text-mw-neutral-500">Screens</p>
1269
+ </div>
1270
+ <div>
1271
+ <p className="text-sm text-mw-neutral-900 dark:text-white font-medium">00</p>
1272
+ <p className="text-xs text-mw-neutral-500">Networks</p>
1273
+ </div>
1274
+ <div>
1275
+ <p className="text-sm text-mw-neutral-900 dark:text-white font-medium">00</p>
1276
+ <p className="text-xs text-mw-neutral-500">Packages</p>
1277
+ </div>
1278
+ </div>
1279
+
1280
+ <div className="relative mb-4 flex-shrink-0">
1281
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-mw-neutral-400" />
1282
+ <Input
1283
+ placeholder="Search screens, network, package here.."
1284
+ value={inventorySearch}
1285
+ onChange={(e) => setInventorySearch(e.target.value)}
1286
+ className="pl-10"
1287
+ />
1288
+ </div>
1289
+
1290
+ <TabsContent value="screens" className="mt-0 flex-1 overflow-y-auto">
1291
+ <div className="space-y-3 pb-4">
1292
+ {isLoadingInventories ? (
1293
+ <div className="text-center py-8 text-mw-neutral-500">
1294
+ <Loader2 className="h-8 w-8 mx-auto mb-2 animate-spin opacity-50" />
1295
+ <p>Loading inventories...</p>
1296
+ </div>
1297
+ ) : filteredInventories.length === 0 ? (
1298
+ <div className="text-center py-8 text-mw-neutral-500">
1299
+ <Monitor className="h-8 w-8 mx-auto mb-2 opacity-50" />
1300
+ <p>No inventories found</p>
1301
+ </div>
1302
+ ) : (
1303
+ filteredInventories.map((inventory, index) => (
1304
+ <div
1305
+ key={inventory.id}
1306
+ className="p-3 border border-mw-neutral-200 dark:border-mw-neutral-700 rounded-lg"
1307
+ >
1308
+ <div className="flex items-start gap-3">
1309
+ <div className="w-12 h-12 rounded bg-mw-neutral-100 dark:bg-mw-neutral-800 flex items-center justify-center flex-shrink-0 overflow-hidden">
1310
+ {inventory.thumbnail ? (
1311
+ <img
1312
+ src={inventory.thumbnail}
1313
+ alt={inventory.name}
1314
+ className="w-full h-full object-cover"
1315
+ onError={(e) => {
1316
+ (e.target as HTMLImageElement).style.display = 'none';
1317
+ (e.target as HTMLImageElement).parentElement?.classList.add('thumbnail-fallback');
1318
+ }}
1319
+ />
1320
+ ) : (
1321
+ <Monitor className="h-5 w-5 text-mw-neutral-500" />
1322
+ )}
1323
+ </div>
1324
+ <div className="flex-1 min-w-0">
1325
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1326
+ {inventory.name}
1327
+ </p>
1328
+ {inventory.address && (
1329
+ <p className="text-xs text-mw-neutral-500 truncate mt-0.5">
1330
+ {inventory.address}
1331
+ </p>
1332
+ )}
1333
+ <div className="flex items-center gap-2 mt-1">
1334
+ {inventory.venueType && (
1335
+ <Badge variant="outline" className="text-xs">
1336
+ {inventory.venueType}
1337
+ </Badge>
1338
+ )}
1339
+ {inventory.publisherName && (
1340
+ <span className="text-xs text-mw-neutral-500">
1341
+ {inventory.publisherName}
1342
+ </span>
1343
+ )}
1344
+ </div>
1345
+ </div>
1346
+ </div>
1347
+ </div>
1348
+ ))
1349
+ )}
1350
+ </div>
1351
+ </TabsContent>
1352
+
1353
+ <TabsContent value="networks" className="mt-0 flex-1 overflow-y-auto">
1354
+ <div className="text-center py-8 text-mw-neutral-500">
1355
+ <Package className="h-8 w-8 mx-auto mb-2 opacity-50" />
1356
+ <p>No networks available</p>
1357
+ </div>
1358
+ </TabsContent>
1359
+
1360
+ <TabsContent value="packages" className="mt-0 flex-1 overflow-y-auto">
1361
+ <div className="text-center py-8 text-mw-neutral-500">
1362
+ <Package className="h-8 w-8 mx-auto mb-2 opacity-50" />
1363
+ <p>No packages available</p>
1364
+ </div>
1365
+ </TabsContent>
1366
+ </Tabs>
1367
+ </div>
1368
+ )}
1369
+
1370
+ {popoverTab === "creatives" && (
1371
+ <div className="flex-1 overflow-y-auto px-6 pt-4">
1372
+ <div className="relative mb-4">
1373
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-mw-neutral-400" />
1374
+ <Input
1375
+ placeholder="Search creatives..."
1376
+ value={inventorySearch}
1377
+ onChange={(e) => setInventorySearch(e.target.value)}
1378
+ className="pl-10"
1379
+ />
1380
+ </div>
1381
+
1382
+ <div className="space-y-3 pb-4">
1383
+ {isLoadingCreatives ? (
1384
+ <div className="flex items-center justify-center py-8">
1385
+ <Loader2 className="h-6 w-6 animate-spin text-mw-primary-500" />
1386
+ </div>
1387
+ ) : filteredCreatives.length === 0 ? (
1388
+ <div className="text-center py-8 text-mw-neutral-500">
1389
+ <Image className="h-8 w-8 mx-auto mb-2 opacity-50" />
1390
+ <p>No creatives found</p>
1391
+ </div>
1392
+ ) : (
1393
+ filteredCreatives.map((creative) => {
1394
+ const isVideo = creative.mimeType?.startsWith('video/') || creative.type === 'VIDEO';
1395
+ const mediaSource = creative.mediaUrl || creative.thumbnailUrl;
1396
+
1397
+ return (
1398
+ <div
1399
+ key={creative.id}
1400
+ className="p-3 border border-mw-neutral-200 dark:border-mw-neutral-700 rounded-lg"
1401
+ >
1402
+ <div className="flex flex-col gap-3">
1403
+ {/* Media preview */}
1404
+ <div className="w-full aspect-video rounded bg-mw-neutral-100 dark:bg-mw-neutral-800 flex items-center justify-center overflow-hidden">
1405
+ {isVideo && mediaSource ? (
1406
+ <video
1407
+ src={mediaSource}
1408
+ controls
1409
+ className="w-full h-full object-contain"
1410
+ poster={creative.thumbnailUrl}
1411
+ >
1412
+ Your browser does not support the video tag.
1413
+ </video>
1414
+ ) : creative.thumbnailUrl ? (
1415
+ <img src={creative.thumbnailUrl} alt={creative.name} className="w-full h-full object-contain" />
1416
+ ) : mediaSource ? (
1417
+ <img src={mediaSource} alt={creative.name} className="w-full h-full object-contain" />
1418
+ ) : (
1419
+ <Image className="h-8 w-8 text-mw-neutral-500" />
1420
+ )}
1421
+ </div>
1422
+
1423
+ {/* Creative info */}
1424
+ <div className="flex-1 min-w-0">
1425
+ <p className="text-sm font-medium text-mw-neutral-900 dark:text-white truncate">
1426
+ {creative.name}
1427
+ </p>
1428
+ <div className="flex flex-wrap items-center gap-2 mt-1">
1429
+ {creative.type && (
1430
+ <Badge variant="outline" className="text-xs">
1431
+ {creative.type}
1432
+ </Badge>
1433
+ )}
1434
+ {creative.dimensions && (
1435
+ <span className="text-xs text-mw-neutral-500">
1436
+ {creative.dimensions}
1437
+ </span>
1438
+ )}
1439
+ {creative.status && (
1440
+ <Badge
1441
+ variant="outline"
1442
+ className={cn(
1443
+ "text-xs",
1444
+ creative.status === "APPROVED" && "bg-green-50 text-green-700 border-green-200",
1445
+ creative.status === "PENDING" && "bg-yellow-50 text-yellow-700 border-yellow-200",
1446
+ creative.status === "REJECTED" && "bg-red-50 text-red-700 border-red-200"
1447
+ )}
1448
+ >
1449
+ {creative.status}
1450
+ </Badge>
1451
+ )}
1452
+ </div>
1453
+ </div>
1454
+ </div>
1455
+ </div>
1456
+ );
1457
+ })
1458
+ )}
1459
+ </div>
1460
+ </div>
1461
+ )}
1462
+ </div>
1463
+ </div>
1464
+ );
1465
+ })()}
1466
+ </SheetContent>
1467
+ </Sheet>
1468
+
1469
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1470
+ <DialogContent className="sm:max-w-md">
1471
+ <DialogHeader>
1472
+ <DialogTitle>Delete Line Item</DialogTitle>
1473
+ <DialogDescription>
1474
+ Are you sure you want to delete "{lineItemToDelete?.name}"? This
1475
+ action cannot be undone.
1476
+ </DialogDescription>
1477
+ </DialogHeader>
1478
+ <DialogFooter className="gap-2">
1479
+ <Button
1480
+ variant="outline"
1481
+ onClick={() => {
1482
+ setDeleteDialogOpen(false);
1483
+ setLineItemToDelete(null);
1484
+ }}
1485
+ disabled={isDeleting}
1486
+ >
1487
+ Cancel
1488
+ </Button>
1489
+ <Button
1490
+ variant="destructive"
1491
+ onClick={handleConfirmDelete}
1492
+ disabled={isDeleting}
1493
+ className="bg-red-600 hover:bg-red-700 text-white"
1494
+ >
1495
+ {isDeleting ? (
1496
+ <>
1497
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
1498
+ Deleting...
1499
+ </>
1500
+ ) : (
1501
+ "Delete"
1502
+ )}
1503
+ </Button>
1504
+ </DialogFooter>
1505
+ </DialogContent>
1506
+ </Dialog>
1507
+
1508
+ <Sheet open={activateDialogOpen} onOpenChange={setActivateDialogOpen}>
1509
+ <SheetContent className="w-[420px] sm:max-w-[420px] p-0 flex flex-col">
1510
+ <SheetHeader className="px-6 py-4 border-b border-mw-neutral-200 dark:border-mw-neutral-700 flex-shrink-0">
1511
+ <div className="flex items-center justify-between">
1512
+ <SheetTitle className="flex items-center gap-2 text-lg font-semibold">
1513
+ <PlayCircle className="h-5 w-5 text-mw-primary-500" />
1514
+ Activate Deal
1515
+ </SheetTitle>
1516
+ <Button
1517
+ variant="ghost"
1518
+ size="sm"
1519
+ onClick={() => setActivateDialogOpen(false)}
1520
+ className="h-8 w-8 p-0"
1521
+ >
1522
+ <X className="h-4 w-4" />
1523
+ </Button>
1524
+ </div>
1525
+ </SheetHeader>
1526
+ <div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
1527
+ <p className="text-sm text-mw-neutral-600 dark:text-mw-neutral-400">
1528
+ Are you sure you want to activate <span className="font-medium text-mw-neutral-900 dark:text-white">"{deal?.name}"</span>?
1529
+ </p>
1530
+ <div className="bg-mw-neutral-50 dark:bg-mw-neutral-800 rounded-lg p-4 space-y-3">
1531
+ <div className="flex justify-between text-sm">
1532
+ <span className="text-mw-neutral-500">Line Items</span>
1533
+ <span className="font-medium text-mw-neutral-900 dark:text-white">{totalItems}</span>
1534
+ </div>
1535
+ <div className="flex justify-between text-sm">
1536
+ <span className="text-mw-neutral-500">Total Impressions</span>
1537
+ <span className="font-medium text-mw-neutral-900 dark:text-white">{totalImpressions.toLocaleString()}</span>
1538
+ </div>
1539
+ <div className="flex justify-between text-sm">
1540
+ <span className="text-mw-neutral-500">Budget</span>
1541
+ <span className="font-medium text-mw-neutral-900 dark:text-white">{deal?.currency || "MYR"} {totalBudget.toLocaleString()}</span>
1542
+ </div>
1543
+ <div className="flex justify-between text-sm">
1544
+ <span className="text-mw-neutral-500">Mode</span>
1545
+ <span className="font-medium text-mw-neutral-900 dark:text-white">{dealMode === 'PROGRAMMATIC' ? 'Programmatic' : 'Traditional'}</span>
1546
+ </div>
1547
+ </div>
1548
+ <p className="text-xs text-mw-neutral-500">
1549
+ Once activated, the deal status will change to Approved or Live based on the start date. This action cannot be undone.
1550
+ </p>
1551
+ </div>
1552
+ <div className="flex-shrink-0 px-6 py-4 border-t border-mw-neutral-200 dark:border-mw-neutral-700 flex items-center justify-end gap-3">
1553
+ <Button variant="outline" onClick={() => setActivateDialogOpen(false)}>
1554
+ Cancel
1555
+ </Button>
1556
+ <Button
1557
+ className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white"
1558
+ disabled={isActivating}
1559
+ onClick={async () => {
1560
+ await handleActivateCampaign();
1561
+ setActivateDialogOpen(false);
1562
+ }}
1563
+ >
1564
+ {isActivating ? (
1565
+ <>
1566
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
1567
+ Activating...
1568
+ </>
1569
+ ) : (
1570
+ <>
1571
+ <PlayCircle className="h-4 w-4 mr-2" />
1572
+ Confirm Activation
1573
+ </>
1574
+ )}
1575
+ </Button>
1576
+ </div>
1577
+ </SheetContent>
1578
+ </Sheet>
1579
+
1580
+ <Dialog open={archiveDialogOpen} onOpenChange={setArchiveDialogOpen}>
1581
+ <DialogContent className="sm:max-w-md">
1582
+ <DialogHeader>
1583
+ <DialogTitle>Move to Archive</DialogTitle>
1584
+ <DialogDescription>
1585
+ Are you sure you want to archive this deal? Archived deals are no longer active but can be reopened later.
1586
+ </DialogDescription>
1587
+ </DialogHeader>
1588
+ <DialogFooter>
1589
+ <Button variant="outline" onClick={() => setArchiveDialogOpen(false)}>
1590
+ Cancel
1591
+ </Button>
1592
+ <Button
1593
+ variant="destructive"
1594
+ disabled={isArchiving}
1595
+ onClick={handleArchiveDeal}
1596
+ >
1597
+ {isArchiving ? (
1598
+ <><Loader2 className="h-4 w-4 mr-2 animate-spin" />Archiving...</>
1599
+ ) : (
1600
+ <><Archive className="h-4 w-4 mr-2" />Archive Deal</>
1601
+ )}
1602
+ </Button>
1603
+ </DialogFooter>
1604
+ </DialogContent>
1605
+ </Dialog>
1606
+
1607
+ <RequestApprovalSheet
1608
+ dealId={dealId!}
1609
+ open={isApprovalSheetOpen}
1610
+ onOpenChange={setIsApprovalSheetOpen}
1611
+ existingEmails={deal?.direct?.approvalEmails || deal?.approvalEmails || []}
1612
+ />
1613
+
1614
+ <ReopenDealSheet
1615
+ dealId={dealId!}
1616
+ open={isReopenSheetOpen}
1617
+ onOpenChange={setIsReopenSheetOpen}
1618
+ dealName={deal?.name}
1619
+ currentStatus={deal?.status}
1620
+ />
1621
+
1622
+ <ResendApprovalSheet
1623
+ dealId={dealId!}
1624
+ open={isResendApprovalSheetOpen}
1625
+ onOpenChange={setIsResendApprovalSheetOpen}
1626
+ existingEmails={deal?.direct?.approvalEmails || deal?.approvalEmails || []}
1627
+ />
1628
+
1629
+ <Sheet open={historyDrawerOpen} onOpenChange={setHistoryDrawerOpen}>
1630
+ <SheetContent className="w-[540px] sm:max-w-[540px] p-0 flex flex-col">
1631
+ <SheetHeader className="px-6 py-4 border-b border-mw-neutral-200 dark:border-mw-neutral-700 flex-shrink-0">
1632
+ <div className="flex items-center justify-between">
1633
+ <SheetTitle className="text-lg font-semibold">Deal History</SheetTitle>
1634
+ <Button
1635
+ variant="ghost"
1636
+ size="sm"
1637
+ onClick={() => setHistoryDrawerOpen(false)}
1638
+ className="h-8 w-8 p-0"
1639
+ >
1640
+ <X className="h-4 w-4" />
1641
+ </Button>
1642
+ </div>
1643
+ </SheetHeader>
1644
+ <div className="flex-1 overflow-y-auto px-6 py-4">
1645
+ {isLoadingHistory ? (
1646
+ <div className="flex items-center justify-center py-16">
1647
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-mw-primary-500" />
1648
+ </div>
1649
+ ) : !historyData?.items?.length ? (
1650
+ <div className="flex flex-col items-center justify-center py-16 text-center">
1651
+ <History className="h-10 w-10 text-mw-neutral-300 mb-3" />
1652
+ <p className="text-sm text-mw-neutral-500">No history entries found</p>
1653
+ </div>
1654
+ ) : (
1655
+ <div className="space-y-4">
1656
+ {historyData.items.map((entry) => (
1657
+ <div key={entry.id} className="flex gap-3 pb-4 border-b border-mw-neutral-100 dark:border-mw-neutral-800 last:border-0">
1658
+ <div className="h-8 w-8 rounded-full bg-mw-neutral-100 dark:bg-mw-neutral-800 flex items-center justify-center flex-shrink-0 mt-0.5">
1659
+ <History className="h-4 w-4 text-mw-neutral-500" />
1660
+ </div>
1661
+ <div className="flex-1 min-w-0">
1662
+ <div className="flex items-center gap-2 mb-1">
1663
+ <Badge variant="outline" className={cn(
1664
+ "text-xs",
1665
+ entry.action === "CREATE" ? "bg-green-50 text-green-700 border-green-200" :
1666
+ entry.action === "UPDATE" ? "bg-blue-50 text-blue-700 border-blue-200" :
1667
+ entry.action === "Status Changed" ? "bg-yellow-50 text-yellow-700 border-yellow-200" :
1668
+ "bg-gray-50 text-gray-700 border-gray-200"
1669
+ )}>
1670
+ {entry.action}
1671
+ </Badge>
1672
+ <Badge variant="outline" className="text-xs">
1673
+ {entry.entityType}
1674
+ </Badge>
1675
+ </div>
1676
+ <p className="text-sm text-mw-neutral-700 dark:text-mw-neutral-300">
1677
+ {entry.summary || entry.metadata?.entityName || entry.fieldLabel || "-"}
1678
+ </p>
1679
+ <div className="flex items-center gap-2 mt-1">
1680
+ {entry.changedByEmail && (
1681
+ <span className="text-xs text-mw-neutral-500">{entry.changedByEmail}</span>
1682
+ )}
1683
+ <span className="text-xs text-mw-neutral-400">
1684
+ {new Date(entry.timestamp).toLocaleDateString("en-US", {
1685
+ month: "short",
1686
+ day: "numeric",
1687
+ year: "numeric",
1688
+ hour: "2-digit",
1689
+ minute: "2-digit",
1690
+ })}
1691
+ </span>
1692
+ </div>
1693
+ </div>
1694
+ </div>
1695
+ ))}
1696
+ </div>
1697
+ )}
1698
+ </div>
1699
+ </SheetContent>
1700
+ </Sheet>
1701
+ </div>
1702
+ );
1703
+ }