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,75 @@
1
+ export interface POIType {
2
+ id: string;
3
+ label: string;
4
+ category: string;
5
+ }
6
+
7
+ export const POI_TYPES: POIType[] = [
8
+ { id: 'rest_stop', label: 'Rest Stop', category: 'Travel' },
9
+ { id: 'public_bath', label: 'Public Bath', category: 'Services' },
10
+ { id: 'rv_park', label: 'RV Park', category: 'Travel' },
11
+ { id: 'restaurant', label: 'Restaurant', category: 'Food & Dining' },
12
+ { id: 'cafe', label: 'Cafe', category: 'Food & Dining' },
13
+ { id: 'fast_food', label: 'Fast Food', category: 'Food & Dining' },
14
+ { id: 'bar', label: 'Bar', category: 'Food & Dining' },
15
+ { id: 'hospital', label: 'Hospital', category: 'Healthcare' },
16
+ { id: 'pharmacy', label: 'Pharmacy', category: 'Healthcare' },
17
+ { id: 'doctor', label: 'Doctor', category: 'Healthcare' },
18
+ { id: 'dentist', label: 'Dentist', category: 'Healthcare' },
19
+ { id: 'school', label: 'School', category: 'Education' },
20
+ { id: 'university', label: 'University', category: 'Education' },
21
+ { id: 'library', label: 'Library', category: 'Education' },
22
+ { id: 'gas_station', label: 'Gas Station', category: 'Automotive' },
23
+ { id: 'car_wash', label: 'Car Wash', category: 'Automotive' },
24
+ { id: 'car_repair', label: 'Car Repair', category: 'Automotive' },
25
+ { id: 'parking', label: 'Parking', category: 'Automotive' },
26
+ { id: 'shopping_mall', label: 'Shopping Mall', category: 'Shopping' },
27
+ { id: 'supermarket', label: 'Supermarket', category: 'Shopping' },
28
+ { id: 'convenience_store', label: 'Convenience Store', category: 'Shopping' },
29
+ { id: 'clothing_store', label: 'Clothing Store', category: 'Shopping' },
30
+ { id: 'gym', label: 'Gym', category: 'Fitness' },
31
+ { id: 'spa', label: 'Spa', category: 'Fitness' },
32
+ { id: 'bank', label: 'Bank', category: 'Finance' },
33
+ { id: 'atm', label: 'ATM', category: 'Finance' },
34
+ { id: 'hotel', label: 'Hotel', category: 'Lodging' },
35
+ { id: 'motel', label: 'Motel', category: 'Lodging' },
36
+ { id: 'campground', label: 'Campground', category: 'Lodging' },
37
+ { id: 'airport', label: 'Airport', category: 'Transportation' },
38
+ { id: 'train_station', label: 'Train Station', category: 'Transportation' },
39
+ { id: 'bus_station', label: 'Bus Station', category: 'Transportation' },
40
+ { id: 'transit_station', label: 'Transit Station', category: 'Transportation' },
41
+ { id: 'taxi_stand', label: 'Taxi Stand', category: 'Transportation' },
42
+ { id: 'park', label: 'Park', category: 'Recreation' },
43
+ { id: 'amusement_park', label: 'Amusement Park', category: 'Recreation' },
44
+ { id: 'zoo', label: 'Zoo', category: 'Recreation' },
45
+ { id: 'aquarium', label: 'Aquarium', category: 'Recreation' },
46
+ { id: 'museum', label: 'Museum', category: 'Culture' },
47
+ { id: 'art_gallery', label: 'Art Gallery', category: 'Culture' },
48
+ { id: 'movie_theater', label: 'Movie Theater', category: 'Entertainment' },
49
+ { id: 'night_club', label: 'Night Club', category: 'Entertainment' },
50
+ { id: 'bowling_alley', label: 'Bowling Alley', category: 'Entertainment' },
51
+ { id: 'casino', label: 'Casino', category: 'Entertainment' },
52
+ { id: 'stadium', label: 'Stadium', category: 'Sports' },
53
+ { id: 'post_office', label: 'Post Office', category: 'Services' },
54
+ { id: 'laundry', label: 'Laundry', category: 'Services' },
55
+ { id: 'beauty_salon', label: 'Beauty Salon', category: 'Services' },
56
+ { id: 'hair_care', label: 'Hair Care', category: 'Services' },
57
+ { id: 'church', label: 'Church', category: 'Religious' },
58
+ { id: 'mosque', label: 'Mosque', category: 'Religious' },
59
+ { id: 'temple', label: 'Temple', category: 'Religious' },
60
+ { id: 'synagogue', label: 'Synagogue', category: 'Religious' },
61
+ ];
62
+
63
+ export const POI_CATEGORIES = [...new Set(POI_TYPES.map(poi => poi.category))];
64
+
65
+ export function getPOIsByCategory(category: string): POIType[] {
66
+ return POI_TYPES.filter(poi => poi.category === category);
67
+ }
68
+
69
+ export function getPOIById(id: string): POIType | undefined {
70
+ return POI_TYPES.find(poi => poi.id === id);
71
+ }
72
+
73
+ export function getPOILabel(id: string): string {
74
+ return getPOIById(id)?.label || id;
75
+ }
@@ -0,0 +1,144 @@
1
+ import { QueryClient, QueryFunction } from "@tanstack/react-query";
2
+ import * as oauthService from "./oauth-service";
3
+
4
+ const DIRECT_CAMPAIGNS_API_BASE_URL = import.meta.env.VITE_OAUTH_API_URL;
5
+
6
+ /**
7
+ * Convert relative paths to absolute URLs
8
+ * Handles /api/direct-io/* paths by prepending the Direct Campaigns API base URL
9
+ */
10
+ function buildAbsoluteUrl(url: string): string {
11
+ // If URL is already absolute, return as-is
12
+ if (url.startsWith('http://') || url.startsWith('https://')) {
13
+ return url;
14
+ }
15
+
16
+ // If URL is a /api/direct-io/* path, prepend the Direct Campaigns API base URL
17
+ if (url.startsWith('/api/direct-io/')) {
18
+ // Replace /api/direct-io/ with the full Direct Campaigns API base URL
19
+ const path = url.replace('/api/direct-io', '');
20
+ return `${DIRECT_CAMPAIGNS_API_BASE_URL}/api/v1/rest${path}`;
21
+ }
22
+
23
+ // For other relative paths (local backend), keep as relative
24
+ return url;
25
+ }
26
+
27
+ async function throwIfResNotOk(res: Response) {
28
+ if (!res.ok) {
29
+ const text = (await res.text()) || res.statusText;
30
+ throw new Error(`${res.status}: ${text}`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Enhanced fetch with automatic token refresh on 401
36
+ * Properly preserves request body and headers for retry
37
+ * Automatically injects Authorization header with Bearer token
38
+ */
39
+ export async function fetchWithTokenRefresh(
40
+ url: string,
41
+ options: RequestInit = {},
42
+ isRetry = false
43
+ ): Promise<Response> {
44
+ // Convert relative paths to absolute URLs
45
+ const absoluteUrl = buildAbsoluteUrl(url);
46
+
47
+ // Clone the options to preserve original body for potential retry
48
+ const clonedOptions = { ...options };
49
+
50
+ // If body exists and is a string/object, preserve it
51
+ if (options.body) {
52
+ // Body can only be read once, so we need to preserve it
53
+ clonedOptions.body = options.body;
54
+ }
55
+
56
+ // Initialize headers if not present
57
+ const headers = new Headers(clonedOptions.headers || {});
58
+
59
+ // Automatically inject OAuth token into Authorization header
60
+ const accessToken = oauthService.getAccessToken();
61
+ if (accessToken) {
62
+ headers.set('Authorization', `Bearer ${accessToken}`);
63
+ }
64
+
65
+ const res = await fetch(absoluteUrl, {
66
+ ...clonedOptions,
67
+ headers,
68
+ });
69
+
70
+ // If we get a 401 and this is not a retry, attempt token refresh
71
+ if (res.status === 401 && !isRetry && oauthService.isAuthenticated()) {
72
+ try {
73
+ // Attempt to refresh the token
74
+ await oauthService.refreshAccessToken();
75
+
76
+ // Retry the original request with the same options
77
+ // The new access token will be automatically injected
78
+ return await fetchWithTokenRefresh(url, options, true);
79
+ } catch (refreshError) {
80
+ // If refresh fails, clear auth state and throw
81
+ console.error('Token refresh failed:', refreshError);
82
+ oauthService.clearAuthStorage();
83
+ throw new Error('Session expired. Please log in again.');
84
+ }
85
+ }
86
+
87
+ return res;
88
+ }
89
+
90
+ export { buildAbsoluteUrl };
91
+
92
+ export async function apiRequest(
93
+ method: string,
94
+ url: string,
95
+ data?: unknown | undefined,
96
+ ): Promise<Response> {
97
+ const res = await fetchWithTokenRefresh(url, {
98
+ method,
99
+ headers: data ? { "Content-Type": "application/json" } : {},
100
+ body: data ? JSON.stringify(data) : undefined,
101
+ });
102
+
103
+ await throwIfResNotOk(res);
104
+ return res;
105
+ }
106
+
107
+ type UnauthorizedBehavior = "returnNull" | "throw";
108
+ export const getQueryFn: <T>(options: {
109
+ on401: UnauthorizedBehavior;
110
+ }) => QueryFunction<T> =
111
+ ({ on401: unauthorizedBehavior }) =>
112
+ async ({ queryKey }) => {
113
+ // Join query key to create URL path
114
+ const urlPath = queryKey.join("/") as string;
115
+
116
+ // Log for debugging
117
+ if (urlPath.includes('/api/direct-io/')) {
118
+ console.log('[QueryClient] Fetching from Direct Campaigns API:', urlPath);
119
+ }
120
+
121
+ const res = await fetchWithTokenRefresh(urlPath);
122
+
123
+ if (unauthorizedBehavior === "returnNull" && res.status === 401) {
124
+ return null;
125
+ }
126
+
127
+ await throwIfResNotOk(res);
128
+ return await res.json();
129
+ };
130
+
131
+ export const queryClient = new QueryClient({
132
+ defaultOptions: {
133
+ queries: {
134
+ queryFn: getQueryFn({ on401: "throw" }),
135
+ refetchInterval: false,
136
+ refetchOnWindowFocus: false,
137
+ staleTime: Infinity,
138
+ retry: false,
139
+ },
140
+ mutations: {
141
+ retry: false,
142
+ },
143
+ },
144
+ });
@@ -0,0 +1,380 @@
1
+ import { getAccessToken } from './oauth-service';
2
+ import { API_CONFIG } from './api-config';
3
+
4
+ const RECOMMENDATION_API_BASE = API_CONFIG.recommendation || `${API_CONFIG.inventory}/api/v1/recommendation`;
5
+
6
+ export interface GeographyTargeting {
7
+ cities?: string[];
8
+ states?: string[];
9
+ geofences?: Array<{
10
+ type: string;
11
+ coordinates: number[][];
12
+ centerLng: number;
13
+ centerLat: number;
14
+ }>;
15
+ }
16
+
17
+ export interface AudienceTargeting {
18
+ audienceSegments?: string[];
19
+ demographics?: Record<string, string[]>;
20
+ }
21
+
22
+ export interface RecommendationRequest {
23
+ country: string;
24
+ startDate: string;
25
+ endDate: string;
26
+ productId?: string;
27
+ companyId?: string;
28
+ mediaOwnerIds?: string[];
29
+ geographyTargeting?: GeographyTargeting;
30
+ audienceTargeting?: AudienceTargeting;
31
+ brandId?: string;
32
+ budget?: number;
33
+ goal?: 'IMPRESSIONS' | 'REACH' | 'SOV';
34
+ goalValue?: number;
35
+ excludedInventoryIds?: string[];
36
+ excludedIabCategories?: string[];
37
+ topN?: number;
38
+ }
39
+
40
+ export interface RecommendationMetadata {
41
+ totalInventoriesScored?: number;
42
+ topNReturned?: number;
43
+ processingTimeMs?: number;
44
+ }
45
+
46
+ export interface RecommendationStatusResponse {
47
+ runId: string;
48
+ status: 'IN_PROGRESS' | 'COMPLETED';
49
+ completionPercentage?: number;
50
+ campaignId?: string;
51
+ productId?: string;
52
+ companyId?: string;
53
+ generatedAt?: string;
54
+ metadata?: RecommendationMetadata;
55
+ warnings?: string[];
56
+ }
57
+
58
+ export interface ComponentScores {
59
+ measureFit?: number;
60
+ geoFit?: number;
61
+ availability?: number;
62
+ budgetFit?: number;
63
+ audienceFit?: number;
64
+ brandFit?: number;
65
+ qualityFit?: number;
66
+ timeFit?: number;
67
+ }
68
+
69
+ export interface AvailabilitySummary {
70
+ availableSlots?: number;
71
+ totalSlots?: number;
72
+ availableDays?: number;
73
+ totalDays?: number;
74
+ availabilityPercentage?: number;
75
+ summary?: string;
76
+ allAvailable?: boolean;
77
+ }
78
+
79
+ export interface ForecastedMetrics {
80
+ estimatedImpressions?: number;
81
+ estimatedReach?: number;
82
+ estimatedSov?: number;
83
+ estimatedFrequency?: number;
84
+ }
85
+
86
+ export interface CostEstimate {
87
+ estimatedCost?: number;
88
+ currency?: string;
89
+ costPerImpression?: number;
90
+ }
91
+
92
+ export interface InventoryLocation {
93
+ countryId?: string;
94
+ countryName?: string;
95
+ stateId?: string;
96
+ stateName?: string;
97
+ cityId?: string;
98
+ cityName?: string;
99
+ locationCoordinates?: { latitude: number; longitude: number };
100
+ }
101
+
102
+ export interface InventoryDetails {
103
+ classification?: string;
104
+ type?: string;
105
+ format?: string;
106
+ environment?: string;
107
+ venueTypes?: string[];
108
+ orientation?: string;
109
+ sizes?: string[];
110
+ mediaOwnerId?: string;
111
+ mediaOwnerName?: string;
112
+ address?: string;
113
+ location?: InventoryLocation;
114
+ }
115
+
116
+ export interface RecommendedInventory {
117
+ inventoryId: string;
118
+ referenceId?: string;
119
+ name: string;
120
+ finalScore?: number;
121
+ componentScores?: ComponentScores;
122
+ why?: string;
123
+ availability?: AvailabilitySummary;
124
+ forecast?: ForecastedMetrics;
125
+ cost?: CostEstimate;
126
+ inventoryDetails?: InventoryDetails;
127
+ isSelected?: boolean;
128
+ isExcluded?: boolean;
129
+ }
130
+
131
+ export interface PaginationInfo {
132
+ page: number;
133
+ size: number;
134
+ totalElements: number;
135
+ totalPages: number;
136
+ }
137
+
138
+ export interface PaginatedRecommendationResponse {
139
+ runId: string;
140
+ campaignId?: string;
141
+ productId?: string;
142
+ companyId?: string;
143
+ recommendations: RecommendedInventory[];
144
+ pagination: PaginationInfo;
145
+ }
146
+
147
+ export interface RecommendationResultFilter {
148
+ inventoryIds?: string[];
149
+ referenceIds?: string[];
150
+ classifications?: string[];
151
+ types?: string[];
152
+ formats?: string[];
153
+ environments?: string[];
154
+ venueTypes?: string[];
155
+ orientations?: string[];
156
+ sizes?: string[];
157
+ mediaOwnerIds?: string[];
158
+ mediaOwnerNames?: string[];
159
+ countryIds?: string[];
160
+ countryNames?: string[];
161
+ stateIds?: string[];
162
+ stateNames?: string[];
163
+ cityIds?: string[];
164
+ cityNames?: string[];
165
+ currencies?: string[];
166
+ minFinalScore?: number;
167
+ maxFinalScore?: number;
168
+ minMeasureFit?: number;
169
+ maxMeasureFit?: number;
170
+ minGeoFit?: number;
171
+ maxGeoFit?: number;
172
+ minAvailability?: number;
173
+ maxAvailability?: number;
174
+ minBudgetFit?: number;
175
+ maxBudgetFit?: number;
176
+ minAudienceFit?: number;
177
+ maxAudienceFit?: number;
178
+ minBrandFit?: number;
179
+ maxBrandFit?: number;
180
+ minQualityFit?: number;
181
+ maxQualityFit?: number;
182
+ minTimeFit?: number;
183
+ maxTimeFit?: number;
184
+ minEstimatedImpressions?: number;
185
+ maxEstimatedImpressions?: number;
186
+ minEstimatedReach?: number;
187
+ maxEstimatedReach?: number;
188
+ minEstimatedFrequency?: number;
189
+ maxEstimatedFrequency?: number;
190
+ minEstimatedCost?: number;
191
+ maxEstimatedCost?: number;
192
+ minAvailabilityPercentage?: number;
193
+ maxAvailabilityPercentage?: number;
194
+ allAvailable?: boolean;
195
+ }
196
+
197
+ async function getHeaders(): Promise<Record<string, string>> {
198
+ const accessToken = getAccessToken();
199
+ const headers: Record<string, string> = {
200
+ 'Content-Type': 'application/json',
201
+ };
202
+ if (accessToken) {
203
+ headers['Authorization'] = `Bearer ${accessToken}`;
204
+ }
205
+ return headers;
206
+ }
207
+
208
+ export async function submitRecommendation(
209
+ lineItemId: string,
210
+ request: RecommendationRequest
211
+ ): Promise<RecommendationStatusResponse> {
212
+ const headers = await getHeaders();
213
+
214
+ console.log('[Recommendation API] Submitting request:', JSON.stringify(request, null, 2));
215
+ console.log('[Recommendation API] Headers:', headers);
216
+
217
+ const response = await fetch(
218
+ `${RECOMMENDATION_API_BASE}/recommendation/campaigns/${lineItemId}/recommendations`,
219
+ {
220
+ method: 'POST',
221
+ headers,
222
+ body: JSON.stringify(request),
223
+ }
224
+ );
225
+
226
+ const result = await response.json();
227
+ console.log('[Recommendation API] Response:', JSON.stringify(result, null, 2));
228
+
229
+ if (!response.ok) {
230
+ throw new Error(result.message || result.error?.message || `Failed to submit recommendation: ${response.status}`);
231
+ }
232
+
233
+ // Check for API-level success flag (some APIs return 200 with success: false)
234
+ if (result.success === false) {
235
+ const errorDetails = result.error?.details || result.error?.message || result.message || 'Unknown error';
236
+ throw new Error(`Recommendation failed: ${errorDetails}`);
237
+ }
238
+
239
+ return result.data || result;
240
+ }
241
+
242
+ export async function getRecommendationStatus(
243
+ lineItemId: string,
244
+ request: RecommendationRequest
245
+ ): Promise<RecommendationStatusResponse> {
246
+ const headers = await getHeaders();
247
+
248
+ console.log('[Recommendation API] Checking status with same payload:', JSON.stringify(request, null, 2));
249
+
250
+ // Use same POST request with same payload to check status
251
+ const response = await fetch(
252
+ `${RECOMMENDATION_API_BASE}/recommendation/campaigns/${lineItemId}/recommendations`,
253
+ {
254
+ method: 'POST',
255
+ headers,
256
+ body: JSON.stringify(request),
257
+ }
258
+ );
259
+
260
+ const result = await response.json();
261
+ console.log('[Recommendation API] Status response:', JSON.stringify(result, null, 2));
262
+
263
+ if (!response.ok) {
264
+ throw new Error(result.message || result.error?.message || `Failed to get recommendation status: ${response.status}`);
265
+ }
266
+
267
+ // Check for API-level success flag
268
+ if (result.success === false) {
269
+ const errorDetails = result.error?.details || result.error?.message || result.message || 'Unknown error';
270
+ throw new Error(`Status check failed: ${errorDetails}`);
271
+ }
272
+
273
+ return result.data || result;
274
+ }
275
+
276
+ export async function getRecommendationResults(
277
+ runId: string,
278
+ page: number = 0,
279
+ size: number = 20,
280
+ sort: string[] = ['finalScore,desc'],
281
+ filter?: RecommendationResultFilter
282
+ ): Promise<PaginatedRecommendationResponse> {
283
+ const headers = await getHeaders();
284
+
285
+ const params = new URLSearchParams({
286
+ page: page.toString(),
287
+ size: size.toString(),
288
+ });
289
+
290
+ sort.forEach(s => params.append('sort', s));
291
+
292
+ // TODO: Re-enable types filter when backend supports it
293
+ // const requestBody = {
294
+ // types: ["Digital"],
295
+ // ...(filter || {}),
296
+ // };
297
+ const requestBody = filter || {};
298
+
299
+ const response = await fetch(
300
+ `${RECOMMENDATION_API_BASE}/recommendation/runs/${runId}/results?${params.toString()}`,
301
+ {
302
+ method: 'POST',
303
+ headers,
304
+ body: JSON.stringify(requestBody),
305
+ }
306
+ );
307
+
308
+ if (!response.ok) {
309
+ const errorData = await response.json().catch(() => ({}));
310
+ throw new Error(errorData.message || `Failed to get recommendation results: ${response.status}`);
311
+ }
312
+
313
+ const result = await response.json();
314
+ return result.data || result;
315
+ }
316
+
317
+ export interface PollingProgress {
318
+ status: 'IN_PROGRESS' | 'COMPLETED';
319
+ completionPercentage: number;
320
+ attempt: number;
321
+ maxAttempts: number;
322
+ elapsedSeconds: number;
323
+ }
324
+
325
+ export async function pollUntilCompleted(
326
+ lineItemId: string,
327
+ request: RecommendationRequest,
328
+ maxAttempts: number = 150,
329
+ intervalMs: number = 2000,
330
+ onProgress?: (progress: PollingProgress) => void
331
+ ): Promise<RecommendationStatusResponse> {
332
+ let attempts = 0;
333
+ const totalMinutes = Math.round((maxAttempts * intervalMs) / 60000);
334
+
335
+ while (attempts < maxAttempts) {
336
+ const status = await getRecommendationStatus(lineItemId, request);
337
+
338
+ const elapsedSeconds = Math.round((attempts + 1) * intervalMs / 1000);
339
+ const completionPercentage = status.completionPercentage ?? 0;
340
+
341
+ onProgress?.({
342
+ status: status.status,
343
+ completionPercentage,
344
+ attempt: attempts + 1,
345
+ maxAttempts,
346
+ elapsedSeconds,
347
+ });
348
+
349
+ if (status.status === 'COMPLETED') {
350
+ console.log(`[Recommendation API] Completed after ${attempts + 1} attempts (${elapsedSeconds}s)`);
351
+ return status;
352
+ }
353
+
354
+ attempts++;
355
+ if (attempts % 15 === 0) {
356
+ console.log(`[Recommendation API] Still polling... attempt ${attempts}/${maxAttempts} (${Math.round(attempts * intervalMs / 1000)}s elapsed, ${totalMinutes}min max)`);
357
+ }
358
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
359
+ }
360
+
361
+ throw new Error(`Recommendation timed out after ${totalMinutes} minutes. Please try again.`);
362
+ }
363
+
364
+ export async function submitAndWaitForResults(
365
+ lineItemId: string,
366
+ request: RecommendationRequest,
367
+ page: number = 0,
368
+ size: number = 20
369
+ ): Promise<{ status: RecommendationStatusResponse; results: PaginatedRecommendationResponse }> {
370
+ const statusResponse = await submitRecommendation(lineItemId, request);
371
+
372
+ let finalStatus = statusResponse;
373
+ if (statusResponse.status === 'IN_PROGRESS') {
374
+ finalStatus = await pollUntilCompleted(lineItemId, request);
375
+ }
376
+
377
+ const results = await getRecommendationResults(finalStatus.runId, page, size);
378
+
379
+ return { status: finalStatus, results };
380
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Storage Utilities for OAuth Token Storage
3
+ *
4
+ * Provides cookie-based operations for storing OAuth access and refresh tokens
5
+ * Uses Secure and SameSite=Strict flags for enhanced security
6
+ */
7
+
8
+ const STORAGE_PREFIX = 'oauth_';
9
+ const COOKIE_OPTIONS = {
10
+ path: '/',
11
+ sameSite: 'Strict',
12
+ // Secure flag will be applied only over HTTPS
13
+ secure: window.location.protocol === 'https:',
14
+ };
15
+
16
+ /**
17
+ * Set a cookie with the given key and value
18
+ */
19
+ export function setItem(key: string, value: string): void {
20
+ try {
21
+ const cookieName = STORAGE_PREFIX + key;
22
+
23
+ // Build cookie string
24
+ let cookieStr = `${encodeURIComponent(cookieName)}=${encodeURIComponent(value)}`;
25
+ cookieStr += `; path=${COOKIE_OPTIONS.path}`;
26
+ cookieStr += `; SameSite=${COOKIE_OPTIONS.sameSite}`;
27
+
28
+ // Add Secure flag only for HTTPS
29
+ if (COOKIE_OPTIONS.secure) {
30
+ cookieStr += '; Secure';
31
+ }
32
+
33
+ // Set token expiry to 7 days (can be overridden per token)
34
+ const maxAge = 7 * 24 * 60 * 60; // 7 days in seconds
35
+ cookieStr += `; Max-Age=${maxAge}`;
36
+
37
+ document.cookie = cookieStr;
38
+ console.log(`[Storage] Cookie set: ${cookieName}`);
39
+ } catch (error) {
40
+ console.error('Failed to set cookie:', error);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get a cookie value by key
46
+ */
47
+ export function getItem(key: string): string | null {
48
+ try {
49
+ const cookieName = STORAGE_PREFIX + key;
50
+ const nameEQ = encodeURIComponent(cookieName) + '=';
51
+
52
+ // Split cookies and find the one we're looking for
53
+ const cookies = document.cookie.split(';');
54
+ for (let i = 0; i < cookies.length; i++) {
55
+ let cookie = cookies[i].trim();
56
+ if (cookie.indexOf(nameEQ) === 0) {
57
+ return decodeURIComponent(cookie.substring(nameEQ.length));
58
+ }
59
+ }
60
+
61
+ return null;
62
+ } catch (error) {
63
+ console.error('Failed to get cookie:', error);
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Delete a cookie by setting its expiry to the past
70
+ */
71
+ export function removeItem(key: string): void {
72
+ try {
73
+ const cookieName = STORAGE_PREFIX + key;
74
+ document.cookie = `${encodeURIComponent(cookieName)}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Strict`;
75
+ console.log(`[Storage] Cookie deleted: ${cookieName}`);
76
+ } catch (error) {
77
+ console.error('Failed to remove cookie:', error);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Check if a cookie exists
83
+ */
84
+ export function hasItem(key: string): boolean {
85
+ return getItem(key) !== null;
86
+ }
87
+
88
+ /**
89
+ * Clear all OAuth-related cookies
90
+ */
91
+ export function clearAll(): void {
92
+ try {
93
+ const cookies = document.cookie.split(';');
94
+ cookies.forEach(cookie => {
95
+ const cookieName = cookie.split('=')[0].trim();
96
+ if (cookieName.startsWith(encodeURIComponent(STORAGE_PREFIX))) {
97
+ document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Strict`;
98
+ }
99
+ });
100
+ console.log('[Storage] All OAuth cookies cleared');
101
+ } catch (error) {
102
+ console.error('Failed to clear cookies:', error);
103
+ }
104
+ }