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,1595 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { pgTable, text, varchar, integer, boolean, timestamp, decimal, jsonb } from "drizzle-orm/pg-core";
3
+ import { createInsertSchema } from "drizzle-zod";
4
+ import { z } from "zod";
5
+
6
+ // Users table
7
+ export const users = pgTable("users", {
8
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
9
+ username: text("username").notNull().unique(),
10
+ password: text("password").notNull(),
11
+ role: text("role").notNull().default("user"),
12
+ });
13
+
14
+ export const insertUserSchema = createInsertSchema(users).pick({
15
+ username: true,
16
+ password: true,
17
+ });
18
+
19
+ export type InsertUser = z.infer<typeof insertUserSchema>;
20
+ export type User = typeof users.$inferSelect;
21
+
22
+ // Advertisers/Clients
23
+ export const advertisers = pgTable("advertisers", {
24
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
25
+ name: text("name").notNull(),
26
+ contactEmail: text("contact_email"),
27
+ contactPhone: text("contact_phone"),
28
+ status: text("status").notNull().default("active"),
29
+ });
30
+
31
+ export const insertAdvertiserSchema = createInsertSchema(advertisers).omit({ id: true });
32
+ export type InsertAdvertiser = z.infer<typeof insertAdvertiserSchema>;
33
+ export type Advertiser = typeof advertisers.$inferSelect;
34
+
35
+ // Agencies
36
+ export const agencies = pgTable("agencies", {
37
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
38
+ name: text("name").notNull(),
39
+ contactEmail: text("contact_email"),
40
+ country: text("country"),
41
+ status: text("status").notNull().default("active"),
42
+ });
43
+
44
+ export const insertAgencySchema = createInsertSchema(agencies).omit({ id: true });
45
+ export type InsertAgency = z.infer<typeof insertAgencySchema>;
46
+ export type Agency = typeof agencies.$inferSelect;
47
+
48
+ // Brands
49
+ export const brands = pgTable("brands", {
50
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
51
+ name: text("name").notNull(),
52
+ iabCategory: text("iab_category"),
53
+ website: text("website"),
54
+ logoUrl: text("logo_url"),
55
+ status: text("status").notNull().default("active"),
56
+ });
57
+
58
+ export const insertBrandSchema = createInsertSchema(brands).omit({ id: true });
59
+ export type InsertBrand = z.infer<typeof insertBrandSchema>;
60
+ export type Brand = typeof brands.$inferSelect;
61
+
62
+ // Media Owner Business Types
63
+ export const MEDIA_OWNER_BUSINESS_TYPES = ["Media Owner", "Reseller"] as const;
64
+ export type MediaOwnerBusinessType = typeof MEDIA_OWNER_BUSINESS_TYPES[number];
65
+
66
+ // Media Owners (Companies)
67
+ // Media owners can have a parent-child hierarchy where parent companies
68
+ // can have child companies. This is managed through Admin Console.
69
+ export const mediaOwners = pgTable("media_owners", {
70
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
71
+ name: text("name").notNull(),
72
+ country: text("country"),
73
+ status: text("status").notNull().default("active"),
74
+ // Company hierarchy fields (populated from Admin Console)
75
+ parentId: varchar("parent_id"), // References parent media owner for child companies
76
+ companyType: text("company_type").default("media_owner"), // 'media_owner' or 'child_company'
77
+ // User association - if this company has a user account that can log in
78
+ userId: varchar("user_id"),
79
+ // Business type for filtering
80
+ businessType: text("business_type").default("Media Owner"), // 'Media Owner' or 'Reseller'
81
+ });
82
+
83
+ export const insertMediaOwnerSchema = createInsertSchema(mediaOwners).omit({ id: true });
84
+ export type InsertMediaOwner = z.infer<typeof insertMediaOwnerSchema>;
85
+ export type MediaOwner = typeof mediaOwners.$inferSelect;
86
+
87
+ // Venues (physical locations)
88
+ export const venues = pgTable("venues", {
89
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
90
+ name: text("name").notNull(),
91
+ address: text("address"),
92
+ city: text("city"),
93
+ country: text("country"),
94
+ venueType: text("venue_type").notNull(),
95
+ status: text("status").notNull().default("active"),
96
+ });
97
+
98
+ export const insertVenueSchema = createInsertSchema(venues).omit({ id: true });
99
+ export type InsertVenue = z.infer<typeof insertVenueSchema>;
100
+ export type Venue = typeof venues.$inferSelect;
101
+
102
+ // Screens (inventory)
103
+ export const screens = pgTable("screens", {
104
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
105
+ name: text("name").notNull(),
106
+ venueId: varchar("venue_id").references(() => venues.id),
107
+ screenType: text("screen_type").notNull(),
108
+ width: integer("width").notNull(),
109
+ height: integer("height").notNull(),
110
+ orientation: text("orientation").notNull().default("landscape"),
111
+ status: text("status").notNull().default("active"),
112
+ dailyImpressions: integer("daily_impressions").default(0),
113
+ cpm: decimal("cpm", { precision: 10, scale: 2 }).default("10.00"),
114
+ country: text("country"),
115
+ city: text("city"),
116
+ mediaOwnerId: varchar("media_owner_id").references(() => mediaOwners.id),
117
+ sspId: varchar("ssp_id").references(() => sspPartners.id),
118
+ latitude: decimal("latitude", { precision: 10, scale: 7 }),
119
+ longitude: decimal("longitude", { precision: 10, scale: 7 }),
120
+ format: text("format"),
121
+ classification: text("classification").default("Digital"),
122
+ inventoryType: text("inventory_type"),
123
+ sizeCategory: text("size_category"),
124
+ webcamUrl: text("webcam_url"),
125
+ webcamStatus: text("webcam_status").default("inactive"),
126
+ });
127
+
128
+ export const insertScreenSchema = createInsertSchema(screens).omit({ id: true });
129
+ export type InsertScreen = z.infer<typeof insertScreenSchema>;
130
+ export type Screen = typeof screens.$inferSelect;
131
+
132
+ // Deals - 4-level hierarchy: Advertiser → Deal → Line Item → Creative Assignment
133
+ // Deal now contains all fields previously in Insertion Orders
134
+ export const deals = pgTable("deals", {
135
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
136
+ name: text("name").notNull(),
137
+ advertiserId: varchar("advertiser_id").references(() => advertisers.id),
138
+ brandId: varchar("brand_id").references(() => brands.id),
139
+ agencyId: varchar("agency_id").references(() => agencies.id),
140
+ // Deal status: active or paused
141
+ status: text("status").notNull().default("active"),
142
+ // External Deal ID for integration with external systems
143
+ externalDealId: text("external_deal_id"),
144
+ // Billable flag
145
+ billable: boolean("billable").default(true),
146
+ // Media type (DOOH only for now)
147
+ mediaType: text("media_type").notNull().default("dooh"),
148
+ // Client type: direct or agency
149
+ clientType: text("client_type").default("direct"),
150
+ // SSP Partner - defaults to Influence (built-in SSP). SSP list comes from Admin Console based on logged-in user's company
151
+ sspId: varchar("ssp_id"),
152
+ // Deal Type: traditional, pg, preferred_deal, pmp, always_on, open_auction
153
+ dealType: text("deal_type").notNull().default("traditional"),
154
+ // DSP configuration (for programmatic deals when agency or brand selected)
155
+ // DSP list comes from Admin Console based on company's DSP Access
156
+ dspId: varchar("dsp_id"),
157
+ // Seat name selected from Admin Console for the chosen agency/brand
158
+ seatName: text("seat_name"),
159
+ // Seat ID fetched from Admin Console (view-only, verified before save)
160
+ seatId: text("seat_id"),
161
+ // Legacy fields for backward compatibility
162
+ seatNames: text("seat_names").array(),
163
+ seatIds: text("seat_ids").array(),
164
+ // Geography - default to user's home country, options from Admin Console based on user's country access
165
+ countries: text("countries").array(),
166
+ currency: text("currency").default("USD"),
167
+ timezone: text("timezone"),
168
+ // Budget and Goals
169
+ budget: decimal("budget", { precision: 12, scale: 2 }),
170
+ spent: decimal("spent", { precision: 12, scale: 2 }).default("0.00"),
171
+ goalType: text("goal_type"),
172
+ goalValue: integer("goal_value"),
173
+ targetType: text("target_type"),
174
+ targetValue: integer("target_value"),
175
+ // Minimum Target Threshold - only applicable for PG deals
176
+ minimumTargetThreshold: integer("minimum_target_threshold"),
177
+ // Hard Stop - only applicable for PG deals
178
+ hardStop: boolean("hard_stop").default(false),
179
+ // Flight dates
180
+ startDate: text("start_date"),
181
+ endDate: text("end_date"),
182
+ // Verification
183
+ adPlayVerificationEnabled: boolean("adplay_verification_enabled").default(false),
184
+ adPlayVerificationProvider: text("adplay_verification_provider"),
185
+ // Source of the deal
186
+ source: text("source").notNull().default("influence"),
187
+ // Acceptance sent flag - when true, deal has been sent to DSP/stakeholders
188
+ acceptanceSent: boolean("acceptance_sent").default(false),
189
+ // Reopened flag - when true, deal has been reopened for editing after being sent
190
+ reopened: boolean("reopened").default(false),
191
+ // RFP flag - for Activate deals, indicates deal is pending RFP review
192
+ isRfp: boolean("is_rfp").default(false),
193
+ // Audit fields
194
+ createdBy: text("created_by"),
195
+ createdAt: text("created_at"),
196
+ updatedAt: text("updated_at"),
197
+ });
198
+
199
+ export const insertDealSchema = createInsertSchema(deals).omit({ id: true });
200
+ export type InsertDeal = z.infer<typeof insertDealSchema>;
201
+ export type Deal = typeof deals.$inferSelect;
202
+
203
+ // Line Items - now directly under Deal (no Insertion Order layer)
204
+ export const lineItems = pgTable("line_items", {
205
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
206
+ name: text("name").notNull(),
207
+ // Line items now reference deals directly (4-level hierarchy)
208
+ dealId: varchar("deal_id").references(() => deals.id),
209
+ // Media owner for this line item (for Planner integration - one line item per media owner)
210
+ mediaOwnerId: varchar("media_owner_id"),
211
+ status: text("status").notNull().default("draft"),
212
+ copiedFromId: varchar("copied_from_id"),
213
+ creativeType: text("creative_type").default("display"),
214
+ priority: integer("priority").default(5),
215
+ targeting: jsonb("targeting"),
216
+ inventoryFilters: jsonb("inventory_filters"),
217
+ startDate: text("start_date"),
218
+ endDate: text("end_date"),
219
+ schedules: jsonb("schedules"),
220
+ floorRateType: text("floor_rate_type").default("cpm"),
221
+ floorRate: decimal("floor_rate", { precision: 10, scale: 2 }),
222
+ maxBid: decimal("max_bid", { precision: 10, scale: 2 }),
223
+ bidType: text("bid_type").default("cpm"),
224
+ automatedBidding: boolean("automated_bidding").default(false),
225
+ budgetAllocation: jsonb("budget_allocation"),
226
+ pacing: text("pacing").default("even"),
227
+ customFees: jsonb("custom_fees"),
228
+ frequencyCap: jsonb("frequency_cap"),
229
+ trafficAllocation: integer("traffic_allocation").default(100),
230
+ targetImpressions: integer("target_impressions"),
231
+ deliveredImpressions: integer("delivered_impressions").default(0),
232
+ budget: decimal("budget", { precision: 12, scale: 2 }),
233
+ spent: decimal("spent", { precision: 12, scale: 2 }).default("0.00"),
234
+ cpm: decimal("cpm", { precision: 10, scale: 2 }),
235
+ creativeUrl: text("creative_url"),
236
+ creativeDuration: integer("creative_duration").default(10),
237
+ dspId: varchar("dsp_id").references(() => dspPartners.id),
238
+ dspSeatId: text("dsp_seat_id"),
239
+ pushToDsp: boolean("push_to_dsp").default(false),
240
+ triggerId: varchar("trigger_id"),
241
+ triggerEnabled: boolean("trigger_enabled").default(false),
242
+ resolution: text("resolution"),
243
+ createdBy: text("created_by"),
244
+ createdAt: text("created_at"),
245
+ updatedAt: text("updated_at"),
246
+ });
247
+
248
+ export const insertLineItemSchema = createInsertSchema(lineItems).omit({ id: true });
249
+ export type InsertLineItem = z.infer<typeof insertLineItemSchema>;
250
+ export type LineItem = typeof lineItems.$inferSelect;
251
+
252
+ // SSP Partners
253
+ export const sspPartners = pgTable("ssp_partners", {
254
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
255
+ name: text("name").notNull(),
256
+ endpoint: text("endpoint"),
257
+ apiKey: text("api_key"),
258
+ status: text("status").notNull().default("active"),
259
+ partnerType: text("partner_type").notNull().default("ssp"),
260
+ });
261
+
262
+ export const insertSspPartnerSchema = createInsertSchema(sspPartners).omit({ id: true });
263
+ export type InsertSspPartner = z.infer<typeof insertSspPartnerSchema>;
264
+ export type SspPartner = typeof sspPartners.$inferSelect;
265
+
266
+ // DSP Partners
267
+ export const dspPartners = pgTable("dsp_partners", {
268
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
269
+ name: text("name").notNull(),
270
+ endpoint: text("endpoint"),
271
+ apiKey: text("api_key"),
272
+ status: text("status").notNull().default("active"),
273
+ });
274
+
275
+ export const insertDspPartnerSchema = createInsertSchema(dspPartners).omit({ id: true });
276
+ export type InsertDspPartner = z.infer<typeof insertDspPartnerSchema>;
277
+ export type DspPartner = typeof dspPartners.$inferSelect;
278
+
279
+ // Creative Folders
280
+ export const creativeFolders = pgTable("creative_folders", {
281
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
282
+ name: text("name").notNull(),
283
+ createdAt: text("created_at"),
284
+ });
285
+
286
+ export const insertCreativeFolderSchema = createInsertSchema(creativeFolders).omit({ id: true });
287
+ export type InsertCreativeFolder = z.infer<typeof insertCreativeFolderSchema>;
288
+ export type CreativeFolder = typeof creativeFolders.$inferSelect;
289
+
290
+ // Creatives
291
+ export const creatives = pgTable("creatives", {
292
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
293
+ name: text("name").notNull(),
294
+ type: text("type").notNull().default("display"),
295
+ format: text("format").notNull(),
296
+ width: integer("width").notNull(),
297
+ height: integer("height").notNull(),
298
+ fileSize: integer("file_size").notNull(),
299
+ duration: integer("duration"),
300
+ status: text("status").notNull().default("new"),
301
+ folderId: varchar("folder_id").references(() => creativeFolders.id),
302
+ tags: text("tags").array(),
303
+ uploadedAt: text("uploaded_at"),
304
+ uploadedBy: text("uploaded_by"),
305
+ thumbnailUrl: text("thumbnail_url"),
306
+ fileUrl: text("file_url"),
307
+ inadequateReason: text("inadequate_reason"),
308
+ // Creative source - where the creative originated from
309
+ creativeSource: text("creative_source").default("uploaded"), // uploaded, bid_stream_vast, api, media_owner
310
+ // Source platform - which platform the creative came from
311
+ sourcePlatform: text("source_platform"), // planner, activate, influence, media_owner
312
+ // Transcoding options
313
+ transcodingEnabled: boolean("transcoding_enabled").default(false),
314
+ transcodingStatus: text("transcoding_status"), // pending, processing, completed, failed
315
+ transcodedUrl: text("transcoded_url"),
316
+ });
317
+
318
+ export const insertCreativeSchema = createInsertSchema(creatives).omit({ id: true });
319
+ export type InsertCreative = z.infer<typeof insertCreativeSchema>;
320
+ export type Creative = typeof creatives.$inferSelect;
321
+
322
+ // Creative types and statuses
323
+ export type CreativeAssetType = "display" | "video" | "html" | "native";
324
+ export type CreativeAssetStatus = "new" | "processing" | "accepted" | "inadequate" | "archived";
325
+
326
+ export const CREATIVE_ASSET_TYPES: { value: CreativeAssetType; label: string }[] = [
327
+ { value: "display", label: "Display" },
328
+ { value: "video", label: "Video" },
329
+ { value: "html", label: "HTML" },
330
+ { value: "native", label: "Native" },
331
+ ];
332
+
333
+ export const CREATIVE_ASSET_STATUSES: { value: CreativeAssetStatus; label: string }[] = [
334
+ { value: "new", label: "New" },
335
+ { value: "processing", label: "Processing" },
336
+ { value: "accepted", label: "Accepted" },
337
+ { value: "inadequate", label: "Inadequate" },
338
+ { value: "archived", label: "Archived" },
339
+ ];
340
+
341
+ // Creative source types - where the creative originated from
342
+ export type CreativeSource = "uploaded" | "bid_stream_vast" | "api" | "media_owner";
343
+
344
+ export const CREATIVE_SOURCES: { value: CreativeSource; label: string }[] = [
345
+ { value: "uploaded", label: "Uploaded" },
346
+ { value: "bid_stream_vast", label: "Bid Stream (VAST)" },
347
+ { value: "api", label: "API" },
348
+ { value: "media_owner", label: "Media Owner" },
349
+ ];
350
+
351
+ // Transcoding status types
352
+ export type TranscodingStatus = "pending" | "processing" | "completed" | "failed";
353
+
354
+ export const TRANSCODING_STATUSES: { value: TranscodingStatus; label: string }[] = [
355
+ { value: "pending", label: "Pending" },
356
+ { value: "processing", label: "Processing" },
357
+ { value: "completed", label: "Completed" },
358
+ { value: "failed", label: "Failed" },
359
+ ];
360
+
361
+ // Line Item Creative Assignments (Junction table for Tier 2 approval)
362
+ export const lineItemCreatives = pgTable("line_item_creatives", {
363
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
364
+ lineItemId: varchar("line_item_id").references(() => lineItems.id).notNull(),
365
+ creativeId: varchar("creative_id").references(() => creatives.id).notNull(),
366
+ tier2Status: text("tier2_status").notNull().default("pending"),
367
+ tier2ReviewedBy: text("tier2_reviewed_by"),
368
+ tier2ReviewedAt: text("tier2_reviewed_at"),
369
+ tier2RejectionReason: text("tier2_rejection_reason"),
370
+ assignedBy: text("assigned_by"),
371
+ assignedAt: text("assigned_at"),
372
+ displayOrder: integer("display_order").default(0),
373
+ weight: integer("weight").default(100),
374
+ startTime: text("start_time"),
375
+ endTime: text("end_time"),
376
+ schedule: jsonb("schedule"),
377
+ rule: text("rule").default("default"),
378
+ adPlayCount: integer("ad_play_count").default(0),
379
+ });
380
+
381
+ export const insertLineItemCreativeSchema = createInsertSchema(lineItemCreatives).omit({ id: true });
382
+ export type InsertLineItemCreative = z.infer<typeof insertLineItemCreativeSchema>;
383
+ export type LineItemCreative = typeof lineItemCreatives.$inferSelect;
384
+
385
+ // Signals - conditional signals for line item automation
386
+ export const signals = pgTable("signals", {
387
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
388
+ name: text("name").notNull(),
389
+ description: text("description"),
390
+ signalType: text("signal_type").notNull().default("weather"),
391
+ status: text("status").notNull().default("active"),
392
+ createdBy: text("created_by"),
393
+ createdAt: text("created_at"),
394
+ updatedAt: text("updated_at"),
395
+ });
396
+
397
+ export const insertSignalSchema = createInsertSchema(signals).omit({ id: true });
398
+ export type InsertSignal = z.infer<typeof insertSignalSchema>;
399
+ export type Signal = typeof signals.$inferSelect;
400
+
401
+ // Signal Rules - conditions within a signal
402
+ export const signalRules = pgTable("signal_rules", {
403
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
404
+ signalId: varchar("signal_id").references(() => signals.id).notNull(),
405
+ name: text("name").notNull(),
406
+ conditionType: text("condition_type").notNull(),
407
+ parameters: jsonb("parameters"),
408
+ status: text("status").notNull().default("active"),
409
+ createdAt: text("created_at"),
410
+ updatedAt: text("updated_at"),
411
+ });
412
+
413
+ export const insertSignalRuleSchema = createInsertSchema(signalRules).omit({ id: true });
414
+ export type InsertSignalRule = z.infer<typeof insertSignalRuleSchema>;
415
+ export type SignalRule = typeof signalRules.$inferSelect;
416
+
417
+ // Signal types
418
+ export type SignalType = "audiences" | "footfall" | "search" | "weather";
419
+
420
+ export const SIGNAL_TYPES: { value: SignalType; label: string; description: string }[] = [
421
+ { value: "audiences", label: "Audiences", description: "Signal based on audience segments" },
422
+ { value: "footfall", label: "Footfall", description: "Signal based on store foot traffic" },
423
+ { value: "search", label: "Search", description: "Signal based on search keyword trends" },
424
+ { value: "weather", label: "Weather", description: "Signal based on weather conditions" },
425
+ ];
426
+
427
+ // Change Logs (Audit Trail)
428
+ export const changeLogs = pgTable("change_logs", {
429
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
430
+ entityType: text("entity_type").notNull(), // 'deal' | 'order' | 'line_item' | 'creative'
431
+ entityId: varchar("entity_id").notNull(),
432
+ action: text("action").notNull(), // 'created' | 'updated' | 'deleted' | 'status_changed'
433
+ changedBy: text("changed_by").notNull(),
434
+ changedAt: text("changed_at").notNull(),
435
+ changes: jsonb("changes"), // JSON object with field changes: { field: { old: value, new: value } }
436
+ description: text("description"), // Human-readable summary
437
+ });
438
+
439
+ export const insertChangeLogSchema = createInsertSchema(changeLogs).omit({ id: true });
440
+ export type InsertChangeLog = z.infer<typeof insertChangeLogSchema>;
441
+ export type ChangeLog = typeof changeLogs.$inferSelect;
442
+
443
+ // Change Log entity types (order removed in 4-level hierarchy)
444
+ export type ChangeLogEntityType = "deal" | "line_item" | "creative";
445
+ export type ChangeLogAction = "created" | "updated" | "deleted" | "status_changed";
446
+
447
+ // Tier 2 approval statuses for assigned creatives
448
+ export type Tier2Status = "pending" | "approved" | "rejected" | "changes_requested";
449
+
450
+ export const TIER2_STATUSES: { value: Tier2Status; label: string; color: string }[] = [
451
+ { value: "pending", label: "Pending Review", color: "yellow" },
452
+ { value: "approved", label: "Approved", color: "green" },
453
+ { value: "rejected", label: "Rejected", color: "red" },
454
+ { value: "changes_requested", label: "Changes Requested", color: "orange" },
455
+ ];
456
+
457
+ // Creative Schedule structure for hour/day matrix
458
+ export interface CreativeSchedule {
459
+ allHours: boolean;
460
+ selectedHours: Record<string, number[]>; // date string -> array of selected hours (0-23)
461
+ }
462
+
463
+ // Creative Assignment Rule types
464
+ export type CreativeRule = "default" | "custom";
465
+
466
+ export const CREATIVE_RULES: { value: CreativeRule; label: string }[] = [
467
+ { value: "default", label: "Default" },
468
+ { value: "custom", label: "Custom" },
469
+ ];
470
+
471
+ // Transcoded asset for creatives
472
+ export interface TranscodedAsset {
473
+ id: string;
474
+ name: string;
475
+ active: boolean;
476
+ scaling: string;
477
+ duration: string;
478
+ bitrate: string;
479
+ dimensions: string;
480
+ type: string;
481
+ delivery: string;
482
+ codecs: string;
483
+ }
484
+
485
+ // Analytics/Reporting
486
+ export const impressionLogs = pgTable("impression_logs", {
487
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
488
+ lineItemId: varchar("line_item_id").references(() => lineItems.id),
489
+ screenId: varchar("screen_id").references(() => screens.id),
490
+ timestamp: text("timestamp").notNull(),
491
+ cost: decimal("cost", { precision: 10, scale: 4 }),
492
+ source: text("source").default("direct"),
493
+ });
494
+
495
+ export const insertImpressionLogSchema = createInsertSchema(impressionLogs).omit({ id: true });
496
+ export type InsertImpressionLog = z.infer<typeof insertImpressionLogSchema>;
497
+ export type ImpressionLog = typeof impressionLogs.$inferSelect;
498
+
499
+ // Type definitions for frontend state
500
+ export type DealStatus = "active" | "paused";
501
+ export type LineItemStatus = "active" | "paused" | "draft";
502
+ export type ScreenStatus = "active" | "inactive" | "maintenance";
503
+ export type VenueType = "mall" | "airport" | "transit" | "street" | "retail" | "office" | "entertainment";
504
+ export type DealCategory = "traditional" | "programmatic" | "hybrid";
505
+ export type MediaType = "dooh" | "mobile" | "youtube" | "ctv";
506
+ export type DealType = "traditional" | "pg" | "preferred_deal" | "pmp" | "always_on" | "open_auction";
507
+ export type ClientType = "direct" | "agency";
508
+ export type CreativeType = "display" | "video" | "audio";
509
+ export type FloorRateType = "cpm" | "cps";
510
+ export type Pacing = "asap" | "even" | "front_loaded";
511
+ export type IncomeBracket = "low" | "lower_middle" | "medium" | "upper_middle" | "high";
512
+ export type PartnerStatus = "active" | "inactive" | "pending";
513
+
514
+ // Reference data for dropdowns
515
+ export const MEDIA_TYPES: { value: MediaType; label: string; disabled?: boolean }[] = [
516
+ { value: "dooh", label: "DOOH" },
517
+ { value: "mobile", label: "Mobile", disabled: true },
518
+ { value: "youtube", label: "YouTube", disabled: true },
519
+ { value: "ctv", label: "Connected TV", disabled: true },
520
+ ];
521
+
522
+ export const ADPLAY_VERIFICATION_PROVIDERS = ["MAV", "IAS", "OIS", "Veridooh"];
523
+
524
+ export const DEAL_TYPES: { value: DealType; label: string }[] = [
525
+ { value: "traditional", label: "Traditional" },
526
+ { value: "pg", label: "Programmatic Guaranteed" },
527
+ { value: "preferred_deal", label: "Preferred Deal" },
528
+ { value: "pmp", label: "Private Marketplace (PMP)" },
529
+ { value: "always_on", label: "Always On" },
530
+ { value: "open_auction", label: "Open Auction" },
531
+ ];
532
+
533
+ export type GoalType = "impressions" | "reach" | "share_of_voice" | "carbon_emission" | "ad_plays";
534
+ export type TargetType = "impressions" | "ad_plays";
535
+
536
+ export const GOAL_TYPES: { value: GoalType; label: string; description: string }[] = [
537
+ { value: "impressions", label: "Impressions", description: "Total number of ad views/exposures to your target audience" },
538
+ { value: "reach", label: "Reach", description: "Unique individuals exposed to your campaign" },
539
+ { value: "share_of_voice", label: "Share of Voice", description: "Your brand's presence as a percentage of total market activity" },
540
+ { value: "carbon_emission", label: "Carbon Emission", description: "Minimize or cap your campaign's environmental footprint" },
541
+ { value: "ad_plays", label: "Ad Plays", description: "Total number of times your ad content is played on screens" },
542
+ ];
543
+
544
+ export const TARGET_TYPES: { value: TargetType; label: string }[] = [
545
+ { value: "impressions", label: "Impressions" },
546
+ { value: "ad_plays", label: "Ad Plays" },
547
+ ];
548
+
549
+ export const CLIENT_TYPES: { value: ClientType; label: string }[] = [
550
+ { value: "direct", label: "Direct Advertiser" },
551
+ { value: "agency", label: "Agency" },
552
+ ];
553
+
554
+ export const CREATIVE_TYPES: { value: CreativeType; label: string }[] = [
555
+ { value: "display", label: "Display" },
556
+ { value: "video", label: "Video" },
557
+ { value: "audio", label: "Audio" },
558
+ ];
559
+
560
+ export const PACING_OPTIONS: { value: Pacing; label: string }[] = [
561
+ { value: "asap", label: "ASAP" },
562
+ { value: "even", label: "Even" },
563
+ { value: "front_loaded", label: "Front Loaded" },
564
+ ];
565
+
566
+ export const INCOME_BRACKETS: { value: IncomeBracket; label: string }[] = [
567
+ { value: "low", label: "Low income (<RM 128,000)" },
568
+ { value: "lower_middle", label: "Lower-middle income (RM 128,000-RM 213,000)" },
569
+ { value: "medium", label: "Middle income (RM 213,000-RM 426,000)" },
570
+ { value: "upper_middle", label: "Upper-middle income (RM 426,000-RM 639,000)" },
571
+ { value: "high", label: "High income (>RM 639,000)" },
572
+ ];
573
+
574
+ // Targeting options
575
+ export interface TargetingCriteria {
576
+ demographics?: string[];
577
+ geography?: string[];
578
+ venueTypes?: string[];
579
+ incomeBrackets?: IncomeBracket[];
580
+ behaviors?: string[];
581
+ interests?: string[];
582
+ }
583
+
584
+ // Inventory filter options
585
+ export interface InventoryFilters {
586
+ classification?: string[];
587
+ type?: string[];
588
+ format?: string[];
589
+ resolution?: string[];
590
+ duration?: number[];
591
+ }
592
+
593
+ // Custom fee structure
594
+ export interface CustomFee {
595
+ name: string;
596
+ amount: number;
597
+ type: "fixed" | "percentage";
598
+ hidden?: boolean;
599
+ }
600
+
601
+ // Frequency cap structure
602
+ export interface FrequencyCap {
603
+ impressions: number;
604
+ period: "hour" | "day" | "week" | "month" | "lifetime";
605
+ }
606
+
607
+ // Budget allocation by inventory type
608
+ export interface BudgetAllocation {
609
+ inventoryType: string;
610
+ percentage: number;
611
+ amount?: number;
612
+ }
613
+
614
+ // Dashboard metrics type - 4-level hierarchy (no orders)
615
+ export interface DashboardMetrics {
616
+ totalDeals: number;
617
+ activeDeals: number;
618
+ totalImpressions: number;
619
+ totalRevenue: number;
620
+ totalScreens: number;
621
+ activeScreens: number;
622
+ fillRate: number;
623
+ avgCPM: number;
624
+ // Line Item metrics
625
+ totalLineItems: number;
626
+ activeLineItems: number;
627
+ // Creative metrics
628
+ totalCreatives: number;
629
+ pendingApprovalCreatives: number;
630
+ acceptedCreatives: number;
631
+ assignedCreatives: number;
632
+ }
633
+
634
+ // Traffic allocation type
635
+ export interface TrafficAllocation {
636
+ id: string;
637
+ name: string;
638
+ allocation: number;
639
+ delivered: number;
640
+ remaining: number;
641
+ }
642
+
643
+ // Countries and their currencies/timezones
644
+ export interface Country {
645
+ code: string;
646
+ name: string;
647
+ currency: string;
648
+ timezones: string[];
649
+ }
650
+
651
+ export const COUNTRIES: Country[] = [
652
+ { code: "US", name: "United States", currency: "USD", timezones: ["America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles"] },
653
+ { code: "MY", name: "Malaysia", currency: "MYR", timezones: ["Asia/Kuala_Lumpur"] },
654
+ { code: "SG", name: "Singapore", currency: "SGD", timezones: ["Asia/Singapore"] },
655
+ { code: "TH", name: "Thailand", currency: "THB", timezones: ["Asia/Bangkok"] },
656
+ { code: "ID", name: "Indonesia", currency: "IDR", timezones: ["Asia/Jakarta", "Asia/Makassar", "Asia/Jayapura"] },
657
+ { code: "PH", name: "Philippines", currency: "PHP", timezones: ["Asia/Manila"] },
658
+ { code: "AU", name: "Australia", currency: "AUD", timezones: ["Australia/Sydney", "Australia/Melbourne", "Australia/Brisbane", "Australia/Perth"] },
659
+ { code: "GB", name: "United Kingdom", currency: "GBP", timezones: ["Europe/London"] },
660
+ { code: "DE", name: "Germany", currency: "EUR", timezones: ["Europe/Berlin"] },
661
+ { code: "FR", name: "France", currency: "EUR", timezones: ["Europe/Paris"] },
662
+ { code: "JP", name: "Japan", currency: "JPY", timezones: ["Asia/Tokyo"] },
663
+ { code: "KR", name: "South Korea", currency: "KRW", timezones: ["Asia/Seoul"] },
664
+ { code: "IN", name: "India", currency: "INR", timezones: ["Asia/Kolkata"] },
665
+ { code: "AE", name: "United Arab Emirates", currency: "AED", timezones: ["Asia/Dubai"] },
666
+ { code: "SA", name: "Saudi Arabia", currency: "SAR", timezones: ["Asia/Riyadh"] },
667
+ ];
668
+
669
+ export const CURRENCIES = ["USD", "MYR", "SGD", "THB", "IDR", "PHP", "AUD", "GBP", "EUR", "JPY", "KRW", "INR", "AED", "SAR"];
670
+
671
+ // Inventory classification from Google Sheet
672
+ export const INVENTORY_CLASSIFICATIONS = ["Digital"];
673
+
674
+ export const INVENTORY_TYPES = ["OOH", "Transit", "Retail", "Cinema"];
675
+
676
+ export const INVENTORY_FORMATS_BY_TYPE: Record<string, string[]> = {
677
+ OOH: [
678
+ "Digital Large Format",
679
+ "Digital Billboard",
680
+ "Digital Bus Shelter Screen",
681
+ "Smart City Panel",
682
+ "Digital Video Walls",
683
+ "Digital Kiosk",
684
+ ],
685
+ Transit: [
686
+ "Taxi Top Digital",
687
+ "Platform Digital Screen",
688
+ "Airport Terminal Screens",
689
+ "Train Digital Display",
690
+ "Bus Interior Screen",
691
+ "Metro Station Screen",
692
+ ],
693
+ Retail: [
694
+ "Mall / Retail Screens",
695
+ "Elevator / Lobby Screens",
696
+ "Retail Window Screen",
697
+ "Point of Sale Screen",
698
+ "Shelf-edge Digital",
699
+ ],
700
+ Cinema: [
701
+ "Cinema Pre-Show",
702
+ "Cinema Lobby Screen",
703
+ "Cinema Concession Display",
704
+ "Cinema Auditorium Entry",
705
+ ],
706
+ };
707
+
708
+ export const INVENTORY_FORMATS = [
709
+ ...INVENTORY_FORMATS_BY_TYPE.OOH,
710
+ ...INVENTORY_FORMATS_BY_TYPE.Transit,
711
+ ...INVENTORY_FORMATS_BY_TYPE.Retail,
712
+ ...INVENTORY_FORMATS_BY_TYPE.Cinema,
713
+ ];
714
+
715
+ export const INVENTORY_SIZE_CATEGORIES = ["XS", "S", "M", "L", "XL"];
716
+
717
+ export const SCREEN_RESOLUTIONS = [
718
+ "1920x1080",
719
+ "1080x1920",
720
+ "3840x2160",
721
+ "2160x3840",
722
+ "1280x720",
723
+ "720x1280",
724
+ "1024x768",
725
+ "768x1024",
726
+ "800x600",
727
+ "600x800",
728
+ ] as const;
729
+
730
+ // Age Groups
731
+ export const AGE_GROUPS = [
732
+ "18-24", "25-34", "35-44", "45-54", "55-64", "65+"
733
+ ];
734
+
735
+ // Gender options
736
+ export const GENDERS = ["Male", "Female"];
737
+
738
+ // Demographics (combined age + gender for backward compatibility)
739
+ export const DEMOGRAPHICS = [
740
+ ...AGE_GROUPS,
741
+ ...GENDERS
742
+ ];
743
+
744
+ // POI Categories for custom POI targeting
745
+ export const POI_CATEGORIES = [
746
+ "Shopping",
747
+ "Office",
748
+ "Sports",
749
+ "Entertainment",
750
+ "Restaurant",
751
+ "Hotel",
752
+ "Hospital",
753
+ "Education",
754
+ "Transit",
755
+ "Retail",
756
+ "Custom",
757
+ ] as const;
758
+
759
+ export type POICategory = typeof POI_CATEGORIES[number];
760
+
761
+ // POI Source types
762
+ export const POI_SOURCES = ["google_places", "custom"] as const;
763
+ export type POISource = typeof POI_SOURCES[number];
764
+
765
+ // OpenOOH Venue Taxonomy - Hierarchical Structure
766
+ export interface VenueTypeNode {
767
+ id: string;
768
+ label: string;
769
+ description?: string;
770
+ level: "category" | "sub" | "product";
771
+ color: "blue" | "green" | "pink";
772
+ children?: VenueTypeNode[];
773
+ }
774
+
775
+ export const VENUE_TAXONOMY: VenueTypeNode[] = [
776
+ {
777
+ id: "transit",
778
+ label: "Transit",
779
+ description: "Transportation & mobility venues",
780
+ level: "category",
781
+ color: "blue",
782
+ children: [
783
+ {
784
+ id: "transit.airports",
785
+ label: "Airports",
786
+ level: "sub",
787
+ color: "green",
788
+ children: [
789
+ { id: "transit.airports.terminal", label: "Terminal", level: "product", color: "pink" },
790
+ { id: "transit.airports.gate", label: "Gate Area", level: "product", color: "pink" },
791
+ { id: "transit.airports.baggage", label: "Baggage Claim", level: "product", color: "pink" },
792
+ { id: "transit.airports.lounge", label: "Lounge", level: "product", color: "pink" },
793
+ ]
794
+ },
795
+ {
796
+ id: "transit.train_stations",
797
+ label: "Train Stations",
798
+ level: "sub",
799
+ color: "green",
800
+ children: [
801
+ { id: "transit.train_stations.platform", label: "Platform", level: "product", color: "pink" },
802
+ { id: "transit.train_stations.concourse", label: "Concourse", level: "product", color: "pink" },
803
+ { id: "transit.train_stations.ticketing", label: "Ticketing Area", level: "product", color: "pink" },
804
+ ]
805
+ },
806
+ {
807
+ id: "transit.bus_stations",
808
+ label: "Bus Stations",
809
+ level: "sub",
810
+ color: "green",
811
+ children: [
812
+ { id: "transit.bus_stations.waiting_area", label: "Waiting Area", level: "product", color: "pink" },
813
+ { id: "transit.bus_stations.shelter", label: "Bus Shelter", level: "product", color: "pink" },
814
+ ]
815
+ },
816
+ {
817
+ id: "transit.subway",
818
+ label: "Subway/Metro",
819
+ level: "sub",
820
+ color: "green",
821
+ children: [
822
+ { id: "transit.subway.platform", label: "Platform", level: "product", color: "pink" },
823
+ { id: "transit.subway.entrance", label: "Station Entrance", level: "product", color: "pink" },
824
+ ]
825
+ },
826
+ ]
827
+ },
828
+ {
829
+ id: "retail",
830
+ label: "Retail",
831
+ description: "Shopping & commercial venues",
832
+ level: "category",
833
+ color: "green",
834
+ children: [
835
+ {
836
+ id: "retail.shopping_mall",
837
+ label: "Shopping Mall",
838
+ level: "sub",
839
+ color: "green",
840
+ children: [
841
+ { id: "retail.shopping_mall.atrium", label: "Mall Atrium", level: "product", color: "pink" },
842
+ { id: "retail.shopping_mall.corridor", label: "Mall Corridor", level: "product", color: "pink" },
843
+ { id: "retail.shopping_mall.food_court", label: "Food Court", level: "product", color: "pink" },
844
+ { id: "retail.shopping_mall.entrance", label: "Mall Entrance", level: "product", color: "pink" },
845
+ { id: "retail.shopping_mall.anchor_store", label: "Anchor Store", level: "product", color: "pink" },
846
+ ]
847
+ },
848
+ {
849
+ id: "retail.retail_store",
850
+ label: "Retail Store",
851
+ level: "sub",
852
+ color: "green",
853
+ children: [
854
+ { id: "retail.retail_store.checkout", label: "Checkout Area", level: "product", color: "pink" },
855
+ { id: "retail.retail_store.entrance", label: "Store Entrance", level: "product", color: "pink" },
856
+ { id: "retail.retail_store.aisle", label: "Aisle Display", level: "product", color: "pink" },
857
+ ]
858
+ },
859
+ {
860
+ id: "retail.market_plaza",
861
+ label: "Market & Plaza",
862
+ level: "sub",
863
+ color: "green",
864
+ children: [
865
+ { id: "retail.market_plaza.outdoor", label: "Outdoor Market", level: "product", color: "pink" },
866
+ { id: "retail.market_plaza.food_hall", label: "Food Hall", level: "product", color: "pink" },
867
+ ]
868
+ },
869
+ {
870
+ id: "retail.convenience",
871
+ label: "Convenience Store",
872
+ level: "sub",
873
+ color: "green",
874
+ children: [
875
+ { id: "retail.convenience.counter", label: "Counter Display", level: "product", color: "pink" },
876
+ { id: "retail.convenience.window", label: "Window Display", level: "product", color: "pink" },
877
+ ]
878
+ },
879
+ ]
880
+ },
881
+ {
882
+ id: "outdoor",
883
+ label: "Outdoor",
884
+ description: "Street level & outdoor advertising",
885
+ level: "category",
886
+ color: "pink",
887
+ children: [
888
+ {
889
+ id: "outdoor.roadside",
890
+ label: "Roadside",
891
+ level: "sub",
892
+ color: "green",
893
+ children: [
894
+ { id: "outdoor.roadside.billboard", label: "Billboard", level: "product", color: "pink" },
895
+ { id: "outdoor.roadside.digital_billboard", label: "Digital Billboard", level: "product", color: "pink" },
896
+ { id: "outdoor.roadside.poster", label: "Roadside Poster", level: "product", color: "pink" },
897
+ ]
898
+ },
899
+ {
900
+ id: "outdoor.street_furniture",
901
+ label: "Street Furniture",
902
+ level: "sub",
903
+ color: "green",
904
+ children: [
905
+ { id: "outdoor.street_furniture.bus_shelter", label: "Bus Shelter", level: "product", color: "pink" },
906
+ { id: "outdoor.street_furniture.kiosk", label: "Street Kiosk", level: "product", color: "pink" },
907
+ { id: "outdoor.street_furniture.bench", label: "Bench Advertising", level: "product", color: "pink" },
908
+ ]
909
+ },
910
+ {
911
+ id: "outdoor.urban_panels",
912
+ label: "Urban Panels",
913
+ level: "sub",
914
+ color: "green",
915
+ children: [
916
+ { id: "outdoor.urban_panels.wall_mounted", label: "Wall Mounted", level: "product", color: "pink" },
917
+ { id: "outdoor.urban_panels.freestanding", label: "Freestanding Panel", level: "product", color: "pink" },
918
+ ]
919
+ },
920
+ ]
921
+ },
922
+ {
923
+ id: "entertainment",
924
+ label: "Entertainment",
925
+ description: "Leisure & entertainment venues",
926
+ level: "category",
927
+ color: "blue",
928
+ children: [
929
+ {
930
+ id: "entertainment.cinema",
931
+ label: "Cinema",
932
+ level: "sub",
933
+ color: "green",
934
+ children: [
935
+ { id: "entertainment.cinema.lobby", label: "Cinema Lobby", level: "product", color: "pink" },
936
+ { id: "entertainment.cinema.screen", label: "On-Screen", level: "product", color: "pink" },
937
+ { id: "entertainment.cinema.concession", label: "Concession Area", level: "product", color: "pink" },
938
+ ]
939
+ },
940
+ {
941
+ id: "entertainment.sports_venue",
942
+ label: "Sports Venue",
943
+ level: "sub",
944
+ color: "green",
945
+ children: [
946
+ { id: "entertainment.sports_venue.stadium", label: "Stadium", level: "product", color: "pink" },
947
+ { id: "entertainment.sports_venue.arena", label: "Arena", level: "product", color: "pink" },
948
+ { id: "entertainment.sports_venue.concourse", label: "Venue Concourse", level: "product", color: "pink" },
949
+ ]
950
+ },
951
+ {
952
+ id: "entertainment.theme_park",
953
+ label: "Theme Park",
954
+ level: "sub",
955
+ color: "green",
956
+ children: [
957
+ { id: "entertainment.theme_park.entrance", label: "Park Entrance", level: "product", color: "pink" },
958
+ { id: "entertainment.theme_park.queue", label: "Queue Line", level: "product", color: "pink" },
959
+ ]
960
+ },
961
+ ]
962
+ },
963
+ {
964
+ id: "health_beauty",
965
+ label: "Health & Beauty",
966
+ description: "Healthcare & wellness venues",
967
+ level: "category",
968
+ color: "blue",
969
+ children: [
970
+ {
971
+ id: "health_beauty.gym",
972
+ label: "Gym & Fitness",
973
+ level: "sub",
974
+ color: "green",
975
+ children: [
976
+ { id: "health_beauty.gym.lobby", label: "Gym Lobby", level: "product", color: "pink" },
977
+ { id: "health_beauty.gym.locker_room", label: "Locker Room", level: "product", color: "pink" },
978
+ { id: "health_beauty.gym.workout_area", label: "Workout Area", level: "product", color: "pink" },
979
+ ]
980
+ },
981
+ {
982
+ id: "health_beauty.salon",
983
+ label: "Salon & Spa",
984
+ level: "sub",
985
+ color: "green",
986
+ children: [
987
+ { id: "health_beauty.salon.waiting", label: "Waiting Area", level: "product", color: "pink" },
988
+ { id: "health_beauty.salon.treatment", label: "Treatment Room", level: "product", color: "pink" },
989
+ ]
990
+ },
991
+ {
992
+ id: "health_beauty.pharmacy",
993
+ label: "Pharmacy",
994
+ level: "sub",
995
+ color: "green",
996
+ children: [
997
+ { id: "health_beauty.pharmacy.counter", label: "Pharmacy Counter", level: "product", color: "pink" },
998
+ { id: "health_beauty.pharmacy.waiting", label: "Pharmacy Waiting", level: "product", color: "pink" },
999
+ ]
1000
+ },
1001
+ ]
1002
+ },
1003
+ {
1004
+ id: "point_of_care",
1005
+ label: "Point of Care",
1006
+ description: "Medical & healthcare facilities",
1007
+ level: "category",
1008
+ color: "blue",
1009
+ children: [
1010
+ {
1011
+ id: "point_of_care.hospital",
1012
+ label: "Hospital",
1013
+ level: "sub",
1014
+ color: "green",
1015
+ children: [
1016
+ { id: "point_of_care.hospital.lobby", label: "Hospital Lobby", level: "product", color: "pink" },
1017
+ { id: "point_of_care.hospital.waiting_room", label: "Waiting Room", level: "product", color: "pink" },
1018
+ { id: "point_of_care.hospital.cafeteria", label: "Cafeteria", level: "product", color: "pink" },
1019
+ ]
1020
+ },
1021
+ {
1022
+ id: "point_of_care.clinic",
1023
+ label: "Medical Clinic",
1024
+ level: "sub",
1025
+ color: "green",
1026
+ children: [
1027
+ { id: "point_of_care.clinic.reception", label: "Reception", level: "product", color: "pink" },
1028
+ { id: "point_of_care.clinic.exam_room", label: "Exam Room", level: "product", color: "pink" },
1029
+ ]
1030
+ },
1031
+ ]
1032
+ },
1033
+ {
1034
+ id: "office",
1035
+ label: "Office Buildings",
1036
+ description: "Corporate & business venues",
1037
+ level: "category",
1038
+ color: "blue",
1039
+ children: [
1040
+ {
1041
+ id: "office.corporate",
1042
+ label: "Corporate Office",
1043
+ level: "sub",
1044
+ color: "green",
1045
+ children: [
1046
+ { id: "office.corporate.lobby", label: "Office Lobby", level: "product", color: "pink" },
1047
+ { id: "office.corporate.elevator", label: "Elevator Bank", level: "product", color: "pink" },
1048
+ { id: "office.corporate.cafeteria", label: "Office Cafeteria", level: "product", color: "pink" },
1049
+ ]
1050
+ },
1051
+ {
1052
+ id: "office.coworking",
1053
+ label: "Co-working Space",
1054
+ level: "sub",
1055
+ color: "green",
1056
+ children: [
1057
+ { id: "office.coworking.common_area", label: "Common Area", level: "product", color: "pink" },
1058
+ { id: "office.coworking.meeting_room", label: "Meeting Room", level: "product", color: "pink" },
1059
+ ]
1060
+ },
1061
+ ]
1062
+ },
1063
+ {
1064
+ id: "education",
1065
+ label: "Education",
1066
+ description: "Educational institutions",
1067
+ level: "category",
1068
+ color: "green",
1069
+ children: [
1070
+ {
1071
+ id: "education.university",
1072
+ label: "University",
1073
+ level: "sub",
1074
+ color: "green",
1075
+ children: [
1076
+ { id: "education.university.campus", label: "Campus Common", level: "product", color: "pink" },
1077
+ { id: "education.university.library", label: "Library", level: "product", color: "pink" },
1078
+ { id: "education.university.student_center", label: "Student Center", level: "product", color: "pink" },
1079
+ ]
1080
+ },
1081
+ {
1082
+ id: "education.school",
1083
+ label: "School",
1084
+ level: "sub",
1085
+ color: "green",
1086
+ children: [
1087
+ { id: "education.school.cafeteria", label: "School Cafeteria", level: "product", color: "pink" },
1088
+ { id: "education.school.gymnasium", label: "Gymnasium", level: "product", color: "pink" },
1089
+ ]
1090
+ },
1091
+ ]
1092
+ },
1093
+ ];
1094
+
1095
+ // Legacy flat VENUE_TYPES for backward compatibility
1096
+ export const VENUE_TYPES = [
1097
+ { value: "transit", label: "Transit", description: "Transportation hubs, stations, airports, bus stops" },
1098
+ { value: "retail", label: "Retail", description: "Shopping centers, malls, stores, marketplaces" },
1099
+ { value: "outdoor", label: "Outdoor", description: "Roadside billboards, street furniture, digital displays" },
1100
+ { value: "health_beauty", label: "Health & Beauty", description: "Hospitals, clinics, pharmacies, wellness centers" },
1101
+ { value: "point_of_care", label: "Point of Care", description: "Medical facilities, diagnostic centers, treatment rooms" },
1102
+ { value: "education", label: "Education", description: "Schools, universities, libraries, training centers" },
1103
+ { value: "office", label: "Office Buildings", description: "Corporate offices, business centers, co-working spaces" },
1104
+ { value: "entertainment", label: "Entertainment", description: "Cinemas, theaters, sports venues, entertainment complexes" },
1105
+ { value: "government", label: "Government", description: "Government buildings, public offices, civic centers" },
1106
+ { value: "financial", label: "Financial", description: "Banks, ATMs, financial institutions, trading centers" },
1107
+ { value: "residential", label: "Residential", description: "Apartment complexes, residential communities, housing areas" },
1108
+ ];
1109
+
1110
+ // Audience Behavior categories
1111
+ export const BEHAVIORS = [
1112
+ { value: "commuters", label: "Commuters", description: "People traveling to/from work during peak hours" },
1113
+ { value: "shoppers", label: "Shoppers", description: "Active shoppers in retail environments" },
1114
+ { value: "tourists", label: "Tourists", description: "Visitors and tourists in key destinations" },
1115
+ { value: "business_travelers", label: "Business Travelers", description: "Professional travelers in airports, hotels, business districts" },
1116
+ { value: "students", label: "Students", description: "University and college students in educational areas" },
1117
+ { value: "families", label: "Families", description: "Family groups in entertainment and recreational venues" },
1118
+ { value: "health_seekers", label: "Health Seekers", description: "People visiting medical facilities and wellness centers" },
1119
+ { value: "fitness_enthusiasts", label: "Fitness Enthusiasts", description: "Active individuals near gyms, sports facilities" },
1120
+ ];
1121
+
1122
+ // Interest & Activities categories
1123
+ export const INTERESTS = [
1124
+ "Sports & Fitness",
1125
+ "Technology",
1126
+ "Fashion & Style",
1127
+ "Food & Dining",
1128
+ "Travel",
1129
+ "Music",
1130
+ "Automotive",
1131
+ "Entertainment",
1132
+ "Home & Garden",
1133
+ "Health & Wellness"
1134
+ ];
1135
+
1136
+ // Triggers (for Triggers tab)
1137
+ export const TRIGGERS = [
1138
+ { value: "weather", label: "Weather Triggers", description: "Show ads based on specific weather conditions" },
1139
+ { value: "search_behavior", label: "Search Behavior", description: "Target based on recent search interests" },
1140
+ { value: "footfall", label: "Footfall Patterns", description: "Adjust based on real-time traffic patterns" },
1141
+ { value: "time_based", label: "Time-Based", description: "Show different content based on time of day" },
1142
+ { value: "local_events", label: "Local Events", description: "Target during specific local events or activities" },
1143
+ ];
1144
+
1145
+ // Points of Interest (POI)
1146
+ export interface POI {
1147
+ id: string;
1148
+ name: string;
1149
+ category: string;
1150
+ latitude: number;
1151
+ longitude: number;
1152
+ country: string;
1153
+ city?: string;
1154
+ address?: string;
1155
+ }
1156
+
1157
+
1158
+ // CMS Platform Types
1159
+ export type CMSPlatformType = "studio" | "broadsign" | "navori" | "signagelive" | "scala" | "yodeck" | "screencloud" | "other";
1160
+
1161
+ export const CMS_PLATFORMS: { value: CMSPlatformType; label: string; isInternal: boolean }[] = [
1162
+ { value: "studio", label: "Studio (Internal)", isInternal: true },
1163
+ { value: "broadsign", label: "Broadsign", isInternal: false },
1164
+ { value: "navori", label: "Navori QL", isInternal: false },
1165
+ { value: "signagelive", label: "signageOS", isInternal: false },
1166
+ { value: "scala", label: "Scala", isInternal: false },
1167
+ { value: "yodeck", label: "Yodeck", isInternal: false },
1168
+ { value: "screencloud", label: "ScreenCloud", isInternal: false },
1169
+ { value: "other", label: "Other", isInternal: false },
1170
+ ];
1171
+
1172
+ // Player Status Types
1173
+ export type PlayerStatusType = "online" | "offline" | "unknown";
1174
+
1175
+ export const PLAYER_STATUSES: { value: PlayerStatusType; label: string; color: string }[] = [
1176
+ { value: "online", label: "Online", color: "green" },
1177
+ { value: "offline", label: "Offline", color: "red" },
1178
+ { value: "unknown", label: "Unknown", color: "gray" },
1179
+ ];
1180
+
1181
+ // Player Status (read-only, fetched from CMS via Inventory)
1182
+ export const playerStatuses = pgTable("player_statuses", {
1183
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1184
+ screenId: varchar("screen_id").references(() => screens.id).notNull(),
1185
+ playerId: text("player_id"), // External player ID from CMS
1186
+ cmsType: text("cms_type").notNull(), // studio, broadsign, etc.
1187
+ status: text("status").notNull().default("unknown"), // online, offline, unknown
1188
+ lastHeartbeat: text("last_heartbeat"),
1189
+ currentContent: text("current_content"), // Currently playing content name
1190
+ connectionQuality: text("connection_quality"), // good, fair, poor
1191
+ lastUpdated: text("last_updated"),
1192
+ });
1193
+
1194
+ export const insertPlayerStatusSchema = createInsertSchema(playerStatuses).omit({ id: true });
1195
+ export type InsertPlayerStatus = z.infer<typeof insertPlayerStatusSchema>;
1196
+ export type PlayerStatus = typeof playerStatuses.$inferSelect;
1197
+
1198
+ // Proof of Play (PoP) Records - fetched from CMS or derived from playlogs
1199
+ export type ProofOfPlaySource = "cms" | "playlog_upload";
1200
+ export type ProofOfPlayStatus = "processed" | "pending" | "failed";
1201
+
1202
+ export const PROOF_OF_PLAY_SOURCES: { value: ProofOfPlaySource; label: string }[] = [
1203
+ { value: "cms", label: "CMS (Automatic)" },
1204
+ { value: "playlog_upload", label: "Manual Upload" },
1205
+ ];
1206
+
1207
+ export const PROOF_OF_PLAY_STATUSES: { value: ProofOfPlayStatus; label: string; color: string }[] = [
1208
+ { value: "processed", label: "Processed", color: "green" },
1209
+ { value: "pending", label: "Pending", color: "yellow" },
1210
+ { value: "failed", label: "Failed", color: "red" },
1211
+ ];
1212
+
1213
+ export const proofOfPlay = pgTable("proof_of_play", {
1214
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1215
+ screenId: varchar("screen_id").references(() => screens.id),
1216
+ playerId: text("player_id"),
1217
+ creativeId: varchar("creative_id").references(() => creatives.id),
1218
+ dealId: varchar("deal_id").references(() => deals.id),
1219
+ lineItemId: varchar("line_item_id").references(() => lineItems.id),
1220
+ startTimestamp: text("start_timestamp").notNull(),
1221
+ endTimestamp: text("end_timestamp").notNull(),
1222
+ durationSeconds: integer("duration_seconds").notNull(),
1223
+ impressionCount: integer("impression_count").default(1),
1224
+ mediaFileUrl: text("media_file_url"), // For verification
1225
+ proofImageUrl: text("proof_image_url"), // Optional proof image
1226
+ proofVideoUrl: text("proof_video_url"), // Optional proof video
1227
+ source: text("source").notNull().default("cms"), // cms or playlog_upload
1228
+ status: text("status").notNull().default("pending"), // processed, pending, failed
1229
+ errorMessage: text("error_message"),
1230
+ mediaOwnerId: varchar("media_owner_id").references(() => mediaOwners.id),
1231
+ uploadedBy: text("uploaded_by"),
1232
+ uploadedAt: text("uploaded_at"),
1233
+ processedAt: text("processed_at"),
1234
+ syncedToMeasure: boolean("synced_to_measure").default(false),
1235
+ syncedAt: text("synced_at"),
1236
+ });
1237
+
1238
+ export const insertProofOfPlaySchema = createInsertSchema(proofOfPlay).omit({ id: true });
1239
+ export type InsertProofOfPlay = z.infer<typeof insertProofOfPlaySchema>;
1240
+ export type ProofOfPlay = typeof proofOfPlay.$inferSelect;
1241
+
1242
+ // Playlog Uploads - batch uploads from media owners
1243
+ export type PlaylogUploadStatus = "uploaded" | "validating" | "processing" | "completed" | "failed";
1244
+
1245
+ export const PLAYLOG_UPLOAD_STATUSES: { value: PlaylogUploadStatus; label: string; color: string }[] = [
1246
+ { value: "uploaded", label: "Uploaded", color: "blue" },
1247
+ { value: "validating", label: "Validating", color: "yellow" },
1248
+ { value: "processing", label: "Processing", color: "yellow" },
1249
+ { value: "completed", label: "Completed", color: "green" },
1250
+ { value: "failed", label: "Failed", color: "red" },
1251
+ ];
1252
+
1253
+ export const playlogUploads = pgTable("playlog_uploads", {
1254
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1255
+ dealId: varchar("deal_id").references(() => deals.id),
1256
+ lineItemId: varchar("line_item_id").references(() => lineItems.id),
1257
+ creativeId: varchar("creative_id").references(() => creatives.id), // Optional filter
1258
+ mediaOwnerId: varchar("media_owner_id").references(() => mediaOwners.id).notNull(),
1259
+ fileName: text("file_name").notNull(),
1260
+ fileUrl: text("file_url").notNull(),
1261
+ totalRecords: integer("total_records").default(0),
1262
+ validRecords: integer("valid_records").default(0),
1263
+ invalidRecords: integer("invalid_records").default(0),
1264
+ status: text("status").notNull().default("uploaded"),
1265
+ validationErrors: jsonb("validation_errors"), // Array of error messages
1266
+ proofMediaUrls: text("proof_media_urls").array(), // Images/videos as proof
1267
+ uploadedBy: text("uploaded_by").notNull(),
1268
+ uploadedAt: text("uploaded_at").notNull(),
1269
+ processedAt: text("processed_at"),
1270
+ notes: text("notes"),
1271
+ });
1272
+
1273
+ export const insertPlaylogUploadSchema = createInsertSchema(playlogUploads).omit({ id: true });
1274
+ export type InsertPlaylogUpload = z.infer<typeof insertPlaylogUploadSchema>;
1275
+ export type PlaylogUpload = typeof playlogUploads.$inferSelect;
1276
+
1277
+ // Playlog Template Structure (for CSV download)
1278
+ // Based on CMS industry standards (Broadsign, etc.)
1279
+ // Note: Impressions are calculated by Measure platform, not provided by user
1280
+ // Duration is calculated from start/end timestamps
1281
+ export interface PlaylogTemplateRow {
1282
+ dealId: string; // Deal ID for mapping
1283
+ lineItemId: string; // Line Item ID for mapping
1284
+ referenceId: string; // Inventory reference ID (was screenId)
1285
+ playerId?: string; // Player ID (optional)
1286
+ creativeId: string; // Creative ID
1287
+ startDatetime: string; // ISO format (e.g., 2024-01-15T10:00:00Z)
1288
+ endDatetime: string; // ISO format
1289
+ notes?: string; // Optional notes
1290
+ }
1291
+
1292
+ // Playlog Template Guidelines (shown to user)
1293
+ export const PLAYLOG_TEMPLATE_GUIDELINES = {
1294
+ description: "This template is used to upload playlog data for traditional campaigns.",
1295
+ columns: {
1296
+ dealId: "The Deal ID from the platform. Required for mapping.",
1297
+ lineItemId: "The Line Item ID from the platform. Required for mapping.",
1298
+ referenceId: "Your inventory reference ID (e.g., screen ID, display ID).",
1299
+ playerId: "Optional player device ID if different from reference ID.",
1300
+ creativeId: "The Creative ID from the platform or your internal reference.",
1301
+ startDatetime: "When the content started playing. Format: ISO 8601 (e.g., 2024-01-15T10:00:00Z)",
1302
+ endDatetime: "When the content finished playing. Format: ISO 8601 (e.g., 2024-01-15T10:00:15Z)",
1303
+ notes: "Optional notes for this play record.",
1304
+ },
1305
+ instructions: [
1306
+ "Do not modify the header row (first row).",
1307
+ "The second row is a sample - replace it with your actual data.",
1308
+ "Duration and impressions are calculated automatically by the Measure platform.",
1309
+ "Dates must be in ISO 8601 format with timezone (Z for UTC or +HH:MM offset).",
1310
+ "If uploading Excel (.xlsx), save as CSV before upload, or upload Excel directly.",
1311
+ ],
1312
+ };
1313
+
1314
+ // Geotargeting Rules
1315
+ export const geoTargetingRules = pgTable("geo_targeting_rules", {
1316
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1317
+ entityType: text("entity_type").notNull(), // 'deal' | 'lineItem'
1318
+ entityId: text("entity_id").notNull(),
1319
+ mode: text("mode").notNull(), // 'regions' | 'proximity'
1320
+ name: text("name"),
1321
+ // For regions mode
1322
+ locations: jsonb("locations"), // Array of { name, lat, lng, radius, include }
1323
+ // For proximity mode
1324
+ shapes: jsonb("shapes"), // Array of { type: 'polygon'|'circle'|'freeform', coordinates, measurements, include }
1325
+ // POI targeting
1326
+ poiCategories: text("poi_categories").array(),
1327
+ poiLocations: jsonb("poi_locations"), // Array of selected POI IDs with include/exclude
1328
+ createdAt: text("created_at").notNull(),
1329
+ updatedAt: text("updated_at").notNull(),
1330
+ });
1331
+
1332
+ export const insertGeoTargetingRuleSchema = createInsertSchema(geoTargetingRules).omit({ id: true });
1333
+ export type InsertGeoTargetingRule = z.infer<typeof insertGeoTargetingRuleSchema>;
1334
+ export type GeoTargetingRule = typeof geoTargetingRules.$inferSelect;
1335
+
1336
+ // Geotargeting Location interface
1337
+ export interface GeoLocation {
1338
+ id: string;
1339
+ name: string;
1340
+ address?: string;
1341
+ lat: number;
1342
+ lng: number;
1343
+ radius: number; // in meters
1344
+ include: boolean;
1345
+ }
1346
+
1347
+ // Geotargeting Shape interface
1348
+ export interface GeoShape {
1349
+ id: string;
1350
+ type: 'polygon' | 'circle' | 'point';
1351
+ coordinates: number[][][] | number[][] | number[];
1352
+ measurements: string; // e.g., "8,670,133 m²"
1353
+ include: boolean;
1354
+ }
1355
+
1356
+ // Transit Routes (for transit inventory visualization)
1357
+ export const transitRoutes = pgTable("transit_routes", {
1358
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1359
+ name: text("name").notNull(),
1360
+ routeType: text("route_type").notNull(), // 'train' | 'bus' | 'mrt' | 'lrt'
1361
+ routeNumber: text("route_number"),
1362
+ color: text("color"), // hex color for map display
1363
+ geojson: jsonb("geojson").notNull(), // GeoJSON LineString or MultiLineString
1364
+ city: text("city"),
1365
+ country: text("country"),
1366
+ operatorName: text("operator_name"),
1367
+ });
1368
+
1369
+ export const insertTransitRouteSchema = createInsertSchema(transitRoutes).omit({ id: true });
1370
+ export type InsertTransitRoute = z.infer<typeof insertTransitRouteSchema>;
1371
+ export type TransitRoute = typeof transitRoutes.$inferSelect;
1372
+
1373
+ // Transit Zones (for taxi/ride-hail coverage areas)
1374
+ export const transitZones = pgTable("transit_zones", {
1375
+ id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
1376
+ name: text("name").notNull(),
1377
+ zoneType: text("zone_type").notNull(), // 'taxi' | 'ridehail' | 'coverage'
1378
+ geojson: jsonb("geojson").notNull(), // GeoJSON Polygon
1379
+ color: text("color"),
1380
+ city: text("city"),
1381
+ country: text("country"),
1382
+ });
1383
+
1384
+ export const insertTransitZoneSchema = createInsertSchema(transitZones).omit({ id: true });
1385
+ export type InsertTransitZone = z.infer<typeof insertTransitZoneSchema>;
1386
+ export type TransitZone = typeof transitZones.$inferSelect;
1387
+
1388
+ // Creative Specifications (for Content Hub upload validation)
1389
+ export interface CreativeSpec {
1390
+ type: string; // 'display' | 'video' | 'html' | 'native'
1391
+ formats: string[];
1392
+ maxSizeBytes: number;
1393
+ maxSizeMB: number;
1394
+ dimensions?: { width: number; height: number; label: string }[];
1395
+ maxDuration?: number; // seconds, for video
1396
+ minDuration?: number;
1397
+ additionalRequirements?: string[];
1398
+ }
1399
+
1400
+ // =====================================================
1401
+ // RECOMMENDATION ENGINE V2 TYPES
1402
+ // Integration with MW Planner Recommendation Engine
1403
+ // =====================================================
1404
+
1405
+ // Campaign Goal Types - used for recommendation optimization
1406
+ export type CampaignGoalType = 'impressions' | 'reach' | 'sov' | 'ad_plays' | 'carbon_emission';
1407
+
1408
+ export const CAMPAIGN_GOAL_TYPES: { value: CampaignGoalType; label: string; description: string }[] = [
1409
+ { value: 'impressions', label: 'Impressions', description: 'Maximize total ad views' },
1410
+ { value: 'reach', label: 'Reach', description: 'Maximize unique people who see the ad' },
1411
+ { value: 'sov', label: 'Share of Voice', description: 'Achieve target percentage of available ad plays' },
1412
+ { value: 'ad_plays', label: 'Ad Plays', description: 'Maximize number of times ad is displayed' },
1413
+ { value: 'carbon_emission', label: 'Carbon Emission', description: 'Minimize environmental impact' },
1414
+ ];
1415
+
1416
+ // Inventory Classification - broadest categorization (for recommendations)
1417
+ export type RecommendationClassification = 'digital' | 'classic' | 'audio';
1418
+
1419
+ export const RECOMMENDATION_CLASSIFICATIONS: { value: RecommendationClassification; label: string }[] = [
1420
+ { value: 'digital', label: 'Digital' },
1421
+ { value: 'classic', label: 'Classic' },
1422
+ { value: 'audio', label: 'Audio' },
1423
+ ];
1424
+
1425
+ // Inventory Type - categorization by venue/placement context
1426
+ export type RecommendationInventoryType = 'ooh' | 'transit' | 'retail' | 'network' | 'radio' | 'experiential';
1427
+
1428
+ export const RECOMMENDATION_INVENTORY_TYPES: { value: RecommendationInventoryType; label: string }[] = [
1429
+ { value: 'ooh', label: 'OOH' },
1430
+ { value: 'transit', label: 'Transit' },
1431
+ { value: 'retail', label: 'Retail' },
1432
+ { value: 'network', label: 'Network' },
1433
+ { value: 'radio', label: 'Radio' },
1434
+ { value: 'experiential', label: 'Experiential' },
1435
+ ];
1436
+
1437
+ // Recommendation Score Components - 8 factors from the engine
1438
+ export interface RecommendationScoreComponents {
1439
+ measureFit: number; // 0-100: How well inventory delivers campaign goal
1440
+ geoFit: number; // 0-100: Location relevance to target geography
1441
+ availability: number; // 0-100: Fraction of requested schedule available
1442
+ budgetFit: number; // 0-100: Cost efficiency relative to budget
1443
+ audienceFit: number; // 0-100: Audience segment overlap
1444
+ brandFit: number; // 0-100: Brand category to venue affinity
1445
+ qualityFit: number; // 0-100: Physical/creative quality score
1446
+ timeFit: number; // 0-100: Daypart/date alignment
1447
+ }
1448
+
1449
+ // Default scoring weights by goal type
1450
+ export const RECOMMENDATION_WEIGHTS: Record<CampaignGoalType | 'default', Record<keyof RecommendationScoreComponents, number>> = {
1451
+ default: { measureFit: 0.20, geoFit: 0.20, availability: 0.10, budgetFit: 0.20, audienceFit: 0.10, brandFit: 0.10, qualityFit: 0.06, timeFit: 0.04 },
1452
+ impressions: { measureFit: 0.25, geoFit: 0.20, availability: 0.10, budgetFit: 0.18, audienceFit: 0.10, brandFit: 0.08, qualityFit: 0.05, timeFit: 0.04 },
1453
+ reach: { measureFit: 0.22, geoFit: 0.22, availability: 0.12, budgetFit: 0.15, audienceFit: 0.12, brandFit: 0.08, qualityFit: 0.05, timeFit: 0.04 },
1454
+ sov: { measureFit: 0.25, geoFit: 0.15, availability: 0.15, budgetFit: 0.20, audienceFit: 0.08, brandFit: 0.08, qualityFit: 0.05, timeFit: 0.04 },
1455
+ ad_plays: { measureFit: 0.28, geoFit: 0.15, availability: 0.12, budgetFit: 0.18, audienceFit: 0.10, brandFit: 0.08, qualityFit: 0.05, timeFit: 0.04 },
1456
+ carbon_emission: { measureFit: 0.18, geoFit: 0.15, availability: 0.10, budgetFit: 0.15, audienceFit: 0.10, brandFit: 0.08, qualityFit: 0.20, timeFit: 0.04 },
1457
+ };
1458
+
1459
+ // Individual Inventory Recommendation
1460
+ export interface InventoryRecommendation {
1461
+ inventoryId: string;
1462
+ inventoryName: string;
1463
+ screenType: string;
1464
+ classification: RecommendationClassification;
1465
+ inventoryType: RecommendationInventoryType;
1466
+ format: string;
1467
+ location: {
1468
+ city: string;
1469
+ country: string;
1470
+ latitude: number;
1471
+ longitude: number;
1472
+ };
1473
+ score: number; // Final score 0-100
1474
+ scoreComponents: RecommendationScoreComponents;
1475
+ metrics: {
1476
+ estimatedImpressions: number;
1477
+ estimatedReach?: number;
1478
+ estimatedAdPlays?: number;
1479
+ estimatedCost: number;
1480
+ cpm: number;
1481
+ dailyImpressions: number;
1482
+ };
1483
+ availability: {
1484
+ availableDays: number;
1485
+ totalDays: number;
1486
+ summary: string; // e.g., "25/31 days available"
1487
+ };
1488
+ whyRecommended: string; // AI-generated explanation
1489
+ aiUsed: boolean;
1490
+ aiConfidence?: number;
1491
+ warnings: string[];
1492
+ }
1493
+
1494
+ // Recommendation Request - sent to the engine
1495
+ export interface RecommendationRequest {
1496
+ country: string;
1497
+ startDate: string;
1498
+ endDate: string;
1499
+ budget?: number;
1500
+ currency?: string;
1501
+ goalType?: CampaignGoalType;
1502
+ goalValue?: number;
1503
+ brandId?: string;
1504
+ brandName?: string;
1505
+ audienceSegments?: string[];
1506
+ geography?: {
1507
+ cities?: string[];
1508
+ polygon?: number[][][];
1509
+ poiIds?: string[];
1510
+ };
1511
+ limit?: number;
1512
+ seed?: string; // For deterministic results
1513
+ }
1514
+
1515
+ // Recommendation Response - from the engine
1516
+ export interface RecommendationResponse {
1517
+ runId: string;
1518
+ status: 'success' | 'no_matches' | 'partial' | 'error';
1519
+ recommendations: InventoryRecommendation[];
1520
+ summary: {
1521
+ totalInventoriesAnalyzed: number;
1522
+ totalReturned: number;
1523
+ estimatedTotalImpressions: number;
1524
+ estimatedTotalReach?: number;
1525
+ estimatedTotalCost: number;
1526
+ budgetUtilization: number; // Percentage
1527
+ goalAchievement?: number; // Percentage of goal met
1528
+ };
1529
+ budgetAllocation?: {
1530
+ byClassification: Record<RecommendationClassification, { percentage: number; amount: number }>;
1531
+ byType: Record<RecommendationInventoryType, { percentage: number; amount: number }>;
1532
+ byCity?: Record<string, { percentage: number; amount: number }>;
1533
+ };
1534
+ suggestions?: string[]; // Alternative suggestions if no matches
1535
+ warnings: string[];
1536
+ generatedAt: string;
1537
+ }
1538
+
1539
+ // Auto-Optimize Request for Line Items
1540
+ export interface AutoOptimizeRequest {
1541
+ lineItemId: string;
1542
+ dealId: string;
1543
+ budget?: number;
1544
+ goalType?: CampaignGoalType;
1545
+ goalValue?: number;
1546
+ maxInventories?: number;
1547
+ preferredTypes?: RecommendationInventoryType[];
1548
+ }
1549
+
1550
+ // Auto-Optimize Response
1551
+ export interface AutoOptimizeResult {
1552
+ lineItemId: string;
1553
+ selectedInventories: InventoryRecommendation[];
1554
+ scheduleCreated: boolean;
1555
+ summary: {
1556
+ inventoriesAdded: number;
1557
+ estimatedImpressions: number;
1558
+ estimatedCost: number;
1559
+ budgetRemaining: number;
1560
+ };
1561
+ warnings: string[];
1562
+ }
1563
+
1564
+ // Validation Schemas for Recommendation API
1565
+ export const recommendationRequestSchema = z.object({
1566
+ country: z.string().default("US"),
1567
+ startDate: z.string().min(1, "Start date is required"),
1568
+ endDate: z.string().min(1, "End date is required"),
1569
+ budget: z.number().optional(),
1570
+ currency: z.string().default("USD"),
1571
+ goalType: z.enum(["impressions", "reach", "sov", "ad_plays", "carbon_emission"]).optional(),
1572
+ goalValue: z.number().optional(),
1573
+ brandId: z.string().optional(),
1574
+ brandName: z.string().optional(),
1575
+ audienceSegments: z.array(z.string()).optional(),
1576
+ geography: z.object({
1577
+ cities: z.array(z.string()).optional(),
1578
+ polygon: z.array(z.array(z.array(z.number()))).optional(),
1579
+ poiIds: z.array(z.string()).optional(),
1580
+ }).optional(),
1581
+ limit: z.number().min(1).max(100).default(10),
1582
+ seed: z.string().optional(),
1583
+ });
1584
+
1585
+ export type ValidatedRecommendationRequest = z.infer<typeof recommendationRequestSchema>;
1586
+
1587
+ export const autoOptimizeRequestSchema = z.object({
1588
+ budget: z.number().optional(),
1589
+ goalType: z.enum(["impressions", "reach", "sov", "ad_plays", "carbon_emission"]).optional(),
1590
+ goalValue: z.number().optional(),
1591
+ maxInventories: z.number().min(1).max(20).default(5),
1592
+ preferredTypes: z.array(z.enum(["ooh", "transit", "retail", "network", "radio", "experiential"])).optional(),
1593
+ });
1594
+
1595
+ export type ValidatedAutoOptimizeRequest = z.infer<typeof autoOptimizeRequestSchema>;