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,1051 @@
1
+ import { useState, useMemo, useRef, useEffect } from "react";
2
+ import { useQuery, useMutation } from "@tanstack/react-query";
3
+ import { useParams, useLocation, Link } from "wouter";
4
+ import { format, parse, addDays, differenceInDays, isSameDay } from "date-fns";
5
+ import { ArrowLeft, Info, CalendarIcon, Loader2, Play, History, CheckCircle, XCircle, MessageSquare } from "lucide-react";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
9
+ import { Label } from "@/components/ui/label";
10
+ import { Calendar } from "@/components/ui/calendar";
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from "@/components/ui/popover";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "@/components/ui/select";
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipTrigger,
27
+ } from "@/components/ui/tooltip";
28
+ import {
29
+ Dialog,
30
+ DialogContent,
31
+ DialogDescription,
32
+ DialogFooter,
33
+ DialogHeader,
34
+ DialogTitle,
35
+ } from "@/components/ui/dialog";
36
+ import { Textarea } from "@/components/ui/textarea";
37
+ import { Skeleton } from "@/components/ui/skeleton";
38
+ import { useToast } from "@/hooks/use-toast";
39
+ import { apiRequest, queryClient } from "@/lib/queryClient";
40
+ import { cn } from "@/lib/utils";
41
+ import { HistoryDrawer } from "@/components/history-drawer";
42
+ import type { Deal, LineItem, Creative, LineItemCreative, CreativeSchedule } from "@shared/schema";
43
+
44
+ interface AssignmentWithCreative extends LineItemCreative {
45
+ creative: Creative;
46
+ }
47
+
48
+ const STATUS_OPTIONS = [
49
+ { value: "active", label: "Active" },
50
+ { value: "paused", label: "Paused" },
51
+ { value: "draft", label: "Draft" },
52
+ ];
53
+
54
+ const TIER2_STATUS_STYLES: Record<string, { className: string; label: string }> = {
55
+ pending: { className: "text-amber-600 dark:text-amber-400", label: "Pending" },
56
+ approved: { className: "text-emerald-600 dark:text-emerald-400", label: "Approved" },
57
+ rejected: { className: "text-red-600 dark:text-red-400", label: "Rejected" },
58
+ changes_requested: { className: "text-orange-600 dark:text-orange-400", label: "Changes Requested" },
59
+ };
60
+
61
+ const HOURS = Array.from({ length: 24 }, (_, i) => {
62
+ const hour = (i + 6) % 24;
63
+ return hour.toString().padStart(2, "0");
64
+ });
65
+
66
+ const DAY_NAMES = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
67
+
68
+ function TranscodingRow({
69
+ name,
70
+ isUploaded,
71
+ active,
72
+ scaling,
73
+ duration,
74
+ bitrate,
75
+ dimensions,
76
+ type,
77
+ delivery,
78
+ codecs,
79
+ }: {
80
+ name: string;
81
+ isUploaded: boolean;
82
+ active: boolean;
83
+ scaling: string;
84
+ duration: string;
85
+ bitrate: string;
86
+ dimensions: string;
87
+ type: string;
88
+ delivery: string;
89
+ codecs: string;
90
+ }) {
91
+ return (
92
+ <tr className="border-b last:border-b-0">
93
+ <td className="py-3 pr-4">
94
+ <div className="flex items-center gap-2">
95
+ <span className="font-medium">{name}</span>
96
+ {isUploaded && (
97
+ <span className="text-xs bg-muted px-2 py-0.5 rounded">Uploaded asset</span>
98
+ )}
99
+ </div>
100
+ </td>
101
+ <td className="py-3 px-4">
102
+ <div
103
+ className={cn(
104
+ "w-10 h-5 rounded-full transition-colors relative cursor-pointer",
105
+ active ? "bg-primary" : "bg-muted"
106
+ )}
107
+ data-testid={`toggle-active-${name.slice(0, 20)}`}
108
+ >
109
+ <div
110
+ className={cn(
111
+ "absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform",
112
+ active ? "translate-x-5" : "translate-x-0.5"
113
+ )}
114
+ />
115
+ </div>
116
+ </td>
117
+ <td className="py-3 px-4 text-muted-foreground">{scaling}</td>
118
+ <td className="py-3 px-4">{duration}</td>
119
+ <td className="py-3 px-4">{bitrate}</td>
120
+ <td className="py-3 px-4">{dimensions}</td>
121
+ <td className="py-3 px-4">{type}</td>
122
+ <td className="py-3 px-4">{delivery}</td>
123
+ <td className="py-3 pl-4">{codecs}</td>
124
+ </tr>
125
+ );
126
+ }
127
+
128
+ function formatDateTimeDisplay(dateStr: string | null | undefined): string {
129
+ if (!dateStr) return "-";
130
+ try {
131
+ const d = new Date(dateStr);
132
+ if (isNaN(d.getTime())) return "-";
133
+ return format(d, "MMM dd, yyyy, hh:mm a");
134
+ } catch {
135
+ return "-";
136
+ }
137
+ }
138
+
139
+ function formatDateForRow(date: Date): string {
140
+ return format(date, "MMM-dd");
141
+ }
142
+
143
+ function parseSchedule(schedule: unknown): CreativeSchedule {
144
+ if (!schedule || typeof schedule !== "object") {
145
+ return { allHours: true, selectedHours: {} };
146
+ }
147
+ const s = schedule as CreativeSchedule;
148
+ return {
149
+ allHours: s.allHours !== false,
150
+ selectedHours: s.selectedHours || {},
151
+ };
152
+ }
153
+
154
+ function DateTimePicker({
155
+ value,
156
+ onChange,
157
+ label,
158
+ testId,
159
+ }: {
160
+ value: Date | undefined;
161
+ onChange: (date: Date | undefined) => void;
162
+ label: string;
163
+ testId: string;
164
+ }) {
165
+ const [timeValue, setTimeValue] = useState(value ? format(value, "HH:mm") : "00:00");
166
+
167
+ useEffect(() => {
168
+ if (value) {
169
+ setTimeValue(format(value, "HH:mm"));
170
+ }
171
+ }, [value]);
172
+
173
+ const handleDateSelect = (date: Date | undefined) => {
174
+ if (date) {
175
+ const [hours, minutes] = timeValue.split(":").map(Number);
176
+ date.setHours(hours, minutes, 0, 0);
177
+ onChange(date);
178
+ } else {
179
+ onChange(undefined);
180
+ }
181
+ };
182
+
183
+ const handleTimeChange = (time: string) => {
184
+ setTimeValue(time);
185
+ if (value) {
186
+ const [hours, minutes] = time.split(":").map(Number);
187
+ const newDate = new Date(value);
188
+ newDate.setHours(hours, minutes, 0, 0);
189
+ onChange(newDate);
190
+ }
191
+ };
192
+
193
+ return (
194
+ <div className="flex flex-col gap-1">
195
+ <Popover>
196
+ <PopoverTrigger asChild>
197
+ <Button
198
+ variant="outline"
199
+ className={cn(
200
+ "justify-start text-left font-normal",
201
+ !value && "text-muted-foreground"
202
+ )}
203
+ data-testid={testId}
204
+ >
205
+ <CalendarIcon className="mr-2 h-4 w-4" />
206
+ {value ? format(value, "MMM dd, yyyy hh:mm a") : label}
207
+ </Button>
208
+ </PopoverTrigger>
209
+ <PopoverContent className="w-auto p-0" align="start">
210
+ <Calendar
211
+ mode="single"
212
+ selected={value}
213
+ onSelect={handleDateSelect}
214
+ initialFocus
215
+ />
216
+ <div className="p-3 border-t">
217
+ <Label className="text-xs text-muted-foreground">Time</Label>
218
+ <input
219
+ type="time"
220
+ value={timeValue}
221
+ onChange={(e) => handleTimeChange(e.target.value)}
222
+ className="w-full mt-1 px-2 py-1 border rounded text-sm"
223
+ data-testid={`${testId}-time`}
224
+ />
225
+ </div>
226
+ </PopoverContent>
227
+ </Popover>
228
+ </div>
229
+ );
230
+ }
231
+
232
+ function ScheduleMatrix({
233
+ startDate,
234
+ endDate,
235
+ schedule,
236
+ onScheduleChange,
237
+ }: {
238
+ startDate: Date | undefined;
239
+ endDate: Date | undefined;
240
+ schedule: CreativeSchedule;
241
+ onScheduleChange: (schedule: CreativeSchedule) => void;
242
+ }) {
243
+ const dates = useMemo(() => {
244
+ if (!startDate || !endDate) return [];
245
+ const days = differenceInDays(endDate, startDate) + 1;
246
+ return Array.from({ length: Math.min(days, 30) }, (_, i) => addDays(startDate, i));
247
+ }, [startDate, endDate]);
248
+
249
+ const isHourSelected = (date: Date, hour: number): boolean => {
250
+ if (schedule.allHours) return true;
251
+ const dateKey = format(date, "yyyy-MM-dd");
252
+ const selectedHours = schedule.selectedHours[dateKey] || [];
253
+ return selectedHours.includes(hour);
254
+ };
255
+
256
+ const toggleHour = (date: Date, hour: number) => {
257
+ if (schedule.allHours) return;
258
+
259
+ const dateKey = format(date, "yyyy-MM-dd");
260
+ const newSelectedHours = { ...schedule.selectedHours };
261
+ const currentHours = newSelectedHours[dateKey] || [];
262
+
263
+ if (currentHours.includes(hour)) {
264
+ newSelectedHours[dateKey] = currentHours.filter((h) => h !== hour);
265
+ } else {
266
+ newSelectedHours[dateKey] = [...currentHours, hour].sort((a, b) => a - b);
267
+ }
268
+
269
+ onScheduleChange({ ...schedule, selectedHours: newSelectedHours });
270
+ };
271
+
272
+ const selectAll = () => {
273
+ if (schedule.allHours) return;
274
+
275
+ const newSelectedHours: Record<string, number[]> = {};
276
+ dates.forEach((date) => {
277
+ const dateKey = format(date, "yyyy-MM-dd");
278
+ newSelectedHours[dateKey] = HOURS.map((h) => parseInt(h));
279
+ });
280
+
281
+ onScheduleChange({ ...schedule, selectedHours: newSelectedHours });
282
+ };
283
+
284
+ const toggleAllHours = (value: string) => {
285
+ if (value === "all") {
286
+ onScheduleChange({ allHours: true, selectedHours: {} });
287
+ } else {
288
+ onScheduleChange({ allHours: false, selectedHours: {} });
289
+ }
290
+ };
291
+
292
+ if (!startDate || !endDate) {
293
+ return (
294
+ <div className="text-sm text-muted-foreground text-center py-8">
295
+ Please select start and end dates to configure the schedule.
296
+ </div>
297
+ );
298
+ }
299
+
300
+ return (
301
+ <div className="space-y-4">
302
+ <div className="flex items-center justify-between">
303
+ <RadioGroup
304
+ value={schedule.allHours ? "all" : "specific"}
305
+ onValueChange={toggleAllHours}
306
+ className="flex gap-6"
307
+ >
308
+ <div className="flex items-center gap-2">
309
+ <RadioGroupItem value="all" id="all-hours" data-testid="radio-all-hours" />
310
+ <Label htmlFor="all-hours" className="cursor-pointer">All Hours</Label>
311
+ </div>
312
+ <div className="flex items-center gap-2">
313
+ <RadioGroupItem value="specific" id="specific-hours" data-testid="radio-specific-hours" />
314
+ <Label htmlFor="specific-hours" className="cursor-pointer">Specific Hours</Label>
315
+ </div>
316
+ </RadioGroup>
317
+
318
+ {!schedule.allHours && (
319
+ <Button
320
+ variant="ghost"
321
+ size="sm"
322
+ onClick={selectAll}
323
+ className="text-primary"
324
+ data-testid="button-select-all"
325
+ >
326
+ Select All
327
+ </Button>
328
+ )}
329
+ </div>
330
+
331
+ <div className="overflow-x-auto">
332
+ <table className="w-full text-xs">
333
+ <thead>
334
+ <tr>
335
+ <th className="text-left font-medium text-muted-foreground pb-2 pr-2 sticky left-0 bg-background">DATES</th>
336
+ {HOURS.map((hour) => (
337
+ <th key={hour} className="font-medium text-muted-foreground pb-2 px-1 text-center min-w-[28px]">
338
+ {hour}
339
+ </th>
340
+ ))}
341
+ <th className="font-medium text-muted-foreground pb-2 pl-2 text-right">DAYS</th>
342
+ </tr>
343
+ </thead>
344
+ <tbody>
345
+ {dates.map((date) => (
346
+ <tr key={date.toISOString()}>
347
+ <td className="py-1 pr-2 text-primary font-medium sticky left-0 bg-background">
348
+ {formatDateForRow(date)}
349
+ </td>
350
+ {HOURS.map((hourStr) => {
351
+ const hour = parseInt(hourStr);
352
+ const selected = isHourSelected(date, hour);
353
+
354
+ return (
355
+ <td key={hourStr} className="py-1 px-1 text-center">
356
+ <button
357
+ type="button"
358
+ onClick={() => toggleHour(date, hour)}
359
+ disabled={schedule.allHours}
360
+ className={cn(
361
+ "w-6 h-6 rounded-full transition-colors",
362
+ selected
363
+ ? "bg-primary text-primary-foreground"
364
+ : "bg-muted hover-elevate",
365
+ schedule.allHours && "cursor-default"
366
+ )}
367
+ data-testid={`schedule-cell-${format(date, "yyyy-MM-dd")}-${hourStr}`}
368
+ />
369
+ </td>
370
+ );
371
+ })}
372
+ <td className="py-1 pl-2 text-right text-primary font-medium">
373
+ {DAY_NAMES[date.getDay()]}
374
+ </td>
375
+ </tr>
376
+ ))}
377
+ </tbody>
378
+ </table>
379
+ </div>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ export default function EditCreativeAssignment() {
385
+ const { dealId, lineItemId, assignmentId } = useParams<{
386
+ dealId: string;
387
+ lineItemId: string;
388
+ assignmentId: string;
389
+ }>();
390
+ const [, setLocation] = useLocation();
391
+ const { toast } = useToast();
392
+ const videoRef = useRef<HTMLVideoElement>(null);
393
+ const [isPlaying, setIsPlaying] = useState(false);
394
+ const [currentTime, setCurrentTime] = useState(0);
395
+
396
+ const [status, setStatus] = useState("active");
397
+ const [startDate, setStartDate] = useState<Date | undefined>();
398
+ const [endDate, setEndDate] = useState<Date | undefined>();
399
+ const [schedule, setSchedule] = useState<CreativeSchedule>({ allHours: true, selectedHours: {} });
400
+ const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
401
+ const [rejectionDialogOpen, setRejectionDialogOpen] = useState(false);
402
+ const [rejectionReason, setRejectionReason] = useState("");
403
+ const [pendingAction, setPendingAction] = useState<"rejected" | "changes_requested" | null>(null);
404
+
405
+ const { data: assignment, isLoading: assignmentLoading } = useQuery<AssignmentWithCreative>({
406
+ queryKey: ["/api/line-item-creatives", assignmentId],
407
+ enabled: !!assignmentId,
408
+ });
409
+
410
+ const { data: deal } = useQuery<Deal>({
411
+ queryKey: ["/api/deals", dealId],
412
+ enabled: !!dealId,
413
+ });
414
+
415
+ const { data: lineItem } = useQuery<LineItem>({
416
+ queryKey: ["/api/line-items", lineItemId],
417
+ enabled: !!lineItemId,
418
+ });
419
+
420
+ useEffect(() => {
421
+ if (assignment) {
422
+ if (assignment.startTime) {
423
+ setStartDate(new Date(assignment.startTime));
424
+ }
425
+ if (assignment.endTime) {
426
+ setEndDate(new Date(assignment.endTime));
427
+ }
428
+ if (assignment.schedule) {
429
+ setSchedule(parseSchedule(assignment.schedule));
430
+ }
431
+ }
432
+ }, [assignment]);
433
+
434
+ const updateMutation = useMutation({
435
+ mutationFn: async (data: {
436
+ startTime?: string;
437
+ endTime?: string;
438
+ schedule?: CreativeSchedule;
439
+ tier2Status?: string;
440
+ tier2ReviewedBy?: string;
441
+ tier2RejectionReason?: string;
442
+ }) => {
443
+ return apiRequest("PATCH", `/api/line-item-creatives/${assignmentId}`, data);
444
+ },
445
+ onSuccess: (_, variables) => {
446
+ queryClient.invalidateQueries({ queryKey: ["/api/line-item-creatives", assignmentId] });
447
+ queryClient.invalidateQueries({ queryKey: ["/api/line-items", lineItemId, "creatives"] });
448
+
449
+ if (variables.tier2Status) {
450
+ const statusLabels: Record<string, string> = {
451
+ approved: "approved",
452
+ rejected: "rejected",
453
+ changes_requested: "requested changes for",
454
+ pending: "set to pending for",
455
+ };
456
+ toast({
457
+ title: "Approval status updated",
458
+ description: `You have ${statusLabels[variables.tier2Status] || "updated"} this creative.`,
459
+ });
460
+ } else {
461
+ toast({
462
+ title: "Assignment updated",
463
+ description: "Creative assignment has been updated successfully.",
464
+ });
465
+ setLocation(`/deals/${dealId}/line-items/${lineItemId}/creatives`);
466
+ }
467
+ },
468
+ onError: (error: Error) => {
469
+ toast({
470
+ title: "Failed to update assignment",
471
+ description: error.message,
472
+ variant: "destructive",
473
+ });
474
+ },
475
+ });
476
+
477
+ const handleSave = () => {
478
+ const data: {
479
+ startTime?: string;
480
+ endTime?: string;
481
+ schedule?: CreativeSchedule;
482
+ tier2Status?: string;
483
+ } = {};
484
+
485
+ if (startDate) {
486
+ data.startTime = startDate.toISOString();
487
+ }
488
+ if (endDate) {
489
+ data.endTime = endDate.toISOString();
490
+ }
491
+ data.schedule = schedule;
492
+
493
+ updateMutation.mutate(data);
494
+ };
495
+
496
+ const handleCancel = () => {
497
+ setLocation(`/deals/${dealId}/line-items/${lineItemId}/creatives`);
498
+ };
499
+
500
+ const handleVideoTimeUpdate = () => {
501
+ if (videoRef.current) {
502
+ setCurrentTime(videoRef.current.currentTime);
503
+ }
504
+ };
505
+
506
+ const togglePlay = () => {
507
+ if (videoRef.current) {
508
+ if (isPlaying) {
509
+ videoRef.current.pause();
510
+ } else {
511
+ videoRef.current.play();
512
+ }
513
+ setIsPlaying(!isPlaying);
514
+ }
515
+ };
516
+
517
+ const formatDuration = (seconds: number): string => {
518
+ const mins = Math.floor(seconds / 60);
519
+ const secs = Math.floor(seconds % 60);
520
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
521
+ };
522
+
523
+ const handleTier2StatusChange = (newStatus: string) => {
524
+ if (newStatus === "rejected" || newStatus === "changes_requested") {
525
+ setPendingAction(newStatus);
526
+ setRejectionReason("");
527
+ setRejectionDialogOpen(true);
528
+ } else {
529
+ updateMutation.mutate({
530
+ tier2Status: newStatus,
531
+ tier2ReviewedBy: "Media Owner",
532
+ });
533
+ }
534
+ };
535
+
536
+ const handleSubmitRejection = () => {
537
+ if (!pendingAction) return;
538
+ if (!rejectionReason.trim()) {
539
+ toast({
540
+ title: "Reason required",
541
+ description: pendingAction === "rejected"
542
+ ? "Please provide a reason for rejecting this creative."
543
+ : "Please describe what changes are needed.",
544
+ variant: "destructive",
545
+ });
546
+ return;
547
+ }
548
+
549
+ updateMutation.mutate({
550
+ tier2Status: pendingAction,
551
+ tier2ReviewedBy: "Media Owner",
552
+ tier2RejectionReason: rejectionReason.trim(),
553
+ });
554
+ setRejectionDialogOpen(false);
555
+ setPendingAction(null);
556
+ setRejectionReason("");
557
+ };
558
+
559
+ if (assignmentLoading) {
560
+ return (
561
+ <div className="flex flex-col h-full">
562
+ <div className="p-6 space-y-6">
563
+ <Skeleton className="h-8 w-64" />
564
+ <Skeleton className="h-4 w-full max-w-2xl" />
565
+ <div className="grid grid-cols-3 gap-6">
566
+ <div className="col-span-2 space-y-4">
567
+ <Skeleton className="h-32 w-full" />
568
+ <Skeleton className="h-64 w-full" />
569
+ </div>
570
+ <div className="space-y-4">
571
+ <Skeleton className="h-32 w-full" />
572
+ <Skeleton className="h-48 w-full" />
573
+ </div>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ );
578
+ }
579
+
580
+ if (!assignment) {
581
+ return (
582
+ <div className="flex flex-col items-center justify-center h-full gap-4">
583
+ <p className="text-muted-foreground">Creative assignment not found.</p>
584
+ <Button variant="outline" onClick={handleCancel} data-testid="button-back-not-found">
585
+ <ArrowLeft className="h-4 w-4 mr-2" />
586
+ Back
587
+ </Button>
588
+ </div>
589
+ );
590
+ }
591
+
592
+ const creative = assignment.creative;
593
+ const tier2Style = TIER2_STATUS_STYLES[assignment.tier2Status] || TIER2_STATUS_STYLES.pending;
594
+ const isVideo = creative.type === "video";
595
+
596
+ return (
597
+ <div className="flex flex-col h-full">
598
+ <div className="flex-1 overflow-auto p-6 pb-24">
599
+ <div className="flex items-start justify-between gap-4 mb-4">
600
+ <h1 className="text-2xl font-semibold" data-testid="text-creative-name">
601
+ {creative.name}
602
+ </h1>
603
+ <div className="flex items-center gap-3">
604
+ <Select value={status} onValueChange={setStatus}>
605
+ <SelectTrigger className="w-32" data-testid="select-status">
606
+ <SelectValue />
607
+ </SelectTrigger>
608
+ <SelectContent>
609
+ {STATUS_OPTIONS.map((opt) => (
610
+ <SelectItem key={opt.value} value={opt.value}>
611
+ {opt.label}
612
+ </SelectItem>
613
+ ))}
614
+ </SelectContent>
615
+ </Select>
616
+ <Button
617
+ variant="outline"
618
+ size="icon"
619
+ onClick={() => setHistoryDrawerOpen(true)}
620
+ data-testid="button-view-history"
621
+ >
622
+ <History className="h-4 w-4" />
623
+ </Button>
624
+ <Button
625
+ variant="outline"
626
+ onClick={handleCancel}
627
+ data-testid="button-back"
628
+ >
629
+ <ArrowLeft className="h-4 w-4 mr-2" />
630
+ Back
631
+ </Button>
632
+ </div>
633
+ </div>
634
+
635
+ <p className="text-sm text-muted-foreground mb-6" data-testid="text-info-message">
636
+ Please configure trigger with line item to select your preferred rule. Multiple creatives scheduled on same date and time will be played in sequence.
637
+ </p>
638
+
639
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
640
+ <div className="lg:col-span-2 space-y-6">
641
+ <Card>
642
+ <CardContent className="pt-6">
643
+ <div className="flex items-center gap-2 mb-4">
644
+ <Label className="text-sm font-medium">Date</Label>
645
+ <Tooltip>
646
+ <TooltipTrigger asChild>
647
+ <Info className="h-4 w-4 text-muted-foreground cursor-help" />
648
+ </TooltipTrigger>
649
+ <TooltipContent>
650
+ <p>Set the date range for when this creative will be active.</p>
651
+ </TooltipContent>
652
+ </Tooltip>
653
+ </div>
654
+ <div className="flex items-center gap-4 flex-wrap">
655
+ <DateTimePicker
656
+ value={startDate}
657
+ onChange={setStartDate}
658
+ label="Start date/time"
659
+ testId="datepicker-start"
660
+ />
661
+ <span className="text-muted-foreground">to</span>
662
+ <DateTimePicker
663
+ value={endDate}
664
+ onChange={setEndDate}
665
+ label="End date/time"
666
+ testId="datepicker-end"
667
+ />
668
+ </div>
669
+ </CardContent>
670
+ </Card>
671
+
672
+ <Card>
673
+ <CardHeader>
674
+ <CardTitle>Schedule</CardTitle>
675
+ </CardHeader>
676
+ <CardContent>
677
+ <ScheduleMatrix
678
+ startDate={startDate}
679
+ endDate={endDate}
680
+ schedule={schedule}
681
+ onScheduleChange={setSchedule}
682
+ />
683
+ </CardContent>
684
+ </Card>
685
+ </div>
686
+
687
+ <div className="space-y-6">
688
+ <Card>
689
+ <CardContent className="pt-6">
690
+ <div className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
691
+ <div>
692
+ <span className="text-muted-foreground">Resolution:</span>
693
+ </div>
694
+ <div className="font-medium" data-testid="text-resolution">
695
+ {creative.width}x{creative.height} px (W x H)
696
+ </div>
697
+
698
+ <div>
699
+ <span className="text-muted-foreground">Duration:</span>
700
+ </div>
701
+ <div className="font-medium" data-testid="text-duration">
702
+ {creative.duration ? `${creative.duration} sec` : "-"}
703
+ </div>
704
+
705
+ <div>
706
+ <span className="text-muted-foreground">Status:</span>
707
+ </div>
708
+ <div className={cn("font-medium", tier2Style.className)} data-testid="text-status">
709
+ {tier2Style.label}
710
+ </div>
711
+
712
+ <div>
713
+ <span className="text-muted-foreground">Start Time:</span>
714
+ </div>
715
+ <div className="font-medium" data-testid="text-start-time">
716
+ {formatDateTimeDisplay(assignment.startTime)}
717
+ </div>
718
+
719
+ <div>
720
+ <span className="text-muted-foreground">End Time:</span>
721
+ </div>
722
+ <div className="font-medium" data-testid="text-end-time">
723
+ {formatDateTimeDisplay(assignment.endTime)}
724
+ </div>
725
+
726
+ <div>
727
+ <span className="text-muted-foreground">Format:</span>
728
+ </div>
729
+ <div className="font-medium uppercase" data-testid="text-format">
730
+ {creative.type === "video" ? "VIDEO" : "DISPLAY"}
731
+ </div>
732
+
733
+ <div>
734
+ <span className="text-muted-foreground">Source:</span>
735
+ </div>
736
+ <div className="font-medium" data-testid="text-source">
737
+ {(() => {
738
+ const source = creative.creativeSource || "uploaded";
739
+ const platform = creative.sourcePlatform || "influence";
740
+
741
+ const platformLabels: Record<string, string> = {
742
+ planner: "Planner",
743
+ activate: "Activate",
744
+ influence: "Influence",
745
+ media_owner: "Media Owner",
746
+ };
747
+
748
+ const sourceLabels: Record<string, string> = {
749
+ uploaded: "Uploaded",
750
+ bid_stream_vast: "Bid Stream",
751
+ api: "API",
752
+ media_owner: "Direct",
753
+ };
754
+
755
+ return platform === "influence"
756
+ ? sourceLabels[source] || "Uploaded"
757
+ : `${platformLabels[platform] || platform} - ${sourceLabels[source] || source}`;
758
+ })()}
759
+ </div>
760
+ </div>
761
+ </CardContent>
762
+ </Card>
763
+
764
+ <Card>
765
+ <CardHeader>
766
+ <CardTitle>Media Owner Approval</CardTitle>
767
+ </CardHeader>
768
+ <CardContent>
769
+ <div className="space-y-4">
770
+ <div className="flex items-center justify-between">
771
+ <span className="text-sm text-muted-foreground">Current Status</span>
772
+ <span className={cn("font-medium", tier2Style.className)}>{tier2Style.label}</span>
773
+ </div>
774
+ <div className="flex flex-col gap-2">
775
+ <Button
776
+ variant={assignment.tier2Status === "approved" ? "default" : "outline"}
777
+ className="w-full justify-start"
778
+ onClick={() => handleTier2StatusChange("approved")}
779
+ disabled={updateMutation.isPending}
780
+ data-testid="button-approve"
781
+ >
782
+ <CheckCircle className="h-4 w-4 mr-2" />
783
+ Approve
784
+ </Button>
785
+ <Button
786
+ variant={assignment.tier2Status === "changes_requested" ? "default" : "outline"}
787
+ className="w-full justify-start"
788
+ onClick={() => handleTier2StatusChange("changes_requested")}
789
+ disabled={updateMutation.isPending}
790
+ data-testid="button-request-changes"
791
+ >
792
+ <MessageSquare className="h-4 w-4 mr-2" />
793
+ Request Changes
794
+ </Button>
795
+ <Button
796
+ variant={assignment.tier2Status === "rejected" ? "destructive" : "outline"}
797
+ className="w-full justify-start"
798
+ onClick={() => handleTier2StatusChange("rejected")}
799
+ disabled={updateMutation.isPending}
800
+ data-testid="button-reject"
801
+ >
802
+ <XCircle className="h-4 w-4 mr-2" />
803
+ Reject
804
+ </Button>
805
+ </div>
806
+ </div>
807
+ </CardContent>
808
+ </Card>
809
+
810
+ <Card>
811
+ <CardHeader>
812
+ <CardTitle>Preview</CardTitle>
813
+ </CardHeader>
814
+ <CardContent>
815
+ <div className="relative aspect-video bg-muted rounded-md overflow-hidden">
816
+ {isVideo && creative.fileUrl ? (
817
+ <>
818
+ <video
819
+ ref={videoRef}
820
+ src={creative.fileUrl}
821
+ className="w-full h-full object-contain"
822
+ onTimeUpdate={handleVideoTimeUpdate}
823
+ onEnded={() => setIsPlaying(false)}
824
+ data-testid="video-preview"
825
+ />
826
+ <button
827
+ onClick={togglePlay}
828
+ className="absolute inset-0 flex items-center justify-center bg-black/20 hover:bg-black/30 transition-colors"
829
+ data-testid="button-play-video"
830
+ >
831
+ {!isPlaying && (
832
+ <div className="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center">
833
+ <Play className="h-5 w-5 text-black ml-1" />
834
+ </div>
835
+ )}
836
+ </button>
837
+ <div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded">
838
+ {formatDuration(currentTime)} / {formatDuration(creative.duration || 0)}
839
+ </div>
840
+ </>
841
+ ) : creative.thumbnailUrl ? (
842
+ <img
843
+ src={creative.thumbnailUrl}
844
+ alt={creative.name}
845
+ className="w-full h-full object-contain"
846
+ data-testid="image-preview"
847
+ />
848
+ ) : (
849
+ <div className="flex items-center justify-center h-full text-muted-foreground">
850
+ No preview available
851
+ </div>
852
+ )}
853
+ </div>
854
+ </CardContent>
855
+ </Card>
856
+
857
+ <Card>
858
+ <CardHeader>
859
+ <CardTitle>Creative History</CardTitle>
860
+ </CardHeader>
861
+ <CardContent>
862
+ <p className="text-sm text-muted-foreground" data-testid="text-no-history">
863
+ There is no available creative history.
864
+ </p>
865
+ </CardContent>
866
+ </Card>
867
+ </div>
868
+ </div>
869
+
870
+ <Card className="mt-6">
871
+ <CardHeader className="flex flex-row items-center justify-between">
872
+ <div className="space-y-1">
873
+ <CardTitle className="text-base">Transcoding</CardTitle>
874
+ <p className="text-sm text-muted-foreground">
875
+ <span className="font-medium">ID:</span> {creative.id.slice(0, 12)} |
876
+ <span className="font-medium ml-2">Master type:</span> {creative.type === "video" ? "Video" : "Display"} |
877
+ <span className="font-medium ml-2">Actual size:</span> {creative.width}x{creative.height}
878
+ </p>
879
+ </div>
880
+ <Button variant="ghost" className="text-primary" data-testid="link-master-companions">
881
+ Master/companions
882
+ </Button>
883
+ </CardHeader>
884
+ <CardContent>
885
+ <div className="overflow-x-auto">
886
+ <table className="w-full text-sm">
887
+ <thead>
888
+ <tr className="border-b">
889
+ <th className="text-left font-medium py-2 pr-4">Assets</th>
890
+ <th className="text-left font-medium py-2 px-4">Active</th>
891
+ <th className="text-left font-medium py-2 px-4">Scaling</th>
892
+ <th className="text-left font-medium py-2 px-4">Duration</th>
893
+ <th className="text-left font-medium py-2 px-4">Bitrate</th>
894
+ <th className="text-left font-medium py-2 px-4">Dimensions</th>
895
+ <th className="text-left font-medium py-2 px-4">Type</th>
896
+ <th className="text-left font-medium py-2 px-4">Delivery</th>
897
+ <th className="text-left font-medium py-2 pl-4">Codecs</th>
898
+ </tr>
899
+ </thead>
900
+ <tbody>
901
+ <TranscodingRow
902
+ name={creative.name}
903
+ isUploaded={true}
904
+ active={true}
905
+ scaling="Maintain aspect ratio"
906
+ duration={`${((creative.duration || 10) / 1000).toFixed(3)}s`}
907
+ bitrate="7254 kbit/s"
908
+ dimensions={`${creative.width}x${creative.height}`}
909
+ type={creative.format || "video/mp4"}
910
+ delivery="Progressive"
911
+ codecs=""
912
+ />
913
+ <TranscodingRow
914
+ name={`${creative.name.replace(/\.[^/.]+$/, "")}-ultralow.m3u8`}
915
+ isUploaded={false}
916
+ active={true}
917
+ scaling="Maintain aspect ratio"
918
+ duration={`${((creative.duration || 10) / 1000 + 0.001).toFixed(2)}s`}
919
+ bitrate="50 to 3543 kbit/s"
920
+ dimensions="186x240"
921
+ type="application/x-mpegurl"
922
+ delivery="Streaming"
923
+ codecs=""
924
+ />
925
+ <TranscodingRow
926
+ name={`${creative.name.replace(/\.[^/.]+$/, "")}-aac480pQ1.mp4`}
927
+ isUploaded={false}
928
+ active={true}
929
+ scaling="Maintain aspect ratio"
930
+ duration={`${((creative.duration || 10) / 1000 + 0.001).toFixed(2)}s`}
931
+ bitrate="894 kbit/s"
932
+ dimensions="374x480"
933
+ type="video/mp4"
934
+ delivery="Progressive"
935
+ codecs=""
936
+ />
937
+ <TranscodingRow
938
+ name={`${creative.name.replace(/\.[^/.]+$/, "")}-aac720pQ2.mp4`}
939
+ isUploaded={false}
940
+ active={true}
941
+ scaling="Maintain aspect ratio"
942
+ duration={`${((creative.duration || 10) / 1000 + 0.001).toFixed(2)}s`}
943
+ bitrate="2135 kbit/s"
944
+ dimensions="560x720"
945
+ type="video/mp4"
946
+ delivery="Progressive"
947
+ codecs=""
948
+ />
949
+ <TranscodingRow
950
+ name={`${creative.name.replace(/\.[^/.]+$/, "")}-higher.mp4`}
951
+ isUploaded={false}
952
+ active={true}
953
+ scaling="Maintain aspect ratio"
954
+ duration={`${((creative.duration || 10) / 1000 + 0.001).toFixed(2)}s`}
955
+ bitrate="928 kbit/s"
956
+ dimensions="560x720"
957
+ type="video/mp4"
958
+ delivery="Progressive"
959
+ codecs=""
960
+ />
961
+ <TranscodingRow
962
+ name={`${creative.name.replace(/\.[^/.]+$/, "")}-highest.mp4`}
963
+ isUploaded={false}
964
+ active={true}
965
+ scaling="Maintain aspect ratio"
966
+ duration={`${((creative.duration || 10) / 1000 + 0.001).toFixed(2)}s`}
967
+ bitrate="1544 kbit/s"
968
+ dimensions="840x1080"
969
+ type="video/mp4"
970
+ delivery="Progressive"
971
+ codecs=""
972
+ />
973
+ </tbody>
974
+ </table>
975
+ </div>
976
+ </CardContent>
977
+ </Card>
978
+ </div>
979
+
980
+ <div className="fixed bottom-0 left-64 right-0 border-t bg-background p-4 flex items-center justify-end gap-3">
981
+ <Button
982
+ variant="outline"
983
+ onClick={handleCancel}
984
+ disabled={updateMutation.isPending}
985
+ data-testid="button-cancel"
986
+ >
987
+ Cancel
988
+ </Button>
989
+ <Button
990
+ onClick={handleSave}
991
+ disabled={updateMutation.isPending}
992
+ data-testid="button-save"
993
+ >
994
+ {updateMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
995
+ Save
996
+ </Button>
997
+ </div>
998
+
999
+ <HistoryDrawer
1000
+ open={historyDrawerOpen}
1001
+ onOpenChange={setHistoryDrawerOpen}
1002
+ entityId={assignment?.creativeId || ""}
1003
+ entityType="creative"
1004
+ entityName={assignment?.creative?.name}
1005
+ hierarchical={false}
1006
+ />
1007
+
1008
+ <Dialog open={rejectionDialogOpen} onOpenChange={setRejectionDialogOpen}>
1009
+ <DialogContent>
1010
+ <DialogHeader>
1011
+ <DialogTitle>
1012
+ {pendingAction === "rejected" ? "Reject Creative" : "Request Changes"}
1013
+ </DialogTitle>
1014
+ <DialogDescription>
1015
+ {pendingAction === "rejected"
1016
+ ? "Please provide a reason for rejecting this creative. This helps the content owner understand what needs to be fixed."
1017
+ : "Please describe what changes are needed for this creative to be approved."}
1018
+ </DialogDescription>
1019
+ </DialogHeader>
1020
+ <div className="py-4">
1021
+ <Textarea
1022
+ placeholder={pendingAction === "rejected" ? "Enter rejection reason..." : "Describe the changes needed..."}
1023
+ value={rejectionReason}
1024
+ onChange={(e) => setRejectionReason(e.target.value)}
1025
+ className="min-h-[100px]"
1026
+ data-testid="textarea-rejection-reason"
1027
+ />
1028
+ </div>
1029
+ <DialogFooter>
1030
+ <Button
1031
+ variant="outline"
1032
+ onClick={() => setRejectionDialogOpen(false)}
1033
+ data-testid="button-cancel-rejection"
1034
+ >
1035
+ Cancel
1036
+ </Button>
1037
+ <Button
1038
+ variant={pendingAction === "rejected" ? "destructive" : "default"}
1039
+ onClick={handleSubmitRejection}
1040
+ disabled={updateMutation.isPending}
1041
+ data-testid="button-confirm-rejection"
1042
+ >
1043
+ {updateMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
1044
+ {pendingAction === "rejected" ? "Reject" : "Request Changes"}
1045
+ </Button>
1046
+ </DialogFooter>
1047
+ </DialogContent>
1048
+ </Dialog>
1049
+ </div>
1050
+ );
1051
+ }