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.
- package/.ci/staging.yml +191 -0
- package/.dockerignore +117 -0
- package/.env +40 -0
- package/.env.staging +38 -0
- package/.gitlab-ci.yml +16 -0
- package/DEMO_STATUS.md +579 -0
- package/Dockerfile +61 -0
- package/Influence-MW-AdServer-12-02-2026/client/index.html +17 -0
- package/Influence-MW-AdServer-12-02-2026/client/public/favicon.png +0 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/App.tsx +91 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/advanced-map-drawer.tsx +1131 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ai-recommendation-panel.tsx +379 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/app-sidebar.tsx +183 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/auto-optimize-button.tsx +184 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/availability-drawer.tsx +385 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/brand-insights-panel.tsx +87 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/create-agency-drawer.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/create-brand-drawer.tsx +275 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/creative-assignment.tsx +526 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table-toolbar.tsx +148 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/data-table.tsx +158 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/filter-drawer.tsx +356 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/form-insights-panel.tsx +82 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/geography-selector.tsx +699 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/header-user-menu.tsx +178 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/history-drawer.tsx +313 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-availability-section.tsx +176 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-format-drawer.tsx +173 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/inventory-selector.tsx +401 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/manual-inventory-drawer.tsx +368 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/mapbox-map.tsx +368 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/market-insights-panel.tsx +202 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/media-owner-drawer.tsx +217 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/metric-card.tsx +58 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/page-header.tsx +27 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/player-status-indicator.tsx +137 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/poi-targeting-drawer.tsx +298 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/recommendation-score-badge.tsx +102 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/recommended-inventories-panel.tsx +248 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/searchable-combobox.tsx +134 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/signal-visualizations.tsx +407 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/status-badge.tsx +35 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-provider.tsx +73 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/theme-toggle.tsx +37 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/traffic-slider.tsx +75 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/accordion.tsx +56 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert-dialog.tsx +139 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/alert.tsx +59 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/aspect-ratio.tsx +5 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/avatar.tsx +51 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/badge.tsx +38 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/breadcrumb.tsx +115 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/button.tsx +62 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/calendar.tsx +68 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/card.tsx +85 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/carousel.tsx +260 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/chart.tsx +365 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/checkbox.tsx +28 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/collapsible.tsx +11 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/command.tsx +151 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/context-menu.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dialog.tsx +122 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/drawer.tsx +118 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/dropdown-menu.tsx +198 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/form.tsx +178 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/hover-card.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input-otp.tsx +69 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/input.tsx +23 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/label.tsx +24 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/menubar.tsx +256 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/navigation-menu.tsx +128 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/pagination.tsx +117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/popover.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/progress.tsx +28 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/radio-group.tsx +42 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/resizable.tsx +45 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/scroll-area.tsx +46 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/select.tsx +160 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/separator.tsx +29 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sheet.tsx +140 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/sidebar.tsx +727 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/skeleton.tsx +15 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/slider.tsx +26 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/switch.tsx +27 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/table.tsx +117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tabs.tsx +53 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/textarea.tsx +22 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toast.tsx +127 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toaster.tsx +33 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle-group.tsx +61 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/toggle.tsx +43 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/ui/tooltip.tsx +30 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/vendor-stores-modal.tsx +336 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-drawer.tsx +359 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/components/venue-type-selector.tsx +436 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-mobile.tsx +19 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/hooks/use-toast.ts +191 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/index.css +244 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/queryClient.ts +57 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/utils.ts +39 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/lib/venue-taxonomy.ts +532 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/main.tsx +5 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/assign-creative.tsx +781 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/content-hub.tsx +995 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/custom-pois.tsx +431 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/dashboard.tsx +620 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-detail.tsx +1062 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deal-form.tsx +1570 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/deals.tsx +716 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/edit-creative-assignment.tsx +1051 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/geotargeting.tsx +675 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/integrations.tsx +425 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-creatives.tsx +622 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-item-form.tsx +3132 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/line-items.tsx +530 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/not-found.tsx +21 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play-upload.tsx +479 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/proof-of-play.tsx +880 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/reports.tsx +235 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/settings.tsx +652 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/signal-form.tsx +1117 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/signals.tsx +366 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/tags.tsx +332 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/pages/venues.tsx +381 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/types/mapbox-gl-draw.d.ts +37 -0
- package/Influence-MW-AdServer-12-02-2026/client/src/types/react-simple-maps.d.ts +57 -0
- package/Influence-MW-AdServer-12-02-2026/components.json +20 -0
- package/Influence-MW-AdServer-12-02-2026/docs/PRD.md +3373 -0
- package/Influence-MW-AdServer-12-02-2026/docs/influence-feature-mapping.csv +498 -0
- package/Influence-MW-AdServer-12-02-2026/drizzle.config.ts +14 -0
- package/Influence-MW-AdServer-12-02-2026/package-lock.json +9672 -0
- package/Influence-MW-AdServer-12-02-2026/package.json +118 -0
- package/Influence-MW-AdServer-12-02-2026/postcss.config.js +6 -0
- package/Influence-MW-AdServer-12-02-2026/replit.md +91 -0
- package/Influence-MW-AdServer-12-02-2026/script/build.ts +67 -0
- package/Influence-MW-AdServer-12-02-2026/scripts/create-miro-diagrams.cjs +318 -0
- package/Influence-MW-AdServer-12-02-2026/scripts/create-remaining-diagrams.cjs +270 -0
- package/Influence-MW-AdServer-12-02-2026/server/index.ts +103 -0
- package/Influence-MW-AdServer-12-02-2026/server/recommendation-service.ts +319 -0
- package/Influence-MW-AdServer-12-02-2026/server/routes.ts +1890 -0
- package/Influence-MW-AdServer-12-02-2026/server/static.ts +19 -0
- package/Influence-MW-AdServer-12-02-2026/server/storage.ts +2058 -0
- package/Influence-MW-AdServer-12-02-2026/server/vite.ts +58 -0
- package/Influence-MW-AdServer-12-02-2026/shared/schema.ts +1595 -0
- package/Influence-MW-AdServer-12-02-2026/tailwind.config.ts +107 -0
- package/Influence-MW-AdServer-12-02-2026/tsconfig.json +23 -0
- package/Influence-MW-AdServer-12-02-2026/vite.config.ts +40 -0
- package/LINE_ITEM_BUDGET_FIELD_MAPPING.md +178 -0
- package/PCM/.env.example +92 -0
- package/PCM/README.md +558 -0
- package/PCM/docs/TEST_CASES.md +422 -0
- package/PCM/index.js +106 -0
- package/PCM/package-lock.json +3282 -0
- package/PCM/package.json +32 -0
- package/PCM/replit.md +64 -0
- package/PCM/schema.sql +495 -0
- package/PCM/scripts/export-schema.js +183 -0
- package/PCM/scripts/seed-comprehensive.js +631 -0
- package/PCM/scripts/seed-production.js +477 -0
- package/PCM/src/config/db.js +56 -0
- package/PCM/src/config/swagger.js +5975 -0
- package/PCM/src/dto/EmailRequestDTO.js +166 -0
- package/PCM/src/middleware/errorHandler.js +52 -0
- package/PCM/src/middleware/logger.js +26 -0
- package/PCM/src/migrations/001_add_campaign_mode_fields.sql +36 -0
- package/PCM/src/migrations/002_create_deal_id_counters.sql +22 -0
- package/PCM/src/migrations/003_update_publishers_column.sql +15 -0
- package/PCM/src/migrations/004_add_direct_dealtype_and_advertiser.sql +5 -0
- package/PCM/src/migrations/005_add_programmatic_fields_and_update_enums.sql +31 -0
- package/PCM/src/migrations/006_add_line_item_programmatic_fields.sql +12 -0
- package/PCM/src/migrations/007_add_line_item_direct_fields.sql +15 -0
- package/PCM/src/migrations/008_add_inventory_fields.sql +45 -0
- package/PCM/src/migrations/009_move_inventory_fields_to_metadata.sql +32 -0
- package/PCM/src/migrations/010_add_draft_status_and_line_items_count.sql +23 -0
- package/PCM/src/migrations/011_add_planning_field.sql +21 -0
- package/PCM/src/migrations/012_fix_inventory_composite_pk.sql +17 -0
- package/PCM/src/migrations/013_make_external_id_optional.sql +3 -0
- package/PCM/src/migrations/014_create_change_history.sql +38 -0
- package/PCM/src/migrations/016_create_publisher_insertion_orders.sql +33 -0
- package/PCM/src/migrations/017_fix_line_item_id_fk_reference.sql +86 -0
- package/PCM/src/migrations/018_create_approval_tables.sql +44 -0
- package/PCM/src/migrations/019_add_encrypted_token_column.sql +2 -0
- package/PCM/src/migrations/020_add_rejection_reason_to_deals.sql +10 -0
- package/PCM/src/migrations/021_add_publisher_external_id_to_inventories.sql +12 -0
- package/PCM/src/migrations/022_add_line_item_extended_fields.sql +24 -0
- package/PCM/src/migrations/023_add_base_price_fields.sql +8 -0
- package/PCM/src/migrations/run-migrations.js +46 -0
- package/PCM/src/models/ApprovalOTP.js +51 -0
- package/PCM/src/models/ApprovalToken.js +79 -0
- package/PCM/src/models/ChangeHistory.js +107 -0
- package/PCM/src/models/Deal.js +186 -0
- package/PCM/src/models/DealIdCounter.js +28 -0
- package/PCM/src/models/LineItem.js +227 -0
- package/PCM/src/models/LineItemCreative.js +89 -0
- package/PCM/src/models/LineItemInventory.js +115 -0
- package/PCM/src/models/PublisherInsertionOrder.js +93 -0
- package/PCM/src/models/TransactionHistory.js +34 -0
- package/PCM/src/models/associations.js +81 -0
- package/PCM/src/routes/approval.js +321 -0
- package/PCM/src/routes/creatives.js +437 -0
- package/PCM/src/routes/deals.js +1638 -0
- package/PCM/src/routes/digitalSignage.js +242 -0
- package/PCM/src/routes/insertionOrders.js +380 -0
- package/PCM/src/routes/lineItems.js +926 -0
- package/PCM/src/routes/system.js +384 -0
- package/PCM/src/services/ApprovalService.js +885 -0
- package/PCM/src/services/CampaignImportConverter.js +631 -0
- package/PCM/src/services/CampaignModeService.js +273 -0
- package/PCM/src/services/CampaignStatusService.js +395 -0
- package/PCM/src/services/ChangeHistoryService.js +316 -0
- package/PCM/src/services/DealIdService.js +94 -0
- package/PCM/src/services/DealResponseFormatter.js +90 -0
- package/PCM/src/services/EmailNotificationService.js +315 -0
- package/PCM/src/services/LineItemResponseFormatter.js +122 -0
- package/PCM/src/services/LineItemStatusService.js +380 -0
- package/PCM/src/tests/comprehensiveTestRunner.js +360 -0
- package/PCM/src/tests/comprehensiveTests.js +1277 -0
- package/PCM/src/tests/dealTypeUnitTests.js +1058 -0
- package/PCM/src/tests/testRunner.js +248 -0
- package/PCM/src/utils/caseConverter.js +92 -0
- package/PCM/src/utils/dealCalculations.js +206 -0
- package/PCM/src/utils/lineItemPayloadNormalizer.js +41 -0
- package/PCM/src/utils/payloadNormalizer.js +34 -0
- package/PCM/src/utils/sourceNormalizer.js +56 -0
- package/PCM/src/validators/creativeValidator.js +27 -0
- package/PCM/src/validators/dealValidator.js +203 -0
- package/PCM/src/validators/lineItemValidator.js +489 -0
- package/PCM/tests/approval-flows.test.js +238 -0
- package/PCM/tests/approval-workflow.test.js +291 -0
- package/PCM/tests/campaign-import-converter.test.js +543 -0
- package/PCM/tests/campaign-import-e2e.test.js +520 -0
- package/PCM/tests/campaign-status.test.js +539 -0
- package/PCM/tests/direct-publisher-split-reimport.test.js +460 -0
- package/PCM/tests/e2e/digital-signage.test.js +145 -0
- package/PCM/tests/e2e/search-filter-pagination.test.js +399 -0
- package/PCM/tests/e2e-comprehensive.test.js +3446 -0
- package/PCM/tests/edge-cases.test.js +340 -0
- package/PCM/tests/line-item-status.test.js +340 -0
- package/PCM/tests/seller-account-external-ids.test.js +877 -0
- package/PCM/tests/source-validation.test.js +324 -0
- package/PRD.md +3373 -0
- package/README.md +186 -0
- package/client/index.html +35 -0
- package/client/public/DEMO_STATUS.md +579 -0
- package/client/public/img/MW-logo-trans_1754045676555.png +0 -0
- package/client/public/locales/ar/approval.json +144 -0
- package/client/public/locales/ar/buyer.json +61 -0
- package/client/public/locales/ar/campaigns.json +1 -0
- package/client/public/locales/ar/common.json +218 -0
- package/client/public/locales/ar/contentHub.json +266 -0
- package/client/public/locales/ar/creatives.json +79 -0
- package/client/public/locales/ar/dashboard.json +57 -0
- package/client/public/locales/ar/deals.json +886 -0
- package/client/public/locales/ar/dsp.json +131 -0
- package/client/public/locales/ar/inventory.json +201 -0
- package/client/public/locales/ar/lineItems.json +553 -0
- package/client/public/locales/ar/navigation.json +48 -0
- package/client/public/locales/ar/wizard.json +1 -0
- package/client/public/locales/en/approval.json +144 -0
- package/client/public/locales/en/buyer.json +65 -0
- package/client/public/locales/en/campaigns.json +1 -0
- package/client/public/locales/en/common.json +218 -0
- package/client/public/locales/en/contentHub.json +266 -0
- package/client/public/locales/en/creatives.json +79 -0
- package/client/public/locales/en/dashboard.json +57 -0
- package/client/public/locales/en/deals.json +886 -0
- package/client/public/locales/en/dsp.json +131 -0
- package/client/public/locales/en/inventory.json +201 -0
- package/client/public/locales/en/lineItems.json +659 -0
- package/client/public/locales/en/navigation.json +48 -0
- package/client/public/locales/en/wizard.json +1 -0
- package/client/public/locales/ja/approval.json +144 -0
- package/client/public/locales/ja/buyer.json +61 -0
- package/client/public/locales/ja/campaigns.json +1 -0
- package/client/public/locales/ja/common.json +218 -0
- package/client/public/locales/ja/contentHub.json +266 -0
- package/client/public/locales/ja/creatives.json +79 -0
- package/client/public/locales/ja/dashboard.json +57 -0
- package/client/public/locales/ja/deals.json +886 -0
- package/client/public/locales/ja/dsp.json +131 -0
- package/client/public/locales/ja/inventory.json +201 -0
- package/client/public/locales/ja/lineItems.json +553 -0
- package/client/public/locales/ja/navigation.json +48 -0
- package/client/public/locales/ja/wizard.json +1 -0
- package/client/public/locales/zh/approval.json +144 -0
- package/client/public/locales/zh/buyer.json +61 -0
- package/client/public/locales/zh/campaigns.json +1 -0
- package/client/public/locales/zh/common.json +218 -0
- package/client/public/locales/zh/contentHub.json +266 -0
- package/client/public/locales/zh/creatives.json +79 -0
- package/client/public/locales/zh/dashboard.json +57 -0
- package/client/public/locales/zh/deals.json +886 -0
- package/client/public/locales/zh/dsp.json +131 -0
- package/client/public/locales/zh/inventory.json +201 -0
- package/client/public/locales/zh/lineItems.json +553 -0
- package/client/public/locales/zh/navigation.json +48 -0
- package/client/public/locales/zh/wizard.json +1 -0
- package/client/public/manifest.json +36 -0
- package/client/src/App.tsx +464 -0
- package/client/src/components/app-sidebar.tsx +312 -0
- package/client/src/components/approval/approval-decision-form.test.tsx +294 -0
- package/client/src/components/approval/approval-decision-form.tsx +326 -0
- package/client/src/components/approval/approval-sheet.tsx +631 -0
- package/client/src/components/approval/line-item-details-sheet.tsx +371 -0
- package/client/src/components/approval/otp-verification.test.tsx +337 -0
- package/client/src/components/approval/otp-verification.tsx +180 -0
- package/client/src/components/content-hub/bulk-transcode-dialog.tsx +379 -0
- package/client/src/components/content-hub/content-hub-manager-v2.tsx +574 -0
- package/client/src/components/content-hub/content-hub-manager.tsx +330 -0
- package/client/src/components/content-hub/creative-card.tsx +456 -0
- package/client/src/components/content-hub/creative-detail-sheet.tsx +685 -0
- package/client/src/components/content-hub/creative-filters.tsx +457 -0
- package/client/src/components/content-hub/creative-grid.tsx +329 -0
- package/client/src/components/content-hub/creative-selector.tsx +415 -0
- package/client/src/components/content-hub/creative-upload.tsx +547 -0
- package/client/src/components/content-hub/folder-dialogs.tsx +445 -0
- package/client/src/components/content-hub/folder-list.tsx +280 -0
- package/client/src/components/content-hub/review-dialogs.tsx +268 -0
- package/client/src/components/content-hub/transcode-dialog.tsx +226 -0
- package/client/src/components/creative-library/creative-details-view.tsx +446 -0
- package/client/src/components/creative-library/creative-filters-panel.tsx +203 -0
- package/client/src/components/creative-library/creative-list.tsx +360 -0
- package/client/src/components/creative-library/creative-status-badge.tsx +71 -0
- package/client/src/components/creative-library/folder-card.tsx +78 -0
- package/client/src/components/creative-library/index.ts +27 -0
- package/client/src/components/creative-library/new-creative-card.tsx +211 -0
- package/client/src/components/creative-library/upload-creative-dialog.tsx +261 -0
- package/client/src/components/dashboard-overview.tsx +109 -0
- package/client/src/components/deals/approval-history-panel.test.tsx +240 -0
- package/client/src/components/deals/approval-history-panel.tsx +156 -0
- package/client/src/components/deals/deal-status-badge.tsx +92 -0
- package/client/src/components/deals/import-from-planner-dialog.tsx +399 -0
- package/client/src/components/deals/market-insights-panel.tsx +237 -0
- package/client/src/components/deals/reopen-deal-sheet.tsx +191 -0
- package/client/src/components/deals/request-approval-sheet.test.tsx +323 -0
- package/client/src/components/deals/request-approval-sheet.tsx +136 -0
- package/client/src/components/deals/resend-approval-sheet.tsx +201 -0
- package/client/src/components/direct-campaigns/campaign-card.tsx +283 -0
- package/client/src/components/direct-campaigns/deal-filter-panel.tsx +325 -0
- package/client/src/components/inventory/advanced-filters-panel.tsx +273 -0
- package/client/src/components/inventory/csv-upload-modal.tsx +639 -0
- package/client/src/components/inventory/inventory-availability-view.tsx +486 -0
- package/client/src/components/inventory/inventory-details-sheet.tsx +376 -0
- package/client/src/components/inventory/inventory-map-view.tsx +596 -0
- package/client/src/components/inventory/inventory-settings-menu.tsx +52 -0
- package/client/src/components/language-switcher.tsx +53 -0
- package/client/src/components/line-items/campaign-forecast-panel.tsx +138 -0
- package/client/src/components/line-items/form-insights.tsx +89 -0
- package/client/src/components/line-items/geofencing/LocationCsvUploadDrawer.tsx +100 -0
- package/client/src/components/line-items/geofencing/POIDropdown.tsx +379 -0
- package/client/src/components/line-items/geofencing/SelectedLocationsSidebar.tsx +436 -0
- package/client/src/components/line-items/geofencing/ViewFileLocationDrawer.tsx +199 -0
- package/client/src/components/line-items/geofencing/components/ExistingFilesTab.tsx +268 -0
- package/client/src/components/line-items/geofencing/components/TemplateDownloadSection.tsx +59 -0
- package/client/src/components/line-items/geofencing/components/UploadTab.tsx +215 -0
- package/client/src/components/line-items/geofencing-map.tsx +1270 -0
- package/client/src/components/line-items/inventory-availability-section.tsx +178 -0
- package/client/src/components/line-items/line-item-schedule-manager.tsx +313 -0
- package/client/src/components/line-items/manual-inventory-drawer.tsx +346 -0
- package/client/src/components/line-items/planner-inventory-card.tsx +495 -0
- package/client/src/components/line-items/planner-schedule-grid.tsx +495 -0
- package/client/src/components/line-items/schedule-rule-editor.tsx +649 -0
- package/client/src/components/line-items/schedule-rule-types.ts +122 -0
- package/client/src/components/line-items/steps/creatives-step.tsx +681 -0
- package/client/src/components/line-items/steps/inventory-schedule-step.tsx +1596 -0
- package/client/src/components/line-items/steps/inventory-step.tsx +1533 -0
- package/client/src/components/line-items/steps/line-item-details-step.tsx +916 -0
- package/client/src/components/line-items/steps/schedule-step.tsx +273 -0
- package/client/src/components/line-items/steps/summary-step.tsx +680 -0
- package/client/src/components/line-items/steps/targeting-step.tsx +1708 -0
- package/client/src/components/product-switcher.tsx +105 -0
- package/client/src/components/protected-route.tsx +49 -0
- package/client/src/components/skip-link.tsx +22 -0
- package/client/src/components/stat-card.tsx +53 -0
- package/client/src/components/status-badge.tsx +96 -0
- package/client/src/components/ui/hierarchical-venue-selector.tsx +389 -0
- package/client/src/components/ui/toaster.tsx +111 -0
- package/client/src/contexts/auth-context.tsx +181 -0
- package/client/src/contexts/sidebar-state.tsx +50 -0
- package/client/src/contexts/theme-context.tsx +66 -0
- package/client/src/data/campaign-data.json +107 -0
- package/client/src/data/countries.json +22 -0
- package/client/src/hooks/use-approval.ts +366 -0
- package/client/src/hooks/use-keyboard-shortcuts.ts +74 -0
- package/client/src/hooks/use-media-query.ts +46 -0
- package/client/src/hooks/use-mobile.tsx +19 -0
- package/client/src/hooks/use-page-title.ts +21 -0
- package/client/src/hooks/use-toast.ts +195 -0
- package/client/src/index.css +694 -0
- package/client/src/lib/__tests__/accessibility.test.ts +104 -0
- package/client/src/lib/__tests__/date-utils.test.ts +199 -0
- package/client/src/lib/__tests__/dsp-buyer-api.test.ts +127 -0
- package/client/src/lib/__tests__/dsp-buyer-integration.test.ts +247 -0
- package/client/src/lib/__tests__/storage-utils.test.ts +167 -0
- package/client/src/lib/__tests__/utils.test.ts +57 -0
- package/client/src/lib/accessibility.ts +141 -0
- package/client/src/lib/api-config.ts +9 -0
- package/client/src/lib/auth-service.ts +209 -0
- package/client/src/lib/campaign-creative-api.ts +82 -0
- package/client/src/lib/company-api.ts +61 -0
- package/client/src/lib/content-hub-api.ts +407 -0
- package/client/src/lib/creative-mapper.ts +61 -0
- package/client/src/lib/date-utils.ts +119 -0
- package/client/src/lib/deal-helpers.ts +220 -0
- package/client/src/lib/dsp-buyer-api.ts +196 -0
- package/client/src/lib/geo-import-api.ts +151 -0
- package/client/src/lib/google-poi-api.ts +305 -0
- package/client/src/lib/i18n/__tests__/formatting.test.ts +202 -0
- package/client/src/lib/i18n/formatting.ts +130 -0
- package/client/src/lib/i18n/index.ts +8 -0
- package/client/src/lib/i18n-compat.ts +76 -0
- package/client/src/lib/influence-deals-api.ts +896 -0
- package/client/src/lib/inventory-api.ts +399 -0
- package/client/src/lib/oauth-service.ts +678 -0
- package/client/src/lib/poi-types.ts +75 -0
- package/client/src/lib/queryClient.ts +144 -0
- package/client/src/lib/recommendation-api.ts +380 -0
- package/client/src/lib/storage-utils.ts +104 -0
- package/client/src/lib/tolgee.ts +85 -0
- package/client/src/lib/utils.ts +0 -0
- package/client/src/main.tsx +67 -0
- package/client/src/mapbox-draw-modes.d.ts +32 -0
- package/client/src/pages/all-folders.tsx +203 -0
- package/client/src/pages/auth-callback.tsx +115 -0
- package/client/src/pages/buyer-form.tsx +339 -0
- package/client/src/pages/buyer-list.tsx +622 -0
- package/client/src/pages/content-hub.tsx +1358 -0
- package/client/src/pages/create-deal.tsx +2093 -0
- package/client/src/pages/creative-assignment-page.tsx +548 -0
- package/client/src/pages/creatives.tsx +5 -0
- package/client/src/pages/custom-pois.tsx +425 -0
- package/client/src/pages/dashboard.tsx +615 -0
- package/client/src/pages/deal-history.tsx +434 -0
- package/client/src/pages/deal-line-items.tsx +1703 -0
- package/client/src/pages/demo-status.tsx +113 -0
- package/client/src/pages/direct-campaign-details.tsx +361 -0
- package/client/src/pages/direct-campaigns-new.tsx +824 -0
- package/client/src/pages/dsp-form.tsx +803 -0
- package/client/src/pages/dsp-list.tsx +239 -0
- package/client/src/pages/folder-content.tsx +336 -0
- package/client/src/pages/integrations.tsx +429 -0
- package/client/src/pages/line-item-creatives.tsx +789 -0
- package/client/src/pages/line-item-detail-page.tsx +684 -0
- package/client/src/pages/line-item-form-page.tsx +3261 -0
- package/client/src/pages/line-item-wizard.tsx +1207 -0
- package/client/src/pages/login.tsx +154 -0
- package/client/src/pages/not-found.tsx +23 -0
- package/client/src/pages/proof-of-play.tsx +397 -0
- package/client/src/pages/public-approval.tsx +551 -0
- package/client/src/pages/reports.tsx +231 -0
- package/client/src/pages/settings.tsx +760 -0
- package/client/src/pages/signals.tsx +389 -0
- package/client/src/pages/tags.tsx +318 -0
- package/client/src/pages/test-results.tsx +328 -0
- package/client/src/store/hooks.ts +5 -0
- package/client/src/store/index.ts +15 -0
- package/client/src/store/mapMarkerLocationsSlice.ts +241 -0
- package/client/src/styles/design-tokens.css +324 -0
- package/client/src/test/setup.ts +261 -0
- package/client/src/test/test-utils.tsx +40 -0
- package/client/src/types/approval.ts +221 -0
- package/client/src/types/content-hub.ts +209 -0
- package/client/src/types/geofencing.ts +67 -0
- package/client/src/types/transcoding.ts +140 -0
- package/client/src/vite-env.d.ts +18 -0
- package/components.json +20 -0
- package/creative-api.json +1 -0
- package/docs/AI_REFERENCE.md +459 -0
- package/docs/MWDesign-Prompt.md +132 -0
- package/docs/MWDesign-System.md +344 -0
- package/docs/test-plan.md +277 -0
- package/e2e/AUTONOMOUS-TESTING.md +406 -0
- package/e2e/README.md +219 -0
- package/e2e/autonomous-flow.spec.ts +308 -0
- package/e2e/debug-sso.spec.ts +163 -0
- package/e2e/direct-campaigns.spec.ts +219 -0
- package/e2e/explore-sso.spec.ts +149 -0
- package/e2e/fixtures/auth.ts +26 -0
- package/e2e/fixtures/enhanced-test.ts +331 -0
- package/e2e/pagination.spec.ts +280 -0
- package/e2e/view-toggle.spec.ts +312 -0
- package/generated-icon.png +0 -0
- package/i18next-scanner.config.cjs +46 -0
- package/package.json +141 -0
- package/playwright.config.ts +93 -0
- package/postcss.config.js +6 -0
- package/replit.md +196 -0
- package/screenshot-after-login.png +0 -0
- package/screenshot-contenthub-grid.png +0 -0
- package/screenshot-contenthub-list-fixed.png +0 -0
- package/screenshot-contenthub-list.png +0 -0
- package/screenshot-create-deal.png +0 -0
- package/screenshot-dashboard.png +0 -0
- package/screenshot-deals.png +0 -0
- package/screenshot-login-filled.png +0 -0
- package/screenshot-login.png +0 -0
- package/screenshot.mjs +24 -0
- package/scripts/deploy-stg.sh +185 -0
- package/shared/direct-io-schema.ts +383 -0
- package/shared/schema.ts +439 -0
- package/shared/screen-types.ts +149 -0
- package/springdocDefault.json +1 -0
- package/swagger-ui-bundle.js +2 -0
- package/swagger-ui-init.js +10316 -0
- package/tailwind.config.ts +282 -0
- package/terraform/README.md +306 -0
- package/terraform/cloudfront.tf +289 -0
- package/terraform/ecs.tf +727 -0
- package/terraform/environments/dev.tfvars +59 -0
- package/terraform/environments/production.tfvars +60 -0
- package/terraform/main.tf +47 -0
- package/terraform/outputs.tf +145 -0
- package/terraform/s3.tf +192 -0
- package/terraform/variables.tf +226 -0
- package/terraform/waf.tf +165 -0
- package/terraform-frontend/.terraform.lock.hcl +25 -0
- package/terraform-frontend/README.md +85 -0
- package/terraform-frontend/cloudfront.tf +125 -0
- package/terraform-frontend/main.tf +31 -0
- package/terraform-frontend/outputs.tf +24 -0
- package/terraform-frontend/terraform.tfvars +12 -0
- package/terraform-frontend/variables.tf +53 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +226 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,2058 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type User,
|
|
3
|
+
type InsertUser,
|
|
4
|
+
type Advertiser,
|
|
5
|
+
type InsertAdvertiser,
|
|
6
|
+
type Agency,
|
|
7
|
+
type InsertAgency,
|
|
8
|
+
type Brand,
|
|
9
|
+
type InsertBrand,
|
|
10
|
+
type MediaOwner,
|
|
11
|
+
type InsertMediaOwner,
|
|
12
|
+
type Venue,
|
|
13
|
+
type InsertVenue,
|
|
14
|
+
type Screen,
|
|
15
|
+
type InsertScreen,
|
|
16
|
+
type Deal,
|
|
17
|
+
type InsertDeal,
|
|
18
|
+
type LineItem,
|
|
19
|
+
type InsertLineItem,
|
|
20
|
+
type SspPartner,
|
|
21
|
+
type InsertSspPartner,
|
|
22
|
+
type DspPartner,
|
|
23
|
+
type InsertDspPartner,
|
|
24
|
+
type DashboardMetrics,
|
|
25
|
+
type Creative,
|
|
26
|
+
type InsertCreative,
|
|
27
|
+
type CreativeFolder,
|
|
28
|
+
type InsertCreativeFolder,
|
|
29
|
+
type LineItemCreative,
|
|
30
|
+
type InsertLineItemCreative,
|
|
31
|
+
type ChangeLog,
|
|
32
|
+
type InsertChangeLog,
|
|
33
|
+
type ChangeLogEntityType,
|
|
34
|
+
type Signal,
|
|
35
|
+
type InsertSignal,
|
|
36
|
+
type SignalRule,
|
|
37
|
+
type InsertSignalRule,
|
|
38
|
+
type POI,
|
|
39
|
+
type PlayerStatus,
|
|
40
|
+
type InsertPlayerStatus,
|
|
41
|
+
type ProofOfPlay,
|
|
42
|
+
type InsertProofOfPlay,
|
|
43
|
+
type PlaylogUpload,
|
|
44
|
+
type InsertPlaylogUpload,
|
|
45
|
+
type GeoTargetingRule,
|
|
46
|
+
type InsertGeoTargetingRule,
|
|
47
|
+
type TransitRoute,
|
|
48
|
+
type InsertTransitRoute,
|
|
49
|
+
type TransitZone,
|
|
50
|
+
type InsertTransitZone,
|
|
51
|
+
} from "@shared/schema";
|
|
52
|
+
import { randomUUID } from "crypto";
|
|
53
|
+
|
|
54
|
+
export interface IStorage {
|
|
55
|
+
// Users
|
|
56
|
+
getUser(id: string): Promise<User | undefined>;
|
|
57
|
+
getUserByUsername(username: string): Promise<User | undefined>;
|
|
58
|
+
createUser(user: InsertUser): Promise<User>;
|
|
59
|
+
|
|
60
|
+
// Advertisers
|
|
61
|
+
getAdvertisers(): Promise<Advertiser[]>;
|
|
62
|
+
getAdvertiser(id: string): Promise<Advertiser | undefined>;
|
|
63
|
+
createAdvertiser(advertiser: InsertAdvertiser): Promise<Advertiser>;
|
|
64
|
+
updateAdvertiser(id: string, data: Partial<InsertAdvertiser>): Promise<Advertiser | undefined>;
|
|
65
|
+
deleteAdvertiser(id: string): Promise<boolean>;
|
|
66
|
+
|
|
67
|
+
// Agencies
|
|
68
|
+
getAgencies(): Promise<Agency[]>;
|
|
69
|
+
getAgency(id: string): Promise<Agency | undefined>;
|
|
70
|
+
createAgency(agency: InsertAgency): Promise<Agency>;
|
|
71
|
+
updateAgency(id: string, data: Partial<InsertAgency>): Promise<Agency | undefined>;
|
|
72
|
+
deleteAgency(id: string): Promise<boolean>;
|
|
73
|
+
|
|
74
|
+
// Brands
|
|
75
|
+
getBrands(): Promise<Brand[]>;
|
|
76
|
+
getBrand(id: string): Promise<Brand | undefined>;
|
|
77
|
+
createBrand(brand: InsertBrand): Promise<Brand>;
|
|
78
|
+
updateBrand(id: string, data: Partial<InsertBrand>): Promise<Brand | undefined>;
|
|
79
|
+
deleteBrand(id: string): Promise<boolean>;
|
|
80
|
+
|
|
81
|
+
// Media Owners
|
|
82
|
+
getMediaOwners(): Promise<MediaOwner[]>;
|
|
83
|
+
getMediaOwner(id: string): Promise<MediaOwner | undefined>;
|
|
84
|
+
createMediaOwner(mediaOwner: InsertMediaOwner): Promise<MediaOwner>;
|
|
85
|
+
updateMediaOwner(id: string, data: Partial<InsertMediaOwner>): Promise<MediaOwner | undefined>;
|
|
86
|
+
deleteMediaOwner(id: string): Promise<boolean>;
|
|
87
|
+
|
|
88
|
+
// Venues
|
|
89
|
+
getVenues(): Promise<Venue[]>;
|
|
90
|
+
getVenue(id: string): Promise<Venue | undefined>;
|
|
91
|
+
createVenue(venue: InsertVenue): Promise<Venue>;
|
|
92
|
+
updateVenue(id: string, data: Partial<InsertVenue>): Promise<Venue | undefined>;
|
|
93
|
+
deleteVenue(id: string): Promise<boolean>;
|
|
94
|
+
|
|
95
|
+
// Screens
|
|
96
|
+
getScreens(): Promise<Screen[]>;
|
|
97
|
+
getScreen(id: string): Promise<Screen | undefined>;
|
|
98
|
+
createScreen(screen: InsertScreen): Promise<Screen>;
|
|
99
|
+
updateScreen(id: string, data: Partial<InsertScreen>): Promise<Screen | undefined>;
|
|
100
|
+
deleteScreen(id: string): Promise<boolean>;
|
|
101
|
+
|
|
102
|
+
// Deals
|
|
103
|
+
getDeals(): Promise<Deal[]>;
|
|
104
|
+
getDeal(id: string): Promise<Deal | undefined>;
|
|
105
|
+
createDeal(deal: InsertDeal): Promise<Deal>;
|
|
106
|
+
updateDeal(id: string, data: Partial<InsertDeal>): Promise<Deal | undefined>;
|
|
107
|
+
deleteDeal(id: string): Promise<boolean>;
|
|
108
|
+
|
|
109
|
+
// Line Items (now directly under Deals - 4-level hierarchy)
|
|
110
|
+
getLineItems(): Promise<LineItem[]>;
|
|
111
|
+
getLineItem(id: string): Promise<LineItem | undefined>;
|
|
112
|
+
getLineItemsByDeal(dealId: string): Promise<LineItem[]>;
|
|
113
|
+
createLineItem(item: InsertLineItem): Promise<LineItem>;
|
|
114
|
+
updateLineItem(id: string, data: Partial<InsertLineItem>): Promise<LineItem | undefined>;
|
|
115
|
+
deleteLineItem(id: string): Promise<boolean>;
|
|
116
|
+
|
|
117
|
+
// SSP Partners
|
|
118
|
+
getSspPartners(): Promise<SspPartner[]>;
|
|
119
|
+
getSspPartner(id: string): Promise<SspPartner | undefined>;
|
|
120
|
+
createSspPartner(partner: InsertSspPartner): Promise<SspPartner>;
|
|
121
|
+
updateSspPartner(id: string, data: Partial<InsertSspPartner>): Promise<SspPartner | undefined>;
|
|
122
|
+
deleteSspPartner(id: string): Promise<boolean>;
|
|
123
|
+
|
|
124
|
+
// DSP Partners
|
|
125
|
+
getDspPartners(): Promise<DspPartner[]>;
|
|
126
|
+
getDspPartner(id: string): Promise<DspPartner | undefined>;
|
|
127
|
+
createDspPartner(partner: InsertDspPartner): Promise<DspPartner>;
|
|
128
|
+
updateDspPartner(id: string, data: Partial<InsertDspPartner>): Promise<DspPartner | undefined>;
|
|
129
|
+
deleteDspPartner(id: string): Promise<boolean>;
|
|
130
|
+
|
|
131
|
+
// Dashboard
|
|
132
|
+
getDashboardMetrics(): Promise<DashboardMetrics>;
|
|
133
|
+
|
|
134
|
+
// Creatives
|
|
135
|
+
getCreatives(filters?: { folderId?: string; type?: string; status?: string; search?: string }): Promise<Creative[]>;
|
|
136
|
+
getCreative(id: string): Promise<Creative | undefined>;
|
|
137
|
+
createCreative(creative: InsertCreative): Promise<Creative>;
|
|
138
|
+
updateCreative(id: string, data: Partial<InsertCreative>): Promise<Creative | undefined>;
|
|
139
|
+
deleteCreative(id: string): Promise<boolean>;
|
|
140
|
+
|
|
141
|
+
// Creative Folders
|
|
142
|
+
getCreativeFolders(): Promise<CreativeFolder[]>;
|
|
143
|
+
getCreativeFolder(id: string): Promise<CreativeFolder | undefined>;
|
|
144
|
+
createCreativeFolder(folder: InsertCreativeFolder): Promise<CreativeFolder>;
|
|
145
|
+
deleteCreativeFolder(id: string): Promise<boolean>;
|
|
146
|
+
|
|
147
|
+
// Line Item Creative Assignments
|
|
148
|
+
getAllLineItemCreatives(): Promise<LineItemCreative[]>;
|
|
149
|
+
getLineItemCreatives(lineItemId: string): Promise<LineItemCreative[]>;
|
|
150
|
+
getLineItemCreative(id: string): Promise<LineItemCreative | undefined>;
|
|
151
|
+
getLineItemCreativeById(id: string): Promise<(LineItemCreative & { creative: Creative }) | undefined>;
|
|
152
|
+
getLineItemCreativeWithDetails(lineItemId: string): Promise<(LineItemCreative & { creative: Creative })[]>;
|
|
153
|
+
assignCreativeToLineItem(assignment: InsertLineItemCreative): Promise<LineItemCreative>;
|
|
154
|
+
removeCreativeFromLineItem(lineItemId: string, creativeId: string): Promise<boolean>;
|
|
155
|
+
updateLineItemCreative(id: string, data: Partial<InsertLineItemCreative>): Promise<LineItemCreative | undefined>;
|
|
156
|
+
getAssignableCreatives(): Promise<Creative[]>;
|
|
157
|
+
|
|
158
|
+
// Change Logs (Audit Trail)
|
|
159
|
+
getChangeLogs(entityType: ChangeLogEntityType, entityId: string): Promise<ChangeLog[]>;
|
|
160
|
+
getHierarchicalChangeLogs(entityType: ChangeLogEntityType, entityId: string): Promise<ChangeLog[]>;
|
|
161
|
+
createChangeLog(log: InsertChangeLog): Promise<ChangeLog>;
|
|
162
|
+
getRecentChangeLogs(limit?: number): Promise<ChangeLog[]>;
|
|
163
|
+
|
|
164
|
+
// Traffic Allocation (single-level - directly on line items within a deal)
|
|
165
|
+
getDealTrafficSummary(dealId: string): Promise<{
|
|
166
|
+
dealId: string;
|
|
167
|
+
totalLineItems: number;
|
|
168
|
+
totalAllocation: number;
|
|
169
|
+
normalizedAllocations: { lineItemId: string; lineItemName: string; allocation: number; priority: number; normalizedShare: number }[];
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
// Signals
|
|
173
|
+
getSignals(): Promise<Signal[]>;
|
|
174
|
+
getSignal(id: string): Promise<Signal | undefined>;
|
|
175
|
+
createSignal(data: InsertSignal): Promise<Signal>;
|
|
176
|
+
updateSignal(id: string, data: Partial<InsertSignal>): Promise<Signal | undefined>;
|
|
177
|
+
deleteSignal(id: string): Promise<boolean>;
|
|
178
|
+
|
|
179
|
+
// Signal Rules
|
|
180
|
+
getSignalRules(signalId: string): Promise<SignalRule[]>;
|
|
181
|
+
getSignalRule(id: string): Promise<SignalRule | undefined>;
|
|
182
|
+
createSignalRule(data: InsertSignalRule): Promise<SignalRule>;
|
|
183
|
+
updateSignalRule(id: string, data: Partial<InsertSignalRule>): Promise<SignalRule | undefined>;
|
|
184
|
+
deleteSignalRule(id: string): Promise<boolean>;
|
|
185
|
+
|
|
186
|
+
// POIs
|
|
187
|
+
getPOIs(country?: string): Promise<POI[]>;
|
|
188
|
+
getPOI(id: string): Promise<POI | undefined>;
|
|
189
|
+
|
|
190
|
+
// Player Status (read-only from CMS)
|
|
191
|
+
getPlayerStatuses(screenIds?: string[]): Promise<PlayerStatus[]>;
|
|
192
|
+
getPlayerStatus(screenId: string): Promise<PlayerStatus | undefined>;
|
|
193
|
+
upsertPlayerStatus(data: InsertPlayerStatus): Promise<PlayerStatus>;
|
|
194
|
+
|
|
195
|
+
// Proof of Play
|
|
196
|
+
getProofOfPlayRecords(filters?: {
|
|
197
|
+
dealId?: string;
|
|
198
|
+
lineItemId?: string;
|
|
199
|
+
creativeId?: string;
|
|
200
|
+
mediaOwnerId?: string;
|
|
201
|
+
source?: string;
|
|
202
|
+
status?: string;
|
|
203
|
+
startDate?: string;
|
|
204
|
+
endDate?: string;
|
|
205
|
+
}): Promise<ProofOfPlay[]>;
|
|
206
|
+
getProofOfPlayRecord(id: string): Promise<ProofOfPlay | undefined>;
|
|
207
|
+
createProofOfPlayRecord(data: InsertProofOfPlay): Promise<ProofOfPlay>;
|
|
208
|
+
updateProofOfPlayRecord(id: string, data: Partial<InsertProofOfPlay>): Promise<ProofOfPlay | undefined>;
|
|
209
|
+
deleteProofOfPlayRecord(id: string): Promise<boolean>;
|
|
210
|
+
|
|
211
|
+
// Playlog Uploads
|
|
212
|
+
getPlaylogUploads(filters?: {
|
|
213
|
+
dealId?: string;
|
|
214
|
+
lineItemId?: string;
|
|
215
|
+
mediaOwnerId?: string;
|
|
216
|
+
status?: string;
|
|
217
|
+
}): Promise<PlaylogUpload[]>;
|
|
218
|
+
getPlaylogUpload(id: string): Promise<PlaylogUpload | undefined>;
|
|
219
|
+
createPlaylogUpload(data: InsertPlaylogUpload): Promise<PlaylogUpload>;
|
|
220
|
+
updatePlaylogUpload(id: string, data: Partial<InsertPlaylogUpload>): Promise<PlaylogUpload | undefined>;
|
|
221
|
+
deletePlaylogUpload(id: string): Promise<boolean>;
|
|
222
|
+
|
|
223
|
+
// Geotargeting
|
|
224
|
+
getGeoTargetingRules(entityType: string, entityId: string): Promise<GeoTargetingRule[]>;
|
|
225
|
+
getGeoTargetingRule(id: string): Promise<GeoTargetingRule | undefined>;
|
|
226
|
+
createGeoTargetingRule(data: InsertGeoTargetingRule): Promise<GeoTargetingRule>;
|
|
227
|
+
updateGeoTargetingRule(id: string, data: Partial<InsertGeoTargetingRule>): Promise<GeoTargetingRule | undefined>;
|
|
228
|
+
deleteGeoTargetingRule(id: string): Promise<boolean>;
|
|
229
|
+
|
|
230
|
+
// Transit Routes and Zones
|
|
231
|
+
getTransitRoutes(city?: string): Promise<TransitRoute[]>;
|
|
232
|
+
getTransitRoute(id: string): Promise<TransitRoute | undefined>;
|
|
233
|
+
getTransitZones(city?: string): Promise<TransitZone[]>;
|
|
234
|
+
getTransitZone(id: string): Promise<TransitZone | undefined>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export class MemStorage implements IStorage {
|
|
238
|
+
private users: Map<string, User>;
|
|
239
|
+
private advertisers: Map<string, Advertiser>;
|
|
240
|
+
private agencies: Map<string, Agency>;
|
|
241
|
+
private brands: Map<string, Brand>;
|
|
242
|
+
private mediaOwners: Map<string, MediaOwner>;
|
|
243
|
+
private venues: Map<string, Venue>;
|
|
244
|
+
private screens: Map<string, Screen>;
|
|
245
|
+
private deals: Map<string, Deal>;
|
|
246
|
+
private lineItems: Map<string, LineItem>;
|
|
247
|
+
private sspPartners: Map<string, SspPartner>;
|
|
248
|
+
private dspPartners: Map<string, DspPartner>;
|
|
249
|
+
private creatives: Map<string, Creative>;
|
|
250
|
+
private creativeFolders: Map<string, CreativeFolder>;
|
|
251
|
+
private lineItemCreatives: Map<string, LineItemCreative>;
|
|
252
|
+
private changeLogs: Map<string, ChangeLog>;
|
|
253
|
+
private signals: Map<string, Signal>;
|
|
254
|
+
private signalRules: Map<string, SignalRule>;
|
|
255
|
+
private pois: Map<string, POI>;
|
|
256
|
+
private playerStatuses: Map<string, PlayerStatus>;
|
|
257
|
+
private proofOfPlayRecords: Map<string, ProofOfPlay>;
|
|
258
|
+
private playlogUploads: Map<string, PlaylogUpload>;
|
|
259
|
+
private geoTargetingRules: Map<string, GeoTargetingRule>;
|
|
260
|
+
private transitRoutes: Map<string, TransitRoute>;
|
|
261
|
+
private transitZones: Map<string, TransitZone>;
|
|
262
|
+
|
|
263
|
+
constructor() {
|
|
264
|
+
this.users = new Map();
|
|
265
|
+
this.advertisers = new Map();
|
|
266
|
+
this.agencies = new Map();
|
|
267
|
+
this.brands = new Map();
|
|
268
|
+
this.mediaOwners = new Map();
|
|
269
|
+
this.venues = new Map();
|
|
270
|
+
this.screens = new Map();
|
|
271
|
+
this.deals = new Map();
|
|
272
|
+
this.lineItems = new Map();
|
|
273
|
+
this.sspPartners = new Map();
|
|
274
|
+
this.dspPartners = new Map();
|
|
275
|
+
this.creatives = new Map();
|
|
276
|
+
this.creativeFolders = new Map();
|
|
277
|
+
this.lineItemCreatives = new Map();
|
|
278
|
+
this.changeLogs = new Map();
|
|
279
|
+
this.signals = new Map();
|
|
280
|
+
this.signalRules = new Map();
|
|
281
|
+
this.pois = new Map();
|
|
282
|
+
this.playerStatuses = new Map();
|
|
283
|
+
this.proofOfPlayRecords = new Map();
|
|
284
|
+
this.playlogUploads = new Map();
|
|
285
|
+
this.geoTargetingRules = new Map();
|
|
286
|
+
this.transitRoutes = new Map();
|
|
287
|
+
this.transitZones = new Map();
|
|
288
|
+
|
|
289
|
+
this.seedData();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private seedData() {
|
|
293
|
+
// Seed SSP partners - Influence is the default/built-in SSP with fixed ID
|
|
294
|
+
const sspInfluence = { id: "ssp-influence-default", name: "Influence", endpoint: "https://api.influence.internal/v1", apiKey: "influence_internal", status: "active", partnerType: "ssp" };
|
|
295
|
+
const ssp1 = { id: randomUUID(), name: "Hivestack", endpoint: "https://api.hivestack.com/v1", apiKey: "hs_xxxxx", status: "active", partnerType: "ssp" };
|
|
296
|
+
const ssp2 = { id: randomUUID(), name: "Vistar", endpoint: "https://api.vistarmedia.com/v2", apiKey: "vm_xxxxx", status: "active", partnerType: "ssp" };
|
|
297
|
+
const ssp3 = { id: randomUUID(), name: "Place Exchange", endpoint: "https://api.placeexchange.com/v1", apiKey: "pe_xxxxx", status: "active", partnerType: "ssp" };
|
|
298
|
+
const ssp4 = { id: randomUUID(), name: "VIOOH", endpoint: "https://api.viooh.com/v1", apiKey: "viooh_xxxxx", status: "active", partnerType: "ssp" };
|
|
299
|
+
const ssp5 = { id: randomUUID(), name: "Broadsign", endpoint: "https://api.broadsign.com/v1", apiKey: "bs_xxxxx", status: "active", partnerType: "ssp" };
|
|
300
|
+
const ssp6 = { id: randomUUID(), name: "Magnite", endpoint: "https://api.magnite.com/v1", apiKey: "mag_xxxxx", status: "active", partnerType: "ssp" };
|
|
301
|
+
this.sspPartners.set(sspInfluence.id, sspInfluence as SspPartner);
|
|
302
|
+
this.sspPartners.set(ssp1.id, ssp1 as SspPartner);
|
|
303
|
+
this.sspPartners.set(ssp2.id, ssp2 as SspPartner);
|
|
304
|
+
this.sspPartners.set(ssp3.id, ssp3 as SspPartner);
|
|
305
|
+
this.sspPartners.set(ssp4.id, ssp4 as SspPartner);
|
|
306
|
+
this.sspPartners.set(ssp5.id, ssp5 as SspPartner);
|
|
307
|
+
this.sspPartners.set(ssp6.id, ssp6 as SspPartner);
|
|
308
|
+
|
|
309
|
+
// Seed DSP partners - Activate is the default DSP with fixed ID
|
|
310
|
+
const dspActivate = { id: "dsp-activate-default", name: "Activate", endpoint: "https://api.activate.com/v1", apiKey: "act_xxxxx", status: "active" };
|
|
311
|
+
const dsp1 = { id: randomUUID(), name: "The Trade Desk", endpoint: "https://api.thetradedesk.com/v3", apiKey: "ttd_xxxxx", status: "active" };
|
|
312
|
+
const dsp2 = { id: randomUUID(), name: "DV360", endpoint: "https://api.displayvideo360.google.com/v1", apiKey: "dv360_xxxxx", status: "active" };
|
|
313
|
+
const dsp3 = { id: randomUUID(), name: "Xandr", endpoint: "https://api.xandr.com/v1", apiKey: "xandr_xxxxx", status: "active" };
|
|
314
|
+
const dsp4 = { id: randomUUID(), name: "Yahoo DSP", endpoint: "https://api.dsp.yahoo.com/v1", apiKey: "yahoo_xxxxx", status: "active" };
|
|
315
|
+
const dsp5 = { id: randomUUID(), name: "Cassie", endpoint: "https://api.cassie.com/v1", apiKey: "cassie_xxxxx", status: "pending" };
|
|
316
|
+
this.dspPartners.set(dspActivate.id, dspActivate as DspPartner);
|
|
317
|
+
this.dspPartners.set(dsp1.id, dsp1 as DspPartner);
|
|
318
|
+
this.dspPartners.set(dsp2.id, dsp2 as DspPartner);
|
|
319
|
+
this.dspPartners.set(dsp3.id, dsp3 as DspPartner);
|
|
320
|
+
this.dspPartners.set(dsp4.id, dsp4 as DspPartner);
|
|
321
|
+
this.dspPartners.set(dsp5.id, dsp5 as DspPartner);
|
|
322
|
+
|
|
323
|
+
// Seed Media Owners (with company hierarchy - parent companies and child companies)
|
|
324
|
+
// Parent companies (media_owner type)
|
|
325
|
+
const mo1 = { id: randomUUID(), name: "JCDecaux", country: "France", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
326
|
+
const mo2 = { id: randomUUID(), name: "Clear Channel Outdoor", country: "USA", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
327
|
+
const mo3 = { id: randomUUID(), name: "Lamar Advertising", country: "USA", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
328
|
+
const mo4 = { id: randomUUID(), name: "Outfront Media", country: "USA", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
329
|
+
const mo5 = { id: randomUUID(), name: "oOh!media", country: "Australia", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
330
|
+
const mo6 = { id: randomUUID(), name: "Global", country: "UK", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
331
|
+
const mo7 = { id: randomUUID(), name: "Ocean Outdoor", country: "UK", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
332
|
+
const mo8 = { id: randomUUID(), name: "Ströer", country: "Germany", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
333
|
+
const mo9 = { id: randomUUID(), name: "APG|SGA", country: "Switzerland", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
334
|
+
const mo10 = { id: randomUUID(), name: "QMS Media", country: "Australia", status: "active", parentId: null, companyType: "media_owner", userId: null };
|
|
335
|
+
|
|
336
|
+
// Child companies (child_company type) - examples of subsidiaries
|
|
337
|
+
const mo11 = { id: randomUUID(), name: "JCDecaux Malaysia", country: "Malaysia", status: "active", parentId: mo1.id, companyType: "child_company", userId: null };
|
|
338
|
+
const mo12 = { id: randomUUID(), name: "JCDecaux Singapore", country: "Singapore", status: "active", parentId: mo1.id, companyType: "child_company", userId: null };
|
|
339
|
+
const mo13 = { id: randomUUID(), name: "Clear Channel Singapore", country: "Singapore", status: "active", parentId: mo2.id, companyType: "child_company", userId: null };
|
|
340
|
+
const mo14 = { id: randomUUID(), name: "oOh!media New Zealand", country: "New Zealand", status: "active", parentId: mo5.id, companyType: "child_company", userId: null };
|
|
341
|
+
const mo15 = { id: randomUUID(), name: "Ströer Austria", country: "Austria", status: "active", parentId: mo8.id, companyType: "child_company", userId: null };
|
|
342
|
+
this.mediaOwners.set(mo1.id, mo1 as MediaOwner);
|
|
343
|
+
this.mediaOwners.set(mo2.id, mo2 as MediaOwner);
|
|
344
|
+
this.mediaOwners.set(mo3.id, mo3 as MediaOwner);
|
|
345
|
+
this.mediaOwners.set(mo4.id, mo4 as MediaOwner);
|
|
346
|
+
this.mediaOwners.set(mo5.id, mo5 as MediaOwner);
|
|
347
|
+
this.mediaOwners.set(mo6.id, mo6 as MediaOwner);
|
|
348
|
+
this.mediaOwners.set(mo7.id, mo7 as MediaOwner);
|
|
349
|
+
this.mediaOwners.set(mo8.id, mo8 as MediaOwner);
|
|
350
|
+
this.mediaOwners.set(mo9.id, mo9 as MediaOwner);
|
|
351
|
+
this.mediaOwners.set(mo10.id, mo10 as MediaOwner);
|
|
352
|
+
this.mediaOwners.set(mo11.id, mo11 as MediaOwner);
|
|
353
|
+
this.mediaOwners.set(mo12.id, mo12 as MediaOwner);
|
|
354
|
+
this.mediaOwners.set(mo13.id, mo13 as MediaOwner);
|
|
355
|
+
this.mediaOwners.set(mo14.id, mo14 as MediaOwner);
|
|
356
|
+
this.mediaOwners.set(mo15.id, mo15 as MediaOwner);
|
|
357
|
+
|
|
358
|
+
// Seed Agencies (10)
|
|
359
|
+
const agency1 = { id: randomUUID(), name: "GroupM", contactEmail: "contact@groupm.com", country: "US", status: "active" };
|
|
360
|
+
const agency2 = { id: randomUUID(), name: "Omnicom Media Group", contactEmail: "contact@omnicommediagroup.com", country: "US", status: "active" };
|
|
361
|
+
const agency3 = { id: randomUUID(), name: "Publicis Media", contactEmail: "contact@publicismedia.com", country: "FR", status: "active" };
|
|
362
|
+
const agency4 = { id: randomUUID(), name: "Dentsu", contactEmail: "contact@dentsu.com", country: "JP", status: "active" };
|
|
363
|
+
const agency5 = { id: randomUUID(), name: "IPG Mediabrands", contactEmail: "contact@ipgmediabrands.com", country: "US", status: "active" };
|
|
364
|
+
const agency6 = { id: randomUUID(), name: "Havas Media", contactEmail: "contact@havasmedia.com", country: "FR", status: "active" };
|
|
365
|
+
const agency7 = { id: randomUUID(), name: "Starcom", contactEmail: "contact@starcom.com", country: "US", status: "active" };
|
|
366
|
+
const agency8 = { id: randomUUID(), name: "Mindshare", contactEmail: "contact@mindshare.com", country: "GB", status: "active" };
|
|
367
|
+
const agency9 = { id: randomUUID(), name: "Wavemaker", contactEmail: "contact@wavemaker.com", country: "GB", status: "active" };
|
|
368
|
+
const agency10 = { id: randomUUID(), name: "PHD", contactEmail: "contact@phdmedia.com", country: "SG", status: "active" };
|
|
369
|
+
this.agencies.set(agency1.id, agency1 as Agency);
|
|
370
|
+
this.agencies.set(agency2.id, agency2 as Agency);
|
|
371
|
+
this.agencies.set(agency3.id, agency3 as Agency);
|
|
372
|
+
this.agencies.set(agency4.id, agency4 as Agency);
|
|
373
|
+
this.agencies.set(agency5.id, agency5 as Agency);
|
|
374
|
+
this.agencies.set(agency6.id, agency6 as Agency);
|
|
375
|
+
this.agencies.set(agency7.id, agency7 as Agency);
|
|
376
|
+
this.agencies.set(agency8.id, agency8 as Agency);
|
|
377
|
+
this.agencies.set(agency9.id, agency9 as Agency);
|
|
378
|
+
this.agencies.set(agency10.id, agency10 as Agency);
|
|
379
|
+
|
|
380
|
+
// Seed Brands (15 with IAB categories)
|
|
381
|
+
const brand1 = { id: randomUUID(), name: "Coca-Cola", iabCategory: "Food & Drink", status: "active" };
|
|
382
|
+
const brand2 = { id: randomUUID(), name: "Nike", iabCategory: "Style & Fashion", status: "active" };
|
|
383
|
+
const brand3 = { id: randomUUID(), name: "Apple", iabCategory: "Technology & Computing", status: "active" };
|
|
384
|
+
const brand4 = { id: randomUUID(), name: "McDonald's", iabCategory: "Food & Drink", status: "active" };
|
|
385
|
+
const brand5 = { id: randomUUID(), name: "Samsung", iabCategory: "Technology & Computing", status: "active" };
|
|
386
|
+
const brand6 = { id: randomUUID(), name: "Toyota", iabCategory: "Automotive", status: "active" };
|
|
387
|
+
const brand7 = { id: randomUUID(), name: "Unilever", iabCategory: "Shopping", status: "active" };
|
|
388
|
+
const brand8 = { id: randomUUID(), name: "Netflix", iabCategory: "Television", status: "active" };
|
|
389
|
+
const brand9 = { id: randomUUID(), name: "Amazon", iabCategory: "Shopping", status: "active" };
|
|
390
|
+
const brand10 = { id: randomUUID(), name: "Google", iabCategory: "Technology & Computing", status: "active" };
|
|
391
|
+
const brand11 = { id: randomUUID(), name: "Microsoft", iabCategory: "Technology & Computing", status: "active" };
|
|
392
|
+
const brand12 = { id: randomUUID(), name: "BMW", iabCategory: "Automotive", status: "active" };
|
|
393
|
+
const brand13 = { id: randomUUID(), name: "L'Oreal", iabCategory: "Style & Fashion", status: "active" };
|
|
394
|
+
const brand14 = { id: randomUUID(), name: "Pepsi", iabCategory: "Food & Drink", status: "active" };
|
|
395
|
+
const brand15 = { id: randomUUID(), name: "Adidas", iabCategory: "Style & Fashion", status: "active" };
|
|
396
|
+
this.brands.set(brand1.id, brand1 as Brand);
|
|
397
|
+
this.brands.set(brand2.id, brand2 as Brand);
|
|
398
|
+
this.brands.set(brand3.id, brand3 as Brand);
|
|
399
|
+
this.brands.set(brand4.id, brand4 as Brand);
|
|
400
|
+
this.brands.set(brand5.id, brand5 as Brand);
|
|
401
|
+
this.brands.set(brand6.id, brand6 as Brand);
|
|
402
|
+
this.brands.set(brand7.id, brand7 as Brand);
|
|
403
|
+
this.brands.set(brand8.id, brand8 as Brand);
|
|
404
|
+
this.brands.set(brand9.id, brand9 as Brand);
|
|
405
|
+
this.brands.set(brand10.id, brand10 as Brand);
|
|
406
|
+
this.brands.set(brand11.id, brand11 as Brand);
|
|
407
|
+
this.brands.set(brand12.id, brand12 as Brand);
|
|
408
|
+
this.brands.set(brand13.id, brand13 as Brand);
|
|
409
|
+
this.brands.set(brand14.id, brand14 as Brand);
|
|
410
|
+
this.brands.set(brand15.id, brand15 as Brand);
|
|
411
|
+
|
|
412
|
+
// Seed advertisers
|
|
413
|
+
const advertiser1 = { id: randomUUID(), name: "TechCorp Inc.", contactEmail: "media@techcorp.com", contactPhone: "+1-555-0101", status: "active" };
|
|
414
|
+
const advertiser2 = { id: randomUUID(), name: "RetailMax", contactEmail: "ads@retailmax.com", contactPhone: "+1-555-0102", status: "active" };
|
|
415
|
+
const advertiser3 = { id: randomUUID(), name: "AutoDrive Motors", contactEmail: "marketing@autodrive.com", contactPhone: "+1-555-0103", status: "active" };
|
|
416
|
+
this.advertisers.set(advertiser1.id, advertiser1);
|
|
417
|
+
this.advertisers.set(advertiser2.id, advertiser2);
|
|
418
|
+
this.advertisers.set(advertiser3.id, advertiser3);
|
|
419
|
+
|
|
420
|
+
// Seed venues
|
|
421
|
+
const venue1 = { id: randomUUID(), name: "Westfield Mall", address: "123 Shopping Blvd", city: "New York", country: "USA", venueType: "mall", status: "active" };
|
|
422
|
+
const venue2 = { id: randomUUID(), name: "JFK Terminal 4", address: "JFK Airport", city: "New York", country: "USA", venueType: "airport", status: "active" };
|
|
423
|
+
const venue3 = { id: randomUUID(), name: "Grand Central Station", address: "89 E 42nd St", city: "New York", country: "USA", venueType: "transit", status: "active" };
|
|
424
|
+
const venue4 = { id: randomUUID(), name: "Times Square Plaza", address: "Times Square", city: "New York", country: "USA", venueType: "street", status: "active" };
|
|
425
|
+
this.venues.set(venue1.id, venue1);
|
|
426
|
+
this.venues.set(venue2.id, venue2);
|
|
427
|
+
this.venues.set(venue3.id, venue3);
|
|
428
|
+
this.venues.set(venue4.id, venue4);
|
|
429
|
+
|
|
430
|
+
// Seed screens with new fields across Malaysia, Singapore, and Thailand
|
|
431
|
+
const screens = [
|
|
432
|
+
// Malaysia - Kuala Lumpur (some with webcams)
|
|
433
|
+
{ id: randomUUID(), name: "KLCC Suria Mall LED", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 45000, cpm: "18.00", country: "Malaysia", city: "Kuala Lumpur", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "3.1588", longitude: "101.7130", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "L", webcamUrl: "https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_1mb.mp4", webcamStatus: "active" },
|
|
434
|
+
{ id: randomUUID(), name: "Pavilion KL Digital Billboard", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 65000, cpm: "22.00", country: "Malaysia", city: "Kuala Lumpur", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "3.1490", longitude: "101.7133", format: "Digital Billboard", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
435
|
+
{ id: randomUUID(), name: "KL Sentral Transit Screen", venueId: null, screenType: "lcd", width: 1920, height: 1080, orientation: "portrait", status: "active", dailyImpressions: 38000, cpm: "14.00", country: "Malaysia", city: "Kuala Lumpur", mediaOwnerId: mo2.id, sspId: ssp2.id, latitude: "3.1346", longitude: "101.6865", format: "Platform Digital Screen", classification: "Digital", inventoryType: "Transit", sizeCategory: "M", webcamUrl: "https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_2mb.mp4", webcamStatus: "active" },
|
|
436
|
+
{ id: randomUUID(), name: "Menara KLCC Elevator Screen", venueId: null, screenType: "lcd", width: 1080, height: 1920, orientation: "portrait", status: "active", dailyImpressions: 12000, cpm: "11.00", country: "Malaysia", city: "Kuala Lumpur", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "3.1570", longitude: "101.7120", format: "Elevator / Lobby Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "S", webcamUrl: null, webcamStatus: "inactive" },
|
|
437
|
+
{ id: randomUUID(), name: "Mid Valley Megamall Screen", venueId: null, screenType: "led", width: 2560, height: 1440, orientation: "landscape", status: "active", dailyImpressions: 52000, cpm: "16.00", country: "Malaysia", city: "Kuala Lumpur", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "3.1178", longitude: "101.6771", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
438
|
+
// Malaysia - Penang
|
|
439
|
+
{ id: randomUUID(), name: "Gurney Plaza Digital", venueId: null, screenType: "led", width: 1920, height: 1080, orientation: "landscape", status: "active", dailyImpressions: 28000, cpm: "12.00", country: "Malaysia", city: "Penang", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "5.4372", longitude: "100.3105", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
440
|
+
{ id: randomUUID(), name: "Penang International Airport", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 25000, cpm: "20.00", country: "Malaysia", city: "Penang", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "5.2973", longitude: "100.2766", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
441
|
+
// Singapore (some with webcams)
|
|
442
|
+
{ id: randomUUID(), name: "Orchard Road Digital Billboard", venueId: null, screenType: "led", width: 7680, height: 4320, orientation: "landscape", status: "active", dailyImpressions: 120000, cpm: "35.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "1.3048", longitude: "103.8318", format: "Digital Large Format", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: "https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_5mb.mp4", webcamStatus: "active" },
|
|
443
|
+
{ id: randomUUID(), name: "ION Orchard Mall Screen", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 85000, cpm: "28.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "1.3039", longitude: "103.8318", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
444
|
+
{ id: randomUUID(), name: "Changi Airport T3 Screen", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 95000, cpm: "32.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "1.3644", longitude: "103.9915", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "XL", webcamUrl: "https://sample-videos.com/video321/mp4/720/big_buck_bunny_720p_10mb.mp4", webcamStatus: "active" },
|
|
445
|
+
{ id: randomUUID(), name: "Marina Bay MRT Station", venueId: null, screenType: "lcd", width: 1920, height: 1080, orientation: "portrait", status: "active", dailyImpressions: 42000, cpm: "18.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo2.id, sspId: ssp2.id, latitude: "1.2764", longitude: "103.8549", format: "Metro Station Screen", classification: "Digital", inventoryType: "Transit", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
446
|
+
{ id: randomUUID(), name: "VivoCity Digital Kiosk", venueId: null, screenType: "lcd", width: 1080, height: 1920, orientation: "portrait", status: "active", dailyImpressions: 35000, cpm: "15.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "1.2644", longitude: "103.8229", format: "Digital Kiosk", classification: "Digital", inventoryType: "Retail", sizeCategory: "S", webcamUrl: null, webcamStatus: "inactive" },
|
|
447
|
+
{ id: randomUUID(), name: "Raffles Place Bus Shelter", venueId: null, screenType: "led", width: 1920, height: 1080, orientation: "landscape", status: "active", dailyImpressions: 28000, cpm: "12.00", country: "Singapore", city: "Singapore", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "1.2830", longitude: "103.8513", format: "Digital Bus Shelter Screen", classification: "Digital", inventoryType: "OOH", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
448
|
+
// Thailand - Bangkok
|
|
449
|
+
{ id: randomUUID(), name: "Siam Paragon Giant LED", venueId: null, screenType: "led", width: 7680, height: 4320, orientation: "landscape", status: "active", dailyImpressions: 150000, cpm: "30.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "13.7466", longitude: "100.5347", format: "Digital Large Format", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
450
|
+
{ id: randomUUID(), name: "Central World Mall Screen", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 78000, cpm: "22.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo2.id, sspId: ssp2.id, latitude: "13.7468", longitude: "100.5393", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
451
|
+
{ id: randomUUID(), name: "BTS Siam Station Screen", venueId: null, screenType: "lcd", width: 1920, height: 1080, orientation: "landscape", status: "active", dailyImpressions: 55000, cpm: "16.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "13.7451", longitude: "100.5341", format: "Train Digital Display", classification: "Digital", inventoryType: "Transit", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
452
|
+
{ id: randomUUID(), name: "Suvarnabhumi Airport Screen", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 88000, cpm: "28.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "13.6900", longitude: "100.7501", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
453
|
+
{ id: randomUUID(), name: "Terminal 21 Video Wall", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 65000, cpm: "24.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "13.7378", longitude: "100.5605", format: "Digital Video Walls", classification: "Digital", inventoryType: "OOH", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
454
|
+
{ id: randomUUID(), name: "MBK Center Elevator Screen", venueId: null, screenType: "lcd", width: 1080, height: 1920, orientation: "portrait", status: "active", dailyImpressions: 22000, cpm: "10.00", country: "Thailand", city: "Bangkok", mediaOwnerId: mo2.id, sspId: ssp2.id, latitude: "13.7446", longitude: "100.5300", format: "Elevator / Lobby Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "S", webcamUrl: null, webcamStatus: "inactive" },
|
|
455
|
+
// Thailand - Phuket
|
|
456
|
+
{ id: randomUUID(), name: "Jungceylon Phuket Screen", venueId: null, screenType: "led", width: 2560, height: 1440, orientation: "landscape", status: "active", dailyImpressions: 32000, cpm: "14.00", country: "Thailand", city: "Phuket", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "7.8961", longitude: "98.2977", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
457
|
+
// USA - New York
|
|
458
|
+
{ id: randomUUID(), name: "Times Square Digital Billboard", venueId: null, screenType: "led", width: 7680, height: 4320, orientation: "landscape", status: "active", dailyImpressions: 250000, cpm: "45.00", country: "US", city: "New York", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "40.7580", longitude: "-73.9855", format: "Digital Large Format", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
459
|
+
{ id: randomUUID(), name: "Grand Central Terminal Screen", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 180000, cpm: "38.00", country: "US", city: "New York", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "40.7527", longitude: "-73.9772", format: "Transit Hub Screen", classification: "Digital", inventoryType: "Transit", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
460
|
+
{ id: randomUUID(), name: "JFK Airport Terminal 4", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 95000, cpm: "32.00", country: "US", city: "New York", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "40.6413", longitude: "-73.7781", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
461
|
+
{ id: randomUUID(), name: "Penn Station Concourse", venueId: null, screenType: "lcd", width: 1920, height: 1080, orientation: "portrait", status: "active", dailyImpressions: 120000, cpm: "28.00", country: "US", city: "New York", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "40.7506", longitude: "-73.9935", format: "Platform Digital Screen", classification: "Digital", inventoryType: "Transit", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
462
|
+
{ id: randomUUID(), name: "Westfield World Trade Center", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 85000, cpm: "26.00", country: "US", city: "New York", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "40.7116", longitude: "-74.0112", format: "Mall / Retail Screens", classification: "Digital", inventoryType: "Retail", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
463
|
+
// USA - Los Angeles
|
|
464
|
+
{ id: randomUUID(), name: "Hollywood & Highland LED", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 145000, cpm: "35.00", country: "US", city: "Los Angeles", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "34.1022", longitude: "-118.3398", format: "Digital Large Format", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
465
|
+
{ id: randomUUID(), name: "LAX Terminal B Screen", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 88000, cpm: "30.00", country: "US", city: "Los Angeles", mediaOwnerId: mo2.id, sspId: ssp2.id, latitude: "33.9425", longitude: "-118.4081", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
466
|
+
{ id: randomUUID(), name: "Santa Monica Pier Digital", venueId: null, screenType: "led", width: 2560, height: 1440, orientation: "landscape", status: "active", dailyImpressions: 65000, cpm: "22.00", country: "US", city: "Los Angeles", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "34.0086", longitude: "-118.4992", format: "Digital Billboard", classification: "Digital", inventoryType: "OOH", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
467
|
+
// USA - Chicago
|
|
468
|
+
{ id: randomUUID(), name: "Magnificent Mile Billboard", venueId: null, screenType: "led", width: 5120, height: 2880, orientation: "landscape", status: "active", dailyImpressions: 110000, cpm: "32.00", country: "US", city: "Chicago", mediaOwnerId: mo1.id, sspId: ssp1.id, latitude: "41.8945", longitude: "-87.6240", format: "Digital Large Format", classification: "Digital", inventoryType: "OOH", sizeCategory: "XL", webcamUrl: null, webcamStatus: "inactive" },
|
|
469
|
+
{ id: randomUUID(), name: "O'Hare Airport Terminal 1", venueId: null, screenType: "led", width: 3840, height: 2160, orientation: "landscape", status: "active", dailyImpressions: 92000, cpm: "28.00", country: "US", city: "Chicago", mediaOwnerId: mo2.id, sspId: ssp1.id, latitude: "41.9742", longitude: "-87.9073", format: "Airport Terminal Screens", classification: "Digital", inventoryType: "Transit", sizeCategory: "L", webcamUrl: null, webcamStatus: "inactive" },
|
|
470
|
+
{ id: randomUUID(), name: "Chicago Union Station", venueId: null, screenType: "lcd", width: 1920, height: 1080, orientation: "portrait", status: "active", dailyImpressions: 75000, cpm: "20.00", country: "US", city: "Chicago", mediaOwnerId: mo1.id, sspId: ssp2.id, latitude: "41.8789", longitude: "-87.6400", format: "Transit Hub Screen", classification: "Digital", inventoryType: "Transit", sizeCategory: "M", webcamUrl: null, webcamStatus: "inactive" },
|
|
471
|
+
];
|
|
472
|
+
screens.forEach(s => this.screens.set(s.id, s as Screen));
|
|
473
|
+
|
|
474
|
+
// Seed player statuses for screens with webcams
|
|
475
|
+
const screensWithWebcams = screens.filter(s => s.webcamUrl);
|
|
476
|
+
screensWithWebcams.forEach((screen, index) => {
|
|
477
|
+
const playerStatus = {
|
|
478
|
+
id: randomUUID(),
|
|
479
|
+
screenId: screen.id,
|
|
480
|
+
playerId: `PLAYER-${String(index + 1).padStart(3, '0')}`,
|
|
481
|
+
cmsType: "studio",
|
|
482
|
+
status: index % 2 === 0 ? "online" : "offline",
|
|
483
|
+
lastHeartbeat: new Date().toISOString(),
|
|
484
|
+
currentContent: index % 2 === 0 ? "Holiday Promo Video" : null,
|
|
485
|
+
connectionQuality: index % 2 === 0 ? "good" : "poor",
|
|
486
|
+
lastUpdated: new Date().toISOString(),
|
|
487
|
+
};
|
|
488
|
+
this.playerStatuses.set(playerStatus.id, playerStatus as PlayerStatus);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Seed deals (4-level hierarchy: Advertiser → Deal → Line Item → Creative Assignment)
|
|
492
|
+
// Deals now contain all fields previously in Insertion Orders
|
|
493
|
+
const deal1 = {
|
|
494
|
+
id: randomUUID(),
|
|
495
|
+
name: "Holiday Season 2024",
|
|
496
|
+
advertiserId: advertiser1.id,
|
|
497
|
+
brandId: brand1.id,
|
|
498
|
+
agencyId: agency1.id,
|
|
499
|
+
status: "active",
|
|
500
|
+
mediaType: "dooh",
|
|
501
|
+
clientType: "agency",
|
|
502
|
+
sspId: sspInfluence.id,
|
|
503
|
+
dealType: "traditional",
|
|
504
|
+
dspId: null,
|
|
505
|
+
seatNames: null,
|
|
506
|
+
seatIds: null,
|
|
507
|
+
countries: ["US"],
|
|
508
|
+
currency: "USD",
|
|
509
|
+
timezone: "America/New_York",
|
|
510
|
+
budget: "50000.00",
|
|
511
|
+
spent: "28500.00",
|
|
512
|
+
goalType: "impressions",
|
|
513
|
+
goalValue: 1000000,
|
|
514
|
+
targetType: "impressions",
|
|
515
|
+
targetValue: 800000,
|
|
516
|
+
hardStop: false,
|
|
517
|
+
startDate: "2024-11-01",
|
|
518
|
+
endDate: "2024-12-31",
|
|
519
|
+
adPlayVerificationEnabled: false,
|
|
520
|
+
adPlayVerificationProvider: null,
|
|
521
|
+
externalDealId: "10042567",
|
|
522
|
+
source: "influence",
|
|
523
|
+
acceptanceSent: false,
|
|
524
|
+
reopened: false,
|
|
525
|
+
isRfp: false,
|
|
526
|
+
createdBy: "John Doe",
|
|
527
|
+
createdAt: "2024-10-15",
|
|
528
|
+
updatedAt: null
|
|
529
|
+
};
|
|
530
|
+
const deal2 = {
|
|
531
|
+
id: randomUUID(),
|
|
532
|
+
name: "Brand Awareness Q1",
|
|
533
|
+
advertiserId: advertiser2.id,
|
|
534
|
+
brandId: brand2.id,
|
|
535
|
+
agencyId: agency2.id,
|
|
536
|
+
status: "active",
|
|
537
|
+
mediaType: "dooh",
|
|
538
|
+
clientType: "agency",
|
|
539
|
+
sspId: sspInfluence.id,
|
|
540
|
+
dealType: "pd", // Preferred Deal - Activate RFP only applies to PG or PD
|
|
541
|
+
dspId: dsp3.id,
|
|
542
|
+
seatNames: ["Primary Seat", "Secondary Seat"],
|
|
543
|
+
seatIds: ["seat-1", "seat-2"],
|
|
544
|
+
countries: ["US"],
|
|
545
|
+
currency: "USD",
|
|
546
|
+
timezone: "America/Chicago",
|
|
547
|
+
budget: "35000.00",
|
|
548
|
+
spent: "22100.00",
|
|
549
|
+
goalType: "reach",
|
|
550
|
+
goalValue: 1000000,
|
|
551
|
+
targetType: "ad_plays",
|
|
552
|
+
targetValue: 800000,
|
|
553
|
+
hardStop: false,
|
|
554
|
+
startDate: "2024-01-01",
|
|
555
|
+
endDate: "2024-03-31",
|
|
556
|
+
adPlayVerificationEnabled: true,
|
|
557
|
+
adPlayVerificationProvider: "MAV",
|
|
558
|
+
externalDealId: "10042891",
|
|
559
|
+
source: "activate",
|
|
560
|
+
acceptanceSent: false,
|
|
561
|
+
reopened: false,
|
|
562
|
+
isRfp: true,
|
|
563
|
+
createdBy: "Jane Smith",
|
|
564
|
+
createdAt: "2023-12-15",
|
|
565
|
+
updatedAt: null
|
|
566
|
+
};
|
|
567
|
+
const deal3 = {
|
|
568
|
+
id: randomUUID(),
|
|
569
|
+
name: "New Model Launch",
|
|
570
|
+
advertiserId: advertiser3.id,
|
|
571
|
+
brandId: brand6.id,
|
|
572
|
+
agencyId: null,
|
|
573
|
+
status: "paused",
|
|
574
|
+
mediaType: "dooh",
|
|
575
|
+
clientType: "direct",
|
|
576
|
+
sspId: sspInfluence.id,
|
|
577
|
+
dealType: "pg",
|
|
578
|
+
dspId: dsp1.id,
|
|
579
|
+
seatNames: ["Primary Seat"],
|
|
580
|
+
seatIds: ["seat-1"],
|
|
581
|
+
countries: ["US", "MY"],
|
|
582
|
+
currency: "USD",
|
|
583
|
+
timezone: "America/New_York",
|
|
584
|
+
budget: "75000.00",
|
|
585
|
+
spent: "0.00",
|
|
586
|
+
goalType: "impressions",
|
|
587
|
+
goalValue: 2000000,
|
|
588
|
+
targetType: "impressions",
|
|
589
|
+
targetValue: 1500000,
|
|
590
|
+
hardStop: true,
|
|
591
|
+
startDate: "2024-02-01",
|
|
592
|
+
endDate: "2024-04-30",
|
|
593
|
+
adPlayVerificationEnabled: false,
|
|
594
|
+
adPlayVerificationProvider: null,
|
|
595
|
+
externalDealId: "10043102",
|
|
596
|
+
source: "influence",
|
|
597
|
+
acceptanceSent: false,
|
|
598
|
+
reopened: false,
|
|
599
|
+
isRfp: false,
|
|
600
|
+
createdBy: "Mike Johnson",
|
|
601
|
+
createdAt: "2024-01-15",
|
|
602
|
+
updatedAt: null
|
|
603
|
+
};
|
|
604
|
+
const deal4 = {
|
|
605
|
+
id: randomUUID(),
|
|
606
|
+
name: "Summer Sale Promo",
|
|
607
|
+
advertiserId: advertiser2.id,
|
|
608
|
+
brandId: brand4.id,
|
|
609
|
+
agencyId: agency3.id,
|
|
610
|
+
status: "paused",
|
|
611
|
+
mediaType: "dooh",
|
|
612
|
+
clientType: "agency",
|
|
613
|
+
sspId: sspInfluence.id,
|
|
614
|
+
dealType: "traditional",
|
|
615
|
+
dspId: null,
|
|
616
|
+
seatNames: null,
|
|
617
|
+
seatIds: null,
|
|
618
|
+
countries: ["SG", "MY"],
|
|
619
|
+
currency: "SGD",
|
|
620
|
+
timezone: "Asia/Singapore",
|
|
621
|
+
budget: "25000.00",
|
|
622
|
+
spent: "0.00",
|
|
623
|
+
goalType: null,
|
|
624
|
+
goalValue: null,
|
|
625
|
+
targetType: null,
|
|
626
|
+
targetValue: null,
|
|
627
|
+
hardStop: false,
|
|
628
|
+
startDate: "2024-06-01",
|
|
629
|
+
endDate: "2024-08-31",
|
|
630
|
+
adPlayVerificationEnabled: false,
|
|
631
|
+
adPlayVerificationProvider: null,
|
|
632
|
+
externalDealId: "10043245",
|
|
633
|
+
source: "planner",
|
|
634
|
+
acceptanceSent: false,
|
|
635
|
+
reopened: false,
|
|
636
|
+
isRfp: false,
|
|
637
|
+
createdBy: "Sarah Wilson",
|
|
638
|
+
createdAt: "2024-05-01",
|
|
639
|
+
updatedAt: null
|
|
640
|
+
};
|
|
641
|
+
this.deals.set(deal1.id, deal1 as Deal);
|
|
642
|
+
this.deals.set(deal2.id, deal2 as Deal);
|
|
643
|
+
this.deals.set(deal3.id, deal3 as Deal);
|
|
644
|
+
this.deals.set(deal4.id, deal4 as Deal);
|
|
645
|
+
|
|
646
|
+
// Seed line items (now directly under deals - 4-level hierarchy)
|
|
647
|
+
const lineItem1 = { id: randomUUID(), name: "Mall Entrance - 15s Spot", dealId: deal1.id, mediaOwnerId: mo2.id, status: "active", startDate: "2024-11-01", endDate: "2024-12-31", targetImpressions: 500000, deliveredImpressions: 489000, budget: "6250.00", spent: "5890.00", cpm: "12.50", trafficAllocation: 100, priority: 1, creativeDuration: 15 };
|
|
648
|
+
const lineItem2 = { id: randomUUID(), name: "Food Court - 10s Spot", dealId: deal1.id, mediaOwnerId: mo2.id, status: "active", startDate: "2024-11-01", endDate: "2024-12-31", targetImpressions: 350000, deliveredImpressions: 310000, budget: "3500.00", spent: "3100.00", cpm: "10.00", trafficAllocation: 75, priority: 2, creativeDuration: 10 };
|
|
649
|
+
const lineItem3 = { id: randomUUID(), name: "Transit Main Concourse", dealId: deal1.id, mediaOwnerId: mo4.id, status: "active", startDate: "2024-11-15", endDate: "2024-12-31", targetImpressions: 800000, deliveredImpressions: 572000, budget: "20000.00", spent: "14300.00", cpm: "25.00", trafficAllocation: 80, priority: 1, creativeDuration: 20 };
|
|
650
|
+
const lineItem4 = { id: randomUUID(), name: "Airport Premium", dealId: deal2.id, mediaOwnerId: mo1.id, status: "active", startDate: "2024-01-01", endDate: "2024-03-31", targetImpressions: 1000000, deliveredImpressions: 920000, budget: "18000.00", spent: "16560.00", cpm: "18.00", trafficAllocation: 100, priority: 1, creativeDuration: 15 };
|
|
651
|
+
this.lineItems.set(lineItem1.id, lineItem1 as LineItem);
|
|
652
|
+
this.lineItems.set(lineItem2.id, lineItem2 as LineItem);
|
|
653
|
+
this.lineItems.set(lineItem3.id, lineItem3 as LineItem);
|
|
654
|
+
this.lineItems.set(lineItem4.id, lineItem4 as LineItem);
|
|
655
|
+
|
|
656
|
+
// Seed Creative Folders
|
|
657
|
+
const folder1 = { id: randomUUID(), name: "Summer Campaign", createdAt: "2024-06-15" };
|
|
658
|
+
const folder2 = { id: randomUUID(), name: "Product Videos", createdAt: "2024-07-01" };
|
|
659
|
+
const folder3 = { id: randomUUID(), name: "Brand Assets", createdAt: "2024-05-20" };
|
|
660
|
+
this.creativeFolders.set(folder1.id, folder1 as CreativeFolder);
|
|
661
|
+
this.creativeFolders.set(folder2.id, folder2 as CreativeFolder);
|
|
662
|
+
this.creativeFolders.set(folder3.id, folder3 as CreativeFolder);
|
|
663
|
+
|
|
664
|
+
// Seed Creatives with source platform tracking
|
|
665
|
+
const creatives = [
|
|
666
|
+
{ id: randomUUID(), name: "Summer Sale Banner 1920x1080", type: "display", format: "JPG", width: 1920, height: 1080, fileSize: 245000, duration: null, status: "accepted", folderId: folder1.id, tags: ["summer", "sale", "banner"], uploadedAt: "2024-06-16", uploadedBy: "john@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "uploaded", sourcePlatform: "influence", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
667
|
+
{ id: randomUUID(), name: "Product Launch Video 30s", type: "video", format: "MP4", width: 1920, height: 1080, fileSize: 15000000, duration: 30, status: "accepted", folderId: folder2.id, tags: ["product", "launch", "video"], uploadedAt: "2024-07-02", uploadedBy: "sarah@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "api", sourcePlatform: "planner", transcodingEnabled: true, transcodingStatus: "completed", transcodedUrl: null },
|
|
668
|
+
{ id: randomUUID(), name: "Brand Logo Animation", type: "video", format: "MP4", width: 800, height: 600, fileSize: 3500000, duration: 5, status: "processing", folderId: folder3.id, tags: ["brand", "logo", "animation"], uploadedAt: "2024-07-10", uploadedBy: "mike@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "bid_stream_vast", sourcePlatform: "activate", transcodingEnabled: true, transcodingStatus: "processing", transcodedUrl: null },
|
|
669
|
+
{ id: randomUUID(), name: "Holiday Promo Display", type: "display", format: "PNG", width: 1080, height: 1920, fileSize: 380000, duration: null, status: "new", folderId: folder1.id, tags: ["holiday", "promo"], uploadedAt: "2024-07-12", uploadedBy: "lisa@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "media_owner", sourcePlatform: "media_owner", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
670
|
+
{ id: randomUUID(), name: "Interactive HTML5 Banner", type: "html", format: "HTML", width: 300, height: 250, fileSize: 125000, duration: null, status: "accepted", folderId: folder3.id, tags: ["interactive", "html5"], uploadedAt: "2024-06-20", uploadedBy: "john@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "uploaded", sourcePlatform: "influence", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
671
|
+
{ id: randomUUID(), name: "Native Ad Creative", type: "native", format: "JSON", width: 0, height: 0, fileSize: 8500, duration: null, status: "accepted", folderId: null, tags: ["native", "social"], uploadedAt: "2024-07-05", uploadedBy: "sarah@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "api", sourcePlatform: "activate", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
672
|
+
{ id: randomUUID(), name: "Summer Beach Video", type: "video", format: "MP4", width: 3840, height: 2160, fileSize: 45000000, duration: 15, status: "inadequate", folderId: folder1.id, tags: ["summer", "beach"], uploadedAt: "2024-06-25", uploadedBy: "mike@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: "Video resolution too high for target screens", creativeSource: "bid_stream_vast", sourcePlatform: "activate", transcodingEnabled: true, transcodingStatus: "failed", transcodedUrl: null },
|
|
673
|
+
{ id: randomUUID(), name: "Clearance Sale PNG", type: "display", format: "PNG", width: 728, height: 90, fileSize: 85000, duration: null, status: "archived", folderId: folder1.id, tags: ["clearance", "sale"], uploadedAt: "2024-05-01", uploadedBy: "lisa@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "uploaded", sourcePlatform: "influence", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
674
|
+
{ id: randomUUID(), name: "Product Demo Video", type: "video", format: "MP4", width: 1280, height: 720, fileSize: 22000000, duration: 45, status: "accepted", folderId: folder2.id, tags: ["demo", "product", "tutorial"], uploadedAt: "2024-07-08", uploadedBy: "john@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "api", sourcePlatform: "planner", transcodingEnabled: true, transcodingStatus: "completed", transcodedUrl: null },
|
|
675
|
+
{ id: randomUUID(), name: "Brand Guidelines Banner", type: "display", format: "JPG", width: 1200, height: 628, fileSize: 195000, duration: null, status: "new", folderId: folder3.id, tags: ["brand", "guidelines"], uploadedAt: "2024-07-14", uploadedBy: "sarah@example.com", thumbnailUrl: null, fileUrl: null, inadequateReason: null, creativeSource: "uploaded", sourcePlatform: "influence", transcodingEnabled: false, transcodingStatus: null, transcodedUrl: null },
|
|
676
|
+
];
|
|
677
|
+
creatives.forEach(c => this.creatives.set(c.id, c as Creative));
|
|
678
|
+
|
|
679
|
+
// Seed proof of play records with linked data
|
|
680
|
+
const popNow = new Date();
|
|
681
|
+
const popScreens = screens.slice(0, 5);
|
|
682
|
+
const popCreatives = creatives.slice(0, 3);
|
|
683
|
+
popScreens.forEach((screen, idx) => {
|
|
684
|
+
for (let i = 0; i < 3; i++) {
|
|
685
|
+
const startTime = new Date(popNow.getTime() - (idx * 24 + i) * 60 * 60 * 1000);
|
|
686
|
+
const endTime = new Date(startTime.getTime() + 15000);
|
|
687
|
+
const popRecord = {
|
|
688
|
+
id: randomUUID(),
|
|
689
|
+
screenId: screen.id,
|
|
690
|
+
playerId: `PLAYER-${String(idx + 1).padStart(3, '0')}`,
|
|
691
|
+
creativeId: popCreatives[i % popCreatives.length].id,
|
|
692
|
+
dealId: deal1.id,
|
|
693
|
+
lineItemId: lineItem1.id,
|
|
694
|
+
startTimestamp: startTime.toISOString(),
|
|
695
|
+
endTimestamp: endTime.toISOString(),
|
|
696
|
+
durationSeconds: 15,
|
|
697
|
+
impressionCount: 1,
|
|
698
|
+
mediaFileUrl: null,
|
|
699
|
+
proofImageUrl: null,
|
|
700
|
+
proofVideoUrl: null,
|
|
701
|
+
source: i % 2 === 0 ? "cms" : "manual",
|
|
702
|
+
status: "processed",
|
|
703
|
+
};
|
|
704
|
+
this.proofOfPlayRecords.set(popRecord.id, popRecord as ProofOfPlay);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// Seed Change Logs for testing
|
|
709
|
+
const now = new Date();
|
|
710
|
+
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
|
711
|
+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
712
|
+
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
713
|
+
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
714
|
+
|
|
715
|
+
this.changeLogs.set("log-deal-1", {
|
|
716
|
+
id: "log-deal-1",
|
|
717
|
+
entityType: "deal",
|
|
718
|
+
entityId: deal1.id,
|
|
719
|
+
action: "created",
|
|
720
|
+
changedBy: "John Doe",
|
|
721
|
+
changedAt: threeDaysAgo.toISOString(),
|
|
722
|
+
changes: null,
|
|
723
|
+
description: "Deal created for Holiday Campaign 2024"
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
this.changeLogs.set("log-deal-2", {
|
|
727
|
+
id: "log-deal-2",
|
|
728
|
+
entityType: "deal",
|
|
729
|
+
entityId: deal1.id,
|
|
730
|
+
action: "updated",
|
|
731
|
+
changedBy: "Jane Smith",
|
|
732
|
+
changedAt: twoDaysAgo.toISOString(),
|
|
733
|
+
changes: { budget: { old: "50000", new: "75000" }, status: { old: "paused", new: "active" } },
|
|
734
|
+
description: "Budget increased and deal activated"
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
this.changeLogs.set("log-deal-3", {
|
|
738
|
+
id: "log-deal-3",
|
|
739
|
+
entityType: "deal",
|
|
740
|
+
entityId: deal2.id,
|
|
741
|
+
action: "created",
|
|
742
|
+
changedBy: "Mike Johnson",
|
|
743
|
+
changedAt: twoDaysAgo.toISOString(),
|
|
744
|
+
changes: null,
|
|
745
|
+
description: "Brand Awareness Q1 deal created"
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
this.changeLogs.set("log-li-1", {
|
|
749
|
+
id: "log-li-1",
|
|
750
|
+
entityType: "line_item",
|
|
751
|
+
entityId: lineItem1.id,
|
|
752
|
+
action: "created",
|
|
753
|
+
changedBy: "John Doe",
|
|
754
|
+
changedAt: oneDayAgo.toISOString(),
|
|
755
|
+
changes: null,
|
|
756
|
+
description: "Mall Entrance - 15s Spot line item created"
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
this.changeLogs.set("log-li-2", {
|
|
760
|
+
id: "log-li-2",
|
|
761
|
+
entityType: "line_item",
|
|
762
|
+
entityId: lineItem1.id,
|
|
763
|
+
action: "updated",
|
|
764
|
+
changedBy: "Mike Johnson",
|
|
765
|
+
changedAt: oneHourAgo.toISOString(),
|
|
766
|
+
changes: { priority: { old: "5", new: "1" }, trafficAllocation: { old: "50", new: "100" } },
|
|
767
|
+
description: "Priority increased and traffic allocation updated"
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
this.changeLogs.set("log-li-3", {
|
|
771
|
+
id: "log-li-3",
|
|
772
|
+
entityType: "line_item",
|
|
773
|
+
entityId: lineItem2.id,
|
|
774
|
+
action: "created",
|
|
775
|
+
changedBy: "Sarah Wilson",
|
|
776
|
+
changedAt: oneHourAgo.toISOString(),
|
|
777
|
+
changes: null,
|
|
778
|
+
description: "Food Court - 10s Spot line item created"
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
this.changeLogs.set("log-creative-1", {
|
|
782
|
+
id: "log-creative-1",
|
|
783
|
+
entityType: "creative",
|
|
784
|
+
entityId: creatives[0].id,
|
|
785
|
+
action: "created",
|
|
786
|
+
changedBy: "Lisa Brown",
|
|
787
|
+
changedAt: twoDaysAgo.toISOString(),
|
|
788
|
+
changes: null,
|
|
789
|
+
description: "Summer Sale Banner uploaded"
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
this.changeLogs.set("log-creative-2", {
|
|
793
|
+
id: "log-creative-2",
|
|
794
|
+
entityType: "creative",
|
|
795
|
+
entityId: creatives[0].id,
|
|
796
|
+
action: "approved",
|
|
797
|
+
changedBy: "Content Reviewer",
|
|
798
|
+
changedAt: oneDayAgo.toISOString(),
|
|
799
|
+
changes: { status: { old: "new", new: "accepted" } },
|
|
800
|
+
description: "Creative approved for distribution"
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
this.changeLogs.set("log-creative-3", {
|
|
804
|
+
id: "log-creative-3",
|
|
805
|
+
entityType: "creative",
|
|
806
|
+
entityId: creatives[6].id,
|
|
807
|
+
action: "rejected",
|
|
808
|
+
changedBy: "Quality Assurance",
|
|
809
|
+
changedAt: oneHourAgo.toISOString(),
|
|
810
|
+
changes: { status: { old: "processing", new: "inadequate" } },
|
|
811
|
+
description: "Creative rejected: Video resolution too high for target screens"
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Seed POIs for Malaysia and Singapore
|
|
815
|
+
const poiData: POI[] = [
|
|
816
|
+
// Malaysia - Kuala Lumpur
|
|
817
|
+
{ id: "poi-my-pavilion", name: "Pavilion KL", category: "Shopping Mall", latitude: 3.1490, longitude: 101.7133, country: "Malaysia", city: "Kuala Lumpur", address: "168, Jalan Bukit Bintang" },
|
|
818
|
+
{ id: "poi-my-klcc", name: "KLCC Suria Mall", category: "Shopping Mall", latitude: 3.1588, longitude: 101.7130, country: "Malaysia", city: "Kuala Lumpur", address: "Kuala Lumpur City Centre" },
|
|
819
|
+
{ id: "poi-my-midvalley", name: "Mid Valley Megamall", category: "Shopping Mall", latitude: 3.1178, longitude: 101.6771, country: "Malaysia", city: "Kuala Lumpur", address: "Mid Valley City, Lingkaran Syed Putra" },
|
|
820
|
+
{ id: "poi-my-sunway", name: "Sunway Pyramid", category: "Shopping Mall", latitude: 3.0731, longitude: 101.6075, country: "Malaysia", city: "Petaling Jaya", address: "3, Jalan PJS 11/15, Bandar Sunway" },
|
|
821
|
+
{ id: "poi-my-onemount", name: "1 Mont Kiara", category: "Shopping Mall", latitude: 3.1711, longitude: 101.6530, country: "Malaysia", city: "Kuala Lumpur", address: "1 Jalan Kiara, Mont Kiara" },
|
|
822
|
+
{ id: "poi-my-klia", name: "KLIA International Airport", category: "Airport", latitude: 2.7456, longitude: 101.7099, country: "Malaysia", city: "Sepang", address: "Kuala Lumpur International Airport" },
|
|
823
|
+
{ id: "poi-my-klsentral", name: "KL Sentral Station", category: "Train Station", latitude: 3.1346, longitude: 101.6865, country: "Malaysia", city: "Kuala Lumpur", address: "Jalan Stesen Sentral" },
|
|
824
|
+
{ id: "poi-my-gleneagles", name: "Gleneagles Hospital", category: "Hospital", latitude: 3.1628, longitude: 101.7321, country: "Malaysia", city: "Kuala Lumpur", address: "286, Jalan Ampang" },
|
|
825
|
+
{ id: "poi-my-gardens", name: "The Gardens Mall", category: "Shopping Mall", latitude: 3.1173, longitude: 101.6774, country: "Malaysia", city: "Kuala Lumpur", address: "Mid Valley City" },
|
|
826
|
+
{ id: "poi-my-gurney", name: "Gurney Plaza", category: "Shopping Mall", latitude: 5.4372, longitude: 100.3105, country: "Malaysia", city: "Penang", address: "170, Persiaran Gurney" },
|
|
827
|
+
// Singapore
|
|
828
|
+
{ id: "poi-sg-ion", name: "ION Orchard", category: "Shopping Mall", latitude: 1.3039, longitude: 103.8318, country: "Singapore", city: "Singapore", address: "2 Orchard Turn" },
|
|
829
|
+
{ id: "poi-sg-vivocity", name: "VivoCity", category: "Shopping Mall", latitude: 1.2644, longitude: 103.8223, country: "Singapore", city: "Singapore", address: "1 HarbourFront Walk" },
|
|
830
|
+
{ id: "poi-sg-mbs", name: "Marina Bay Sands", category: "Hotel", latitude: 1.2847, longitude: 103.8610, country: "Singapore", city: "Singapore", address: "10 Bayfront Avenue" },
|
|
831
|
+
{ id: "poi-sg-jewel", name: "Jewel Changi Airport", category: "Shopping Mall", latitude: 1.3603, longitude: 103.9894, country: "Singapore", city: "Singapore", address: "78 Airport Boulevard" },
|
|
832
|
+
{ id: "poi-sg-changi", name: "Changi Airport", category: "Airport", latitude: 1.3644, longitude: 103.9915, country: "Singapore", city: "Singapore", address: "Airport Boulevard" },
|
|
833
|
+
{ id: "poi-sg-ngeeann", name: "Ngee Ann City", category: "Shopping Mall", latitude: 1.3028, longitude: 103.8358, country: "Singapore", city: "Singapore", address: "391 Orchard Road" },
|
|
834
|
+
{ id: "poi-sg-suntec", name: "Suntec City", category: "Shopping Mall", latitude: 1.2944, longitude: 103.8575, country: "Singapore", city: "Singapore", address: "3 Temasek Boulevard" },
|
|
835
|
+
{ id: "poi-sg-somerset", name: "Somerset 313", category: "Shopping Mall", latitude: 1.3010, longitude: 103.8390, country: "Singapore", city: "Singapore", address: "313 Orchard Road" },
|
|
836
|
+
{ id: "poi-sg-raffles", name: "Raffles City", category: "Shopping Mall", latitude: 1.2938, longitude: 103.8527, country: "Singapore", city: "Singapore", address: "252 North Bridge Road" },
|
|
837
|
+
{ id: "poi-sg-sgh", name: "Singapore General Hospital", category: "Hospital", latitude: 1.2792, longitude: 103.8355, country: "Singapore", city: "Singapore", address: "Outram Road" },
|
|
838
|
+
];
|
|
839
|
+
|
|
840
|
+
poiData.forEach(poi => this.pois.set(poi.id, poi));
|
|
841
|
+
|
|
842
|
+
// Sample player statuses for CMS integration
|
|
843
|
+
const playerStatusData: PlayerStatus[] = [
|
|
844
|
+
{ id: randomUUID(), screenId: "screen-001", playerId: "player-kl-001", cmsType: "studio", status: "online", lastHeartbeat: new Date().toISOString(), currentContent: "Summer_Campaign_2026.mp4", connectionQuality: "excellent", lastUpdated: new Date().toISOString() },
|
|
845
|
+
{ id: randomUUID(), screenId: "screen-002", playerId: "player-kl-002", cmsType: "studio", status: "online", lastHeartbeat: new Date().toISOString(), currentContent: "TechBrand_Launch.mp4", connectionQuality: "good", lastUpdated: new Date().toISOString() },
|
|
846
|
+
{ id: randomUUID(), screenId: "screen-003", playerId: "player-kl-003", cmsType: "broadsign", status: "offline", lastHeartbeat: new Date(Date.now() - 3600000).toISOString(), currentContent: null, connectionQuality: "poor", lastUpdated: new Date().toISOString() },
|
|
847
|
+
{ id: randomUUID(), screenId: "screen-004", playerId: "player-sg-001", cmsType: "broadsign", status: "online", lastHeartbeat: new Date().toISOString(), currentContent: "Holiday_Promo_Asia.mp4", connectionQuality: "excellent", lastUpdated: new Date().toISOString() },
|
|
848
|
+
{ id: randomUUID(), screenId: "screen-005", playerId: "player-sg-002", cmsType: "scala", status: "online", lastHeartbeat: new Date().toISOString(), currentContent: "Retail_Promotion_SG.mp4", connectionQuality: "good", lastUpdated: new Date().toISOString() },
|
|
849
|
+
{ id: randomUUID(), screenId: "screen-006", playerId: "player-sg-003", cmsType: "studio", status: "unknown", lastHeartbeat: null, currentContent: null, connectionQuality: null, lastUpdated: new Date().toISOString() },
|
|
850
|
+
{ id: randomUUID(), screenId: "screen-007", playerId: "player-us-001", cmsType: "navori", status: "online", lastHeartbeat: new Date().toISOString(), currentContent: "Auto_Launch_Campaign.mp4", connectionQuality: "excellent", lastUpdated: new Date().toISOString() },
|
|
851
|
+
{ id: randomUUID(), screenId: "screen-008", playerId: "player-us-002", cmsType: "navori", status: "offline", lastHeartbeat: new Date(Date.now() - 7200000).toISOString(), currentContent: null, connectionQuality: null, lastUpdated: new Date().toISOString() },
|
|
852
|
+
];
|
|
853
|
+
|
|
854
|
+
playerStatusData.forEach(ps => this.playerStatuses.set(ps.id, ps));
|
|
855
|
+
|
|
856
|
+
// Sample transit routes for Kuala Lumpur
|
|
857
|
+
const transitRouteData: TransitRoute[] = [
|
|
858
|
+
{ id: "route-mrt-kajang", name: "MRT Kajang Line", routeType: "mrt", routeNumber: "MRT1", color: "#1E88E5", city: "Kuala Lumpur", country: "Malaysia", operatorName: "Prasarana", geojson: { type: "LineString", coordinates: [[101.6865, 3.1346], [101.6965, 3.1400], [101.7130, 3.1500], [101.7200, 3.1600], [101.7300, 3.1700]] } },
|
|
859
|
+
{ id: "route-mrt-putrajaya", name: "MRT Putrajaya Line", routeType: "mrt", routeNumber: "MRT2", color: "#43A047", city: "Kuala Lumpur", country: "Malaysia", operatorName: "Prasarana", geojson: { type: "LineString", coordinates: [[101.6500, 3.0800], [101.6700, 3.1000], [101.6865, 3.1346], [101.7100, 3.1600]] } },
|
|
860
|
+
{ id: "route-lrt-kelana", name: "LRT Kelana Jaya Line", routeType: "lrt", routeNumber: "LRT1", color: "#E53935", city: "Kuala Lumpur", country: "Malaysia", operatorName: "Prasarana", geojson: { type: "LineString", coordinates: [[101.5900, 3.1100], [101.6200, 3.1200], [101.6500, 3.1300], [101.6865, 3.1346], [101.7200, 3.1500]] } },
|
|
861
|
+
{ id: "route-bus-gokl-green", name: "GoKL Green Line", routeType: "bus", routeNumber: "G", color: "#4CAF50", city: "Kuala Lumpur", country: "Malaysia", operatorName: "GoKL", geojson: { type: "LineString", coordinates: [[101.6865, 3.1346], [101.6900, 3.1400], [101.7000, 3.1450], [101.7100, 3.1500]] } },
|
|
862
|
+
{ id: "route-bus-gokl-red", name: "GoKL Red Line", routeType: "bus", routeNumber: "R", color: "#F44336", city: "Kuala Lumpur", country: "Malaysia", operatorName: "GoKL", geojson: { type: "LineString", coordinates: [[101.6800, 3.1300], [101.6865, 3.1346], [101.6950, 3.1400], [101.7050, 3.1350]] } },
|
|
863
|
+
];
|
|
864
|
+
|
|
865
|
+
transitRouteData.forEach(route => this.transitRoutes.set(route.id, route));
|
|
866
|
+
|
|
867
|
+
// Sample transit zones for taxi/ridehail coverage
|
|
868
|
+
const transitZoneData: TransitZone[] = [
|
|
869
|
+
{ id: "zone-taxi-kl-central", name: "KL Central Taxi Zone", zoneType: "taxi", color: "#FFC107", city: "Kuala Lumpur", country: "Malaysia", geojson: { type: "Polygon", coordinates: [[[101.65, 3.10], [101.75, 3.10], [101.75, 3.20], [101.65, 3.20], [101.65, 3.10]]] } },
|
|
870
|
+
{ id: "zone-grab-premium", name: "Grab Premium Coverage", zoneType: "ridehail", color: "#00C853", city: "Kuala Lumpur", country: "Malaysia", geojson: { type: "Polygon", coordinates: [[[101.60, 3.05], [101.80, 3.05], [101.80, 3.25], [101.60, 3.25], [101.60, 3.05]]] } },
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
transitZoneData.forEach(zone => this.transitZones.set(zone.id, zone));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Users
|
|
877
|
+
async getUser(id: string): Promise<User | undefined> {
|
|
878
|
+
return this.users.get(id);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async getUserByUsername(username: string): Promise<User | undefined> {
|
|
882
|
+
return Array.from(this.users.values()).find((user) => user.username === username);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async createUser(insertUser: InsertUser): Promise<User> {
|
|
886
|
+
const id = randomUUID();
|
|
887
|
+
const user: User = { ...insertUser, id, role: "user" };
|
|
888
|
+
this.users.set(id, user);
|
|
889
|
+
return user;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Advertisers
|
|
893
|
+
async getAdvertisers(): Promise<Advertiser[]> {
|
|
894
|
+
return Array.from(this.advertisers.values());
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async getAdvertiser(id: string): Promise<Advertiser | undefined> {
|
|
898
|
+
return this.advertisers.get(id);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async createAdvertiser(advertiser: InsertAdvertiser): Promise<Advertiser> {
|
|
902
|
+
const id = randomUUID();
|
|
903
|
+
const newAdvertiser: Advertiser = {
|
|
904
|
+
id,
|
|
905
|
+
name: advertiser.name,
|
|
906
|
+
contactEmail: advertiser.contactEmail ?? null,
|
|
907
|
+
contactPhone: advertiser.contactPhone ?? null,
|
|
908
|
+
status: advertiser.status ?? "active",
|
|
909
|
+
};
|
|
910
|
+
this.advertisers.set(id, newAdvertiser);
|
|
911
|
+
return newAdvertiser;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async updateAdvertiser(id: string, data: Partial<InsertAdvertiser>): Promise<Advertiser | undefined> {
|
|
915
|
+
const existing = this.advertisers.get(id);
|
|
916
|
+
if (!existing) return undefined;
|
|
917
|
+
const updated = { ...existing, ...data };
|
|
918
|
+
this.advertisers.set(id, updated);
|
|
919
|
+
return updated;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
async deleteAdvertiser(id: string): Promise<boolean> {
|
|
923
|
+
return this.advertisers.delete(id);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Agencies
|
|
927
|
+
async getAgencies(): Promise<Agency[]> {
|
|
928
|
+
return Array.from(this.agencies.values());
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async getAgency(id: string): Promise<Agency | undefined> {
|
|
932
|
+
return this.agencies.get(id);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async createAgency(agency: InsertAgency): Promise<Agency> {
|
|
936
|
+
const id = randomUUID();
|
|
937
|
+
const newAgency: Agency = {
|
|
938
|
+
id,
|
|
939
|
+
name: agency.name,
|
|
940
|
+
contactEmail: agency.contactEmail ?? null,
|
|
941
|
+
country: agency.country ?? null,
|
|
942
|
+
status: agency.status ?? "active",
|
|
943
|
+
};
|
|
944
|
+
this.agencies.set(id, newAgency);
|
|
945
|
+
return newAgency;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async updateAgency(id: string, data: Partial<InsertAgency>): Promise<Agency | undefined> {
|
|
949
|
+
const existing = this.agencies.get(id);
|
|
950
|
+
if (!existing) return undefined;
|
|
951
|
+
const updated = { ...existing, ...data };
|
|
952
|
+
this.agencies.set(id, updated);
|
|
953
|
+
return updated;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async deleteAgency(id: string): Promise<boolean> {
|
|
957
|
+
return this.agencies.delete(id);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Brands
|
|
961
|
+
async getBrands(): Promise<Brand[]> {
|
|
962
|
+
return Array.from(this.brands.values());
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async getBrand(id: string): Promise<Brand | undefined> {
|
|
966
|
+
return this.brands.get(id);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async createBrand(brand: InsertBrand): Promise<Brand> {
|
|
970
|
+
const id = randomUUID();
|
|
971
|
+
const newBrand: Brand = {
|
|
972
|
+
id,
|
|
973
|
+
name: brand.name,
|
|
974
|
+
iabCategory: brand.iabCategory ?? null,
|
|
975
|
+
website: brand.website ?? null,
|
|
976
|
+
logoUrl: brand.logoUrl ?? null,
|
|
977
|
+
status: brand.status ?? "active",
|
|
978
|
+
};
|
|
979
|
+
this.brands.set(id, newBrand);
|
|
980
|
+
return newBrand;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async updateBrand(id: string, data: Partial<InsertBrand>): Promise<Brand | undefined> {
|
|
984
|
+
const existing = this.brands.get(id);
|
|
985
|
+
if (!existing) return undefined;
|
|
986
|
+
const updated = { ...existing, ...data };
|
|
987
|
+
this.brands.set(id, updated);
|
|
988
|
+
return updated;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async deleteBrand(id: string): Promise<boolean> {
|
|
992
|
+
return this.brands.delete(id);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Media Owners
|
|
996
|
+
async getMediaOwners(): Promise<MediaOwner[]> {
|
|
997
|
+
return Array.from(this.mediaOwners.values());
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async getMediaOwner(id: string): Promise<MediaOwner | undefined> {
|
|
1001
|
+
return this.mediaOwners.get(id);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async createMediaOwner(mediaOwner: InsertMediaOwner): Promise<MediaOwner> {
|
|
1005
|
+
const id = randomUUID();
|
|
1006
|
+
const newMediaOwner: MediaOwner = {
|
|
1007
|
+
id,
|
|
1008
|
+
name: mediaOwner.name,
|
|
1009
|
+
country: mediaOwner.country ?? null,
|
|
1010
|
+
status: mediaOwner.status ?? "active",
|
|
1011
|
+
parentId: mediaOwner.parentId ?? null,
|
|
1012
|
+
companyType: mediaOwner.companyType ?? "media_owner",
|
|
1013
|
+
userId: mediaOwner.userId ?? null,
|
|
1014
|
+
};
|
|
1015
|
+
this.mediaOwners.set(id, newMediaOwner);
|
|
1016
|
+
return newMediaOwner;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async updateMediaOwner(id: string, data: Partial<InsertMediaOwner>): Promise<MediaOwner | undefined> {
|
|
1020
|
+
const existing = this.mediaOwners.get(id);
|
|
1021
|
+
if (!existing) return undefined;
|
|
1022
|
+
const updated = { ...existing, ...data };
|
|
1023
|
+
this.mediaOwners.set(id, updated);
|
|
1024
|
+
return updated;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async deleteMediaOwner(id: string): Promise<boolean> {
|
|
1028
|
+
return this.mediaOwners.delete(id);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Venues
|
|
1032
|
+
async getVenues(): Promise<Venue[]> {
|
|
1033
|
+
return Array.from(this.venues.values());
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async getVenue(id: string): Promise<Venue | undefined> {
|
|
1037
|
+
return this.venues.get(id);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async createVenue(venue: InsertVenue): Promise<Venue> {
|
|
1041
|
+
const id = randomUUID();
|
|
1042
|
+
const newVenue: Venue = {
|
|
1043
|
+
id,
|
|
1044
|
+
name: venue.name,
|
|
1045
|
+
address: venue.address ?? null,
|
|
1046
|
+
city: venue.city ?? null,
|
|
1047
|
+
country: venue.country ?? null,
|
|
1048
|
+
venueType: venue.venueType,
|
|
1049
|
+
status: venue.status ?? "active",
|
|
1050
|
+
};
|
|
1051
|
+
this.venues.set(id, newVenue);
|
|
1052
|
+
return newVenue;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
async updateVenue(id: string, data: Partial<InsertVenue>): Promise<Venue | undefined> {
|
|
1056
|
+
const existing = this.venues.get(id);
|
|
1057
|
+
if (!existing) return undefined;
|
|
1058
|
+
const updated = { ...existing, ...data };
|
|
1059
|
+
this.venues.set(id, updated);
|
|
1060
|
+
return updated;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async deleteVenue(id: string): Promise<boolean> {
|
|
1064
|
+
return this.venues.delete(id);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Screens
|
|
1068
|
+
async getScreens(): Promise<Screen[]> {
|
|
1069
|
+
return Array.from(this.screens.values());
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async getScreen(id: string): Promise<Screen | undefined> {
|
|
1073
|
+
return this.screens.get(id);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async createScreen(screen: InsertScreen): Promise<Screen> {
|
|
1077
|
+
const id = randomUUID();
|
|
1078
|
+
const newScreen: Screen = {
|
|
1079
|
+
id,
|
|
1080
|
+
name: screen.name,
|
|
1081
|
+
venueId: screen.venueId ?? null,
|
|
1082
|
+
screenType: screen.screenType,
|
|
1083
|
+
width: screen.width,
|
|
1084
|
+
height: screen.height,
|
|
1085
|
+
orientation: screen.orientation ?? "landscape",
|
|
1086
|
+
status: screen.status ?? "active",
|
|
1087
|
+
dailyImpressions: screen.dailyImpressions ?? 0,
|
|
1088
|
+
cpm: screen.cpm ?? "10.00",
|
|
1089
|
+
};
|
|
1090
|
+
this.screens.set(id, newScreen);
|
|
1091
|
+
return newScreen;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
async updateScreen(id: string, data: Partial<InsertScreen>): Promise<Screen | undefined> {
|
|
1095
|
+
const existing = this.screens.get(id);
|
|
1096
|
+
if (!existing) return undefined;
|
|
1097
|
+
const updated = { ...existing, ...data };
|
|
1098
|
+
this.screens.set(id, updated);
|
|
1099
|
+
return updated;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async deleteScreen(id: string): Promise<boolean> {
|
|
1103
|
+
return this.screens.delete(id);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Deals
|
|
1107
|
+
async getDeals(): Promise<Deal[]> {
|
|
1108
|
+
return Array.from(this.deals.values());
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async getDeal(id: string): Promise<Deal | undefined> {
|
|
1112
|
+
return this.deals.get(id);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
async createDeal(deal: InsertDeal): Promise<Deal> {
|
|
1116
|
+
const id = randomUUID();
|
|
1117
|
+
const newDeal: Deal = {
|
|
1118
|
+
id,
|
|
1119
|
+
name: deal.name,
|
|
1120
|
+
advertiserId: deal.advertiserId ?? null,
|
|
1121
|
+
brandId: deal.brandId ?? null,
|
|
1122
|
+
agencyId: deal.agencyId ?? null,
|
|
1123
|
+
status: deal.status ?? "active",
|
|
1124
|
+
mediaType: deal.mediaType ?? "dooh",
|
|
1125
|
+
clientType: deal.clientType ?? "direct",
|
|
1126
|
+
sspId: deal.sspId ?? null,
|
|
1127
|
+
dealType: deal.dealType ?? "traditional",
|
|
1128
|
+
dspId: deal.dspId ?? null,
|
|
1129
|
+
seatNames: deal.seatNames ?? null,
|
|
1130
|
+
seatIds: deal.seatIds ?? null,
|
|
1131
|
+
countries: deal.countries ?? null,
|
|
1132
|
+
currency: deal.currency ?? "USD",
|
|
1133
|
+
timezone: deal.timezone ?? null,
|
|
1134
|
+
budget: deal.budget ?? null,
|
|
1135
|
+
spent: "0.00",
|
|
1136
|
+
goalType: deal.goalType ?? null,
|
|
1137
|
+
goalValue: deal.goalValue ?? null,
|
|
1138
|
+
targetType: deal.targetType ?? null,
|
|
1139
|
+
targetValue: deal.targetValue ?? null,
|
|
1140
|
+
hardStop: deal.hardStop ?? false,
|
|
1141
|
+
startDate: deal.startDate ?? null,
|
|
1142
|
+
endDate: deal.endDate ?? null,
|
|
1143
|
+
adPlayVerificationEnabled: deal.adPlayVerificationEnabled ?? false,
|
|
1144
|
+
adPlayVerificationProvider: deal.adPlayVerificationProvider ?? null,
|
|
1145
|
+
externalDealId: deal.externalDealId ?? null,
|
|
1146
|
+
source: deal.source ?? "influence",
|
|
1147
|
+
createdBy: deal.createdBy ?? null,
|
|
1148
|
+
createdAt: deal.createdAt ?? null,
|
|
1149
|
+
updatedAt: deal.updatedAt ?? null,
|
|
1150
|
+
};
|
|
1151
|
+
this.deals.set(id, newDeal);
|
|
1152
|
+
return newDeal;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async updateDeal(id: string, data: Partial<InsertDeal>): Promise<Deal | undefined> {
|
|
1156
|
+
const existing = this.deals.get(id);
|
|
1157
|
+
if (!existing) return undefined;
|
|
1158
|
+
const updated = { ...existing, ...data };
|
|
1159
|
+
this.deals.set(id, updated);
|
|
1160
|
+
return updated;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
async deleteDeal(id: string): Promise<boolean> {
|
|
1164
|
+
return this.deals.delete(id);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Line Items (now directly under Deals - 4-level hierarchy)
|
|
1168
|
+
async getLineItems(): Promise<LineItem[]> {
|
|
1169
|
+
return Array.from(this.lineItems.values());
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
async getLineItem(id: string): Promise<LineItem | undefined> {
|
|
1173
|
+
return this.lineItems.get(id);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async getLineItemsByDeal(dealId: string): Promise<LineItem[]> {
|
|
1177
|
+
return Array.from(this.lineItems.values()).filter(
|
|
1178
|
+
(li) => li.dealId === dealId
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async createLineItem(item: InsertLineItem): Promise<LineItem> {
|
|
1183
|
+
const id = randomUUID();
|
|
1184
|
+
const newItem: LineItem = {
|
|
1185
|
+
id,
|
|
1186
|
+
name: item.name,
|
|
1187
|
+
dealId: item.dealId ?? null,
|
|
1188
|
+
mediaOwnerId: item.mediaOwnerId ?? null,
|
|
1189
|
+
status: item.status ?? "draft",
|
|
1190
|
+
copiedFromId: item.copiedFromId ?? null,
|
|
1191
|
+
creativeType: item.creativeType ?? "display",
|
|
1192
|
+
priority: item.priority ?? 5,
|
|
1193
|
+
targeting: item.targeting ?? null,
|
|
1194
|
+
inventoryFilters: item.inventoryFilters ?? null,
|
|
1195
|
+
startDate: item.startDate ?? null,
|
|
1196
|
+
endDate: item.endDate ?? null,
|
|
1197
|
+
schedules: item.schedules ?? null,
|
|
1198
|
+
floorRateType: item.floorRateType ?? "cpm",
|
|
1199
|
+
floorRate: item.floorRate ?? null,
|
|
1200
|
+
budgetAllocation: item.budgetAllocation ?? null,
|
|
1201
|
+
pacing: item.pacing ?? "even",
|
|
1202
|
+
customFees: item.customFees ?? null,
|
|
1203
|
+
frequencyCap: item.frequencyCap ?? null,
|
|
1204
|
+
trafficAllocation: item.trafficAllocation ?? 100,
|
|
1205
|
+
targetImpressions: item.targetImpressions ?? null,
|
|
1206
|
+
deliveredImpressions: 0,
|
|
1207
|
+
budget: item.budget ?? null,
|
|
1208
|
+
spent: "0.00",
|
|
1209
|
+
cpm: item.cpm ?? null,
|
|
1210
|
+
creativeUrl: item.creativeUrl ?? null,
|
|
1211
|
+
creativeDuration: item.creativeDuration ?? 10,
|
|
1212
|
+
dspId: item.dspId ?? null,
|
|
1213
|
+
dspSeatId: item.dspSeatId ?? null,
|
|
1214
|
+
pushToDsp: item.pushToDsp ?? false,
|
|
1215
|
+
triggerId: item.triggerId ?? null,
|
|
1216
|
+
triggerEnabled: item.triggerEnabled ?? false,
|
|
1217
|
+
resolution: item.resolution ?? null,
|
|
1218
|
+
createdBy: item.createdBy ?? null,
|
|
1219
|
+
createdAt: item.createdAt ?? null,
|
|
1220
|
+
updatedAt: item.updatedAt ?? null,
|
|
1221
|
+
};
|
|
1222
|
+
this.lineItems.set(id, newItem);
|
|
1223
|
+
return newItem;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async updateLineItem(id: string, data: Partial<InsertLineItem>): Promise<LineItem | undefined> {
|
|
1227
|
+
const existing = this.lineItems.get(id);
|
|
1228
|
+
if (!existing) return undefined;
|
|
1229
|
+
const updated = { ...existing, ...data };
|
|
1230
|
+
this.lineItems.set(id, updated);
|
|
1231
|
+
return updated;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
async deleteLineItem(id: string): Promise<boolean> {
|
|
1235
|
+
return this.lineItems.delete(id);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// SSP Partners
|
|
1239
|
+
async getSspPartners(): Promise<SspPartner[]> {
|
|
1240
|
+
return Array.from(this.sspPartners.values());
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async getSspPartner(id: string): Promise<SspPartner | undefined> {
|
|
1244
|
+
return this.sspPartners.get(id);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async createSspPartner(partner: InsertSspPartner): Promise<SspPartner> {
|
|
1248
|
+
const id = randomUUID();
|
|
1249
|
+
const newPartner: SspPartner = {
|
|
1250
|
+
id,
|
|
1251
|
+
name: partner.name,
|
|
1252
|
+
endpoint: partner.endpoint ?? null,
|
|
1253
|
+
apiKey: partner.apiKey ?? null,
|
|
1254
|
+
status: partner.status ?? "active",
|
|
1255
|
+
partnerType: partner.partnerType ?? "ssp",
|
|
1256
|
+
};
|
|
1257
|
+
this.sspPartners.set(id, newPartner);
|
|
1258
|
+
return newPartner;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async updateSspPartner(id: string, data: Partial<InsertSspPartner>): Promise<SspPartner | undefined> {
|
|
1262
|
+
const existing = this.sspPartners.get(id);
|
|
1263
|
+
if (!existing) return undefined;
|
|
1264
|
+
const updated = { ...existing, ...data };
|
|
1265
|
+
this.sspPartners.set(id, updated);
|
|
1266
|
+
return updated;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
async deleteSspPartner(id: string): Promise<boolean> {
|
|
1270
|
+
return this.sspPartners.delete(id);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// DSP Partners
|
|
1274
|
+
async getDspPartners(): Promise<DspPartner[]> {
|
|
1275
|
+
return Array.from(this.dspPartners.values());
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async getDspPartner(id: string): Promise<DspPartner | undefined> {
|
|
1279
|
+
return this.dspPartners.get(id);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async createDspPartner(partner: InsertDspPartner): Promise<DspPartner> {
|
|
1283
|
+
const id = randomUUID();
|
|
1284
|
+
const newPartner: DspPartner = {
|
|
1285
|
+
id,
|
|
1286
|
+
name: partner.name,
|
|
1287
|
+
endpoint: partner.endpoint ?? null,
|
|
1288
|
+
apiKey: partner.apiKey ?? null,
|
|
1289
|
+
status: partner.status ?? "active",
|
|
1290
|
+
};
|
|
1291
|
+
this.dspPartners.set(id, newPartner);
|
|
1292
|
+
return newPartner;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
async updateDspPartner(id: string, data: Partial<InsertDspPartner>): Promise<DspPartner | undefined> {
|
|
1296
|
+
const existing = this.dspPartners.get(id);
|
|
1297
|
+
if (!existing) return undefined;
|
|
1298
|
+
const updated = { ...existing, ...data };
|
|
1299
|
+
this.dspPartners.set(id, updated);
|
|
1300
|
+
return updated;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async deleteDspPartner(id: string): Promise<boolean> {
|
|
1304
|
+
return this.dspPartners.delete(id);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Dashboard Metrics
|
|
1308
|
+
async getDashboardMetrics(): Promise<DashboardMetrics> {
|
|
1309
|
+
const deals = Array.from(this.deals.values());
|
|
1310
|
+
const screens = Array.from(this.screens.values());
|
|
1311
|
+
const lineItems = Array.from(this.lineItems.values());
|
|
1312
|
+
const creatives = Array.from(this.creatives.values());
|
|
1313
|
+
const lineItemCreatives = Array.from(this.lineItemCreatives.values());
|
|
1314
|
+
|
|
1315
|
+
const totalImpressions = lineItems.reduce((sum, li) => sum + (li.deliveredImpressions ?? 0), 0);
|
|
1316
|
+
const totalRevenue = lineItems.reduce((sum, li) => sum + parseFloat(li.spent ?? "0"), 0);
|
|
1317
|
+
const activeDeals = deals.filter((d) => d.status === "active").length;
|
|
1318
|
+
const activeScreens = screens.filter((s) => s.status === "active").length;
|
|
1319
|
+
const totalCPM = lineItems.reduce((sum, li) => sum + parseFloat(li.cpm ?? "0"), 0);
|
|
1320
|
+
const avgCPM = lineItems.length > 0 ? totalCPM / lineItems.length : 0;
|
|
1321
|
+
|
|
1322
|
+
// Line Item metrics (now directly under deals - 4-level hierarchy)
|
|
1323
|
+
const activeLineItems = lineItems.filter((li) => li.status === "active").length;
|
|
1324
|
+
|
|
1325
|
+
// Creative metrics
|
|
1326
|
+
const pendingApprovalCreatives = creatives.filter((c) => c.status === "pending" || c.status === "new" || c.status === "processing").length;
|
|
1327
|
+
const acceptedCreatives = creatives.filter((c) => c.status === "accepted").length;
|
|
1328
|
+
const assignedCreativeIds = new Set(lineItemCreatives.map(lic => lic.creativeId));
|
|
1329
|
+
const assignedCreatives = assignedCreativeIds.size;
|
|
1330
|
+
|
|
1331
|
+
return {
|
|
1332
|
+
totalDeals: deals.length,
|
|
1333
|
+
activeDeals,
|
|
1334
|
+
totalImpressions,
|
|
1335
|
+
totalRevenue,
|
|
1336
|
+
totalScreens: screens.length,
|
|
1337
|
+
activeScreens,
|
|
1338
|
+
fillRate: 87,
|
|
1339
|
+
avgCPM,
|
|
1340
|
+
// Line Item metrics
|
|
1341
|
+
totalLineItems: lineItems.length,
|
|
1342
|
+
activeLineItems,
|
|
1343
|
+
// Creative metrics
|
|
1344
|
+
totalCreatives: creatives.length,
|
|
1345
|
+
pendingApprovalCreatives,
|
|
1346
|
+
acceptedCreatives,
|
|
1347
|
+
assignedCreatives,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Traffic Allocation (single-level - directly on line items within a deal)
|
|
1352
|
+
async getDealTrafficSummary(dealId: string): Promise<{
|
|
1353
|
+
dealId: string;
|
|
1354
|
+
totalLineItems: number;
|
|
1355
|
+
totalAllocation: number;
|
|
1356
|
+
normalizedAllocations: { lineItemId: string; lineItemName: string; allocation: number; priority: number; normalizedShare: number }[];
|
|
1357
|
+
}> {
|
|
1358
|
+
const lineItems = await this.getLineItemsByDeal(dealId);
|
|
1359
|
+
const totalAllocation = lineItems.reduce((sum, li) => sum + (li.trafficAllocation ?? 100), 0);
|
|
1360
|
+
|
|
1361
|
+
const normalizedAllocations = lineItems.map((li) => {
|
|
1362
|
+
const allocation = li.trafficAllocation ?? 100;
|
|
1363
|
+
const priority = li.priority ?? 5;
|
|
1364
|
+
const normalizedShare = totalAllocation > 0 ? (allocation / totalAllocation) * 100 : 0;
|
|
1365
|
+
return {
|
|
1366
|
+
lineItemId: li.id,
|
|
1367
|
+
lineItemName: li.name,
|
|
1368
|
+
allocation,
|
|
1369
|
+
priority,
|
|
1370
|
+
normalizedShare: Math.round(normalizedShare * 100) / 100,
|
|
1371
|
+
};
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
return {
|
|
1375
|
+
dealId,
|
|
1376
|
+
totalLineItems: lineItems.length,
|
|
1377
|
+
totalAllocation,
|
|
1378
|
+
normalizedAllocations,
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Creative Folders
|
|
1383
|
+
async getCreativeFolders(): Promise<CreativeFolder[]> {
|
|
1384
|
+
return Array.from(this.creativeFolders.values());
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async getCreativeFolder(id: string): Promise<CreativeFolder | undefined> {
|
|
1388
|
+
return this.creativeFolders.get(id);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async createCreativeFolder(folder: InsertCreativeFolder): Promise<CreativeFolder> {
|
|
1392
|
+
const id = randomUUID();
|
|
1393
|
+
const newFolder: CreativeFolder = {
|
|
1394
|
+
id,
|
|
1395
|
+
name: folder.name,
|
|
1396
|
+
createdAt: folder.createdAt ?? new Date().toISOString().split('T')[0],
|
|
1397
|
+
};
|
|
1398
|
+
this.creativeFolders.set(id, newFolder);
|
|
1399
|
+
return newFolder;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
async deleteCreativeFolder(id: string): Promise<boolean> {
|
|
1403
|
+
return this.creativeFolders.delete(id);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Creatives
|
|
1407
|
+
async getCreatives(filters?: { folderId?: string; type?: string; status?: string; search?: string }): Promise<Creative[]> {
|
|
1408
|
+
let creatives = Array.from(this.creatives.values());
|
|
1409
|
+
|
|
1410
|
+
if (filters) {
|
|
1411
|
+
if (filters.folderId) {
|
|
1412
|
+
creatives = creatives.filter(c => c.folderId === filters.folderId);
|
|
1413
|
+
}
|
|
1414
|
+
if (filters.type) {
|
|
1415
|
+
creatives = creatives.filter(c => c.type === filters.type);
|
|
1416
|
+
}
|
|
1417
|
+
if (filters.status) {
|
|
1418
|
+
creatives = creatives.filter(c => c.status === filters.status);
|
|
1419
|
+
}
|
|
1420
|
+
if (filters.search) {
|
|
1421
|
+
const searchLower = filters.search.toLowerCase();
|
|
1422
|
+
creatives = creatives.filter(c =>
|
|
1423
|
+
c.name.toLowerCase().includes(searchLower) ||
|
|
1424
|
+
(c.tags && c.tags.some(tag => tag.toLowerCase().includes(searchLower)))
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return creatives;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async getCreative(id: string): Promise<Creative | undefined> {
|
|
1433
|
+
return this.creatives.get(id);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async createCreative(creative: InsertCreative): Promise<Creative> {
|
|
1437
|
+
const id = randomUUID();
|
|
1438
|
+
const newCreative: Creative = {
|
|
1439
|
+
id,
|
|
1440
|
+
name: creative.name,
|
|
1441
|
+
type: creative.type ?? "display",
|
|
1442
|
+
format: creative.format,
|
|
1443
|
+
width: creative.width,
|
|
1444
|
+
height: creative.height,
|
|
1445
|
+
fileSize: creative.fileSize,
|
|
1446
|
+
duration: creative.duration ?? null,
|
|
1447
|
+
status: creative.status ?? "new",
|
|
1448
|
+
folderId: creative.folderId ?? null,
|
|
1449
|
+
tags: creative.tags ?? null,
|
|
1450
|
+
uploadedAt: creative.uploadedAt ?? new Date().toISOString().split('T')[0],
|
|
1451
|
+
uploadedBy: creative.uploadedBy ?? null,
|
|
1452
|
+
thumbnailUrl: creative.thumbnailUrl ?? null,
|
|
1453
|
+
fileUrl: creative.fileUrl ?? null,
|
|
1454
|
+
inadequateReason: creative.inadequateReason ?? null,
|
|
1455
|
+
};
|
|
1456
|
+
this.creatives.set(id, newCreative);
|
|
1457
|
+
return newCreative;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
async updateCreative(id: string, data: Partial<InsertCreative>): Promise<Creative | undefined> {
|
|
1461
|
+
const existing = this.creatives.get(id);
|
|
1462
|
+
if (!existing) return undefined;
|
|
1463
|
+
const updated = { ...existing, ...data };
|
|
1464
|
+
this.creatives.set(id, updated);
|
|
1465
|
+
return updated;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async deleteCreative(id: string): Promise<boolean> {
|
|
1469
|
+
return this.creatives.delete(id);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Line Item Creative Assignments
|
|
1473
|
+
async getAllLineItemCreatives(): Promise<LineItemCreative[]> {
|
|
1474
|
+
return Array.from(this.lineItemCreatives.values());
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async getLineItemCreatives(lineItemId: string): Promise<LineItemCreative[]> {
|
|
1478
|
+
const assignments: LineItemCreative[] = [];
|
|
1479
|
+
Array.from(this.lineItemCreatives.values()).forEach(assignment => {
|
|
1480
|
+
if (assignment.lineItemId === lineItemId) {
|
|
1481
|
+
assignments.push(assignment);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
return assignments.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0));
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
async getLineItemCreative(id: string): Promise<LineItemCreative | undefined> {
|
|
1488
|
+
return this.lineItemCreatives.get(id);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async getLineItemCreativeById(id: string): Promise<(LineItemCreative & { creative: Creative }) | undefined> {
|
|
1492
|
+
const assignment = this.lineItemCreatives.get(id);
|
|
1493
|
+
if (!assignment) return undefined;
|
|
1494
|
+
const creative = this.creatives.get(assignment.creativeId);
|
|
1495
|
+
if (!creative) return undefined;
|
|
1496
|
+
return { ...assignment, creative };
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async getLineItemCreativeWithDetails(lineItemId: string): Promise<(LineItemCreative & { creative: Creative })[]> {
|
|
1500
|
+
const assignments = await this.getLineItemCreatives(lineItemId);
|
|
1501
|
+
const result: (LineItemCreative & { creative: Creative })[] = [];
|
|
1502
|
+
|
|
1503
|
+
for (const assignment of assignments) {
|
|
1504
|
+
const creative = this.creatives.get(assignment.creativeId);
|
|
1505
|
+
if (creative) {
|
|
1506
|
+
result.push({ ...assignment, creative });
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
return result;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async assignCreativeToLineItem(assignment: InsertLineItemCreative): Promise<LineItemCreative> {
|
|
1514
|
+
const id = randomUUID();
|
|
1515
|
+
const newAssignment: LineItemCreative = {
|
|
1516
|
+
id,
|
|
1517
|
+
lineItemId: assignment.lineItemId,
|
|
1518
|
+
creativeId: assignment.creativeId,
|
|
1519
|
+
tier2Status: assignment.tier2Status ?? "pending",
|
|
1520
|
+
tier2ReviewedBy: assignment.tier2ReviewedBy ?? null,
|
|
1521
|
+
tier2ReviewedAt: assignment.tier2ReviewedAt ?? null,
|
|
1522
|
+
tier2RejectionReason: assignment.tier2RejectionReason ?? null,
|
|
1523
|
+
assignedBy: assignment.assignedBy ?? null,
|
|
1524
|
+
assignedAt: assignment.assignedAt ?? new Date().toISOString(),
|
|
1525
|
+
displayOrder: assignment.displayOrder ?? 0,
|
|
1526
|
+
weight: assignment.weight ?? 100,
|
|
1527
|
+
};
|
|
1528
|
+
this.lineItemCreatives.set(id, newAssignment);
|
|
1529
|
+
return newAssignment;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
async removeCreativeFromLineItem(lineItemId: string, creativeId: string): Promise<boolean> {
|
|
1533
|
+
const entries = Array.from(this.lineItemCreatives.entries());
|
|
1534
|
+
for (const [id, assignment] of entries) {
|
|
1535
|
+
if (assignment.lineItemId === lineItemId && assignment.creativeId === creativeId) {
|
|
1536
|
+
return this.lineItemCreatives.delete(id);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async updateLineItemCreative(id: string, data: Partial<InsertLineItemCreative>): Promise<LineItemCreative | undefined> {
|
|
1543
|
+
const existing = this.lineItemCreatives.get(id);
|
|
1544
|
+
if (!existing) return undefined;
|
|
1545
|
+
const updated = { ...existing, ...data };
|
|
1546
|
+
this.lineItemCreatives.set(id, updated);
|
|
1547
|
+
return updated;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
async getAssignableCreatives(): Promise<Creative[]> {
|
|
1551
|
+
const creatives: Creative[] = [];
|
|
1552
|
+
Array.from(this.creatives.values()).forEach(creative => {
|
|
1553
|
+
if (creative.status === "accepted") {
|
|
1554
|
+
creatives.push(creative);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
return creatives;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Change Logs (Audit Trail) Methods
|
|
1561
|
+
async getChangeLogs(entityType: ChangeLogEntityType, entityId: string): Promise<ChangeLog[]> {
|
|
1562
|
+
const logs = Array.from(this.changeLogs.values())
|
|
1563
|
+
.filter(log => log.entityType === entityType && log.entityId === entityId)
|
|
1564
|
+
.sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime());
|
|
1565
|
+
return logs;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
async createChangeLog(log: InsertChangeLog): Promise<ChangeLog> {
|
|
1569
|
+
const id = randomUUID();
|
|
1570
|
+
const newLog: ChangeLog = {
|
|
1571
|
+
id,
|
|
1572
|
+
entityType: log.entityType,
|
|
1573
|
+
entityId: log.entityId,
|
|
1574
|
+
action: log.action,
|
|
1575
|
+
changedBy: log.changedBy,
|
|
1576
|
+
changedAt: log.changedAt,
|
|
1577
|
+
changes: log.changes ?? null,
|
|
1578
|
+
description: log.description ?? null,
|
|
1579
|
+
};
|
|
1580
|
+
this.changeLogs.set(id, newLog);
|
|
1581
|
+
return newLog;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
async getRecentChangeLogs(limit: number = 50): Promise<ChangeLog[]> {
|
|
1585
|
+
const logs = Array.from(this.changeLogs.values())
|
|
1586
|
+
.sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime())
|
|
1587
|
+
.slice(0, limit);
|
|
1588
|
+
return logs;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
async getHierarchicalChangeLogs(entityType: ChangeLogEntityType, entityId: string): Promise<ChangeLog[]> {
|
|
1592
|
+
const allLogs: ChangeLog[] = [];
|
|
1593
|
+
|
|
1594
|
+
if (entityType === "deal") {
|
|
1595
|
+
const dealLogs = Array.from(this.changeLogs.values()).filter(
|
|
1596
|
+
log => log.entityType === "deal" && log.entityId === entityId
|
|
1597
|
+
);
|
|
1598
|
+
allLogs.push(...dealLogs);
|
|
1599
|
+
|
|
1600
|
+
const orders = Array.from(this.insertionOrders.values()).filter(
|
|
1601
|
+
order => order.dealId === entityId
|
|
1602
|
+
);
|
|
1603
|
+
for (const order of orders) {
|
|
1604
|
+
const orderLogs = Array.from(this.changeLogs.values()).filter(
|
|
1605
|
+
log => log.entityType === "order" && log.entityId === order.id
|
|
1606
|
+
);
|
|
1607
|
+
allLogs.push(...orderLogs);
|
|
1608
|
+
|
|
1609
|
+
const lineItems = Array.from(this.lineItems.values()).filter(
|
|
1610
|
+
li => li.insertionOrderId === order.id
|
|
1611
|
+
);
|
|
1612
|
+
for (const lineItem of lineItems) {
|
|
1613
|
+
const liLogs = Array.from(this.changeLogs.values()).filter(
|
|
1614
|
+
log => log.entityType === "line_item" && log.entityId === lineItem.id
|
|
1615
|
+
);
|
|
1616
|
+
allLogs.push(...liLogs);
|
|
1617
|
+
|
|
1618
|
+
const assignments = Array.from(this.lineItemCreatives.values()).filter(
|
|
1619
|
+
lic => lic.lineItemId === lineItem.id
|
|
1620
|
+
);
|
|
1621
|
+
for (const assignment of assignments) {
|
|
1622
|
+
const creativeLogs = Array.from(this.changeLogs.values()).filter(
|
|
1623
|
+
log => log.entityType === "creative" && log.entityId === assignment.creativeId
|
|
1624
|
+
);
|
|
1625
|
+
allLogs.push(...creativeLogs);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
} else if (entityType === "order") {
|
|
1630
|
+
const orderLogs = Array.from(this.changeLogs.values()).filter(
|
|
1631
|
+
log => log.entityType === "order" && log.entityId === entityId
|
|
1632
|
+
);
|
|
1633
|
+
allLogs.push(...orderLogs);
|
|
1634
|
+
|
|
1635
|
+
const lineItems = Array.from(this.lineItems.values()).filter(
|
|
1636
|
+
li => li.insertionOrderId === entityId
|
|
1637
|
+
);
|
|
1638
|
+
for (const lineItem of lineItems) {
|
|
1639
|
+
const liLogs = Array.from(this.changeLogs.values()).filter(
|
|
1640
|
+
log => log.entityType === "line_item" && log.entityId === lineItem.id
|
|
1641
|
+
);
|
|
1642
|
+
allLogs.push(...liLogs);
|
|
1643
|
+
|
|
1644
|
+
const assignments = Array.from(this.lineItemCreatives.values()).filter(
|
|
1645
|
+
lic => lic.lineItemId === lineItem.id
|
|
1646
|
+
);
|
|
1647
|
+
for (const assignment of assignments) {
|
|
1648
|
+
const creativeLogs = Array.from(this.changeLogs.values()).filter(
|
|
1649
|
+
log => log.entityType === "creative" && log.entityId === assignment.creativeId
|
|
1650
|
+
);
|
|
1651
|
+
allLogs.push(...creativeLogs);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
} else if (entityType === "line_item") {
|
|
1655
|
+
const liLogs = Array.from(this.changeLogs.values()).filter(
|
|
1656
|
+
log => log.entityType === "line_item" && log.entityId === entityId
|
|
1657
|
+
);
|
|
1658
|
+
allLogs.push(...liLogs);
|
|
1659
|
+
|
|
1660
|
+
const assignments = Array.from(this.lineItemCreatives.values()).filter(
|
|
1661
|
+
lic => lic.lineItemId === entityId
|
|
1662
|
+
);
|
|
1663
|
+
for (const assignment of assignments) {
|
|
1664
|
+
const creativeLogs = Array.from(this.changeLogs.values()).filter(
|
|
1665
|
+
log => log.entityType === "creative" && log.entityId === assignment.creativeId
|
|
1666
|
+
);
|
|
1667
|
+
allLogs.push(...creativeLogs);
|
|
1668
|
+
}
|
|
1669
|
+
} else {
|
|
1670
|
+
const creativeLogs = Array.from(this.changeLogs.values()).filter(
|
|
1671
|
+
log => log.entityType === "creative" && log.entityId === entityId
|
|
1672
|
+
);
|
|
1673
|
+
allLogs.push(...creativeLogs);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return allLogs.sort((a, b) => new Date(b.changedAt).getTime() - new Date(a.changedAt).getTime());
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Signals
|
|
1680
|
+
async getSignals(): Promise<Signal[]> {
|
|
1681
|
+
return Array.from(this.signals.values());
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
async getSignal(id: string): Promise<Signal | undefined> {
|
|
1685
|
+
return this.signals.get(id);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
async createSignal(data: InsertSignal): Promise<Signal> {
|
|
1689
|
+
const id = randomUUID();
|
|
1690
|
+
const now = new Date().toISOString();
|
|
1691
|
+
const newSignal: Signal = {
|
|
1692
|
+
id,
|
|
1693
|
+
name: data.name,
|
|
1694
|
+
description: data.description ?? null,
|
|
1695
|
+
signalType: data.signalType ?? "weather",
|
|
1696
|
+
status: data.status ?? "active",
|
|
1697
|
+
createdBy: data.createdBy ?? null,
|
|
1698
|
+
createdAt: data.createdAt ?? now,
|
|
1699
|
+
updatedAt: data.updatedAt ?? null,
|
|
1700
|
+
};
|
|
1701
|
+
this.signals.set(id, newSignal);
|
|
1702
|
+
return newSignal;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async updateSignal(id: string, data: Partial<InsertSignal>): Promise<Signal | undefined> {
|
|
1706
|
+
const existing = this.signals.get(id);
|
|
1707
|
+
if (!existing) return undefined;
|
|
1708
|
+
const updated = {
|
|
1709
|
+
...existing,
|
|
1710
|
+
...data,
|
|
1711
|
+
updatedAt: new Date().toISOString()
|
|
1712
|
+
};
|
|
1713
|
+
this.signals.set(id, updated);
|
|
1714
|
+
return updated;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
async deleteSignal(id: string): Promise<boolean> {
|
|
1718
|
+
const entries = Array.from(this.signalRules.entries());
|
|
1719
|
+
for (const [ruleId, rule] of entries) {
|
|
1720
|
+
if (rule.signalId === id) {
|
|
1721
|
+
this.signalRules.delete(ruleId);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return this.signals.delete(id);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Signal Rules
|
|
1728
|
+
async getSignalRules(signalId: string): Promise<SignalRule[]> {
|
|
1729
|
+
return Array.from(this.signalRules.values()).filter(
|
|
1730
|
+
(rule) => rule.signalId === signalId
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async getSignalRule(id: string): Promise<SignalRule | undefined> {
|
|
1735
|
+
return this.signalRules.get(id);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async createSignalRule(data: InsertSignalRule): Promise<SignalRule> {
|
|
1739
|
+
const id = randomUUID();
|
|
1740
|
+
const now = new Date().toISOString();
|
|
1741
|
+
const newRule: SignalRule = {
|
|
1742
|
+
id,
|
|
1743
|
+
signalId: data.signalId,
|
|
1744
|
+
name: data.name,
|
|
1745
|
+
conditionType: data.conditionType,
|
|
1746
|
+
parameters: data.parameters ?? null,
|
|
1747
|
+
status: data.status ?? "active",
|
|
1748
|
+
createdAt: data.createdAt ?? now,
|
|
1749
|
+
updatedAt: data.updatedAt ?? null,
|
|
1750
|
+
};
|
|
1751
|
+
this.signalRules.set(id, newRule);
|
|
1752
|
+
return newRule;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
async updateSignalRule(id: string, data: Partial<InsertSignalRule>): Promise<SignalRule | undefined> {
|
|
1756
|
+
const existing = this.signalRules.get(id);
|
|
1757
|
+
if (!existing) return undefined;
|
|
1758
|
+
const updated = {
|
|
1759
|
+
...existing,
|
|
1760
|
+
...data,
|
|
1761
|
+
updatedAt: new Date().toISOString()
|
|
1762
|
+
};
|
|
1763
|
+
this.signalRules.set(id, updated);
|
|
1764
|
+
return updated;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
async deleteSignalRule(id: string): Promise<boolean> {
|
|
1768
|
+
return this.signalRules.delete(id);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// POIs
|
|
1772
|
+
async getPOIs(country?: string): Promise<POI[]> {
|
|
1773
|
+
const allPOIs = Array.from(this.pois.values());
|
|
1774
|
+
if (country) {
|
|
1775
|
+
return allPOIs.filter(poi => poi.country === country);
|
|
1776
|
+
}
|
|
1777
|
+
return allPOIs;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async getPOI(id: string): Promise<POI | undefined> {
|
|
1781
|
+
return this.pois.get(id);
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// Player Status
|
|
1785
|
+
async getPlayerStatuses(screenIds?: string[]): Promise<PlayerStatus[]> {
|
|
1786
|
+
const allStatuses = Array.from(this.playerStatuses.values());
|
|
1787
|
+
if (screenIds && screenIds.length > 0) {
|
|
1788
|
+
return allStatuses.filter(status => screenIds.includes(status.screenId));
|
|
1789
|
+
}
|
|
1790
|
+
return allStatuses;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
async getPlayerStatus(screenId: string): Promise<PlayerStatus | undefined> {
|
|
1794
|
+
return Array.from(this.playerStatuses.values()).find(s => s.screenId === screenId);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
async upsertPlayerStatus(data: InsertPlayerStatus): Promise<PlayerStatus> {
|
|
1798
|
+
const existing = await this.getPlayerStatus(data.screenId);
|
|
1799
|
+
if (existing) {
|
|
1800
|
+
const updated: PlayerStatus = {
|
|
1801
|
+
...existing,
|
|
1802
|
+
...data,
|
|
1803
|
+
lastUpdated: new Date().toISOString(),
|
|
1804
|
+
};
|
|
1805
|
+
this.playerStatuses.set(existing.id, updated);
|
|
1806
|
+
return updated;
|
|
1807
|
+
}
|
|
1808
|
+
const id = randomUUID();
|
|
1809
|
+
const newStatus: PlayerStatus = {
|
|
1810
|
+
id,
|
|
1811
|
+
screenId: data.screenId,
|
|
1812
|
+
playerId: data.playerId ?? null,
|
|
1813
|
+
cmsType: data.cmsType,
|
|
1814
|
+
status: data.status ?? "unknown",
|
|
1815
|
+
lastHeartbeat: data.lastHeartbeat ?? null,
|
|
1816
|
+
currentContent: data.currentContent ?? null,
|
|
1817
|
+
connectionQuality: data.connectionQuality ?? null,
|
|
1818
|
+
lastUpdated: new Date().toISOString(),
|
|
1819
|
+
};
|
|
1820
|
+
this.playerStatuses.set(id, newStatus);
|
|
1821
|
+
return newStatus;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Proof of Play
|
|
1825
|
+
async getProofOfPlayRecords(filters?: {
|
|
1826
|
+
dealId?: string;
|
|
1827
|
+
lineItemId?: string;
|
|
1828
|
+
creativeId?: string;
|
|
1829
|
+
mediaOwnerId?: string;
|
|
1830
|
+
source?: string;
|
|
1831
|
+
status?: string;
|
|
1832
|
+
startDate?: string;
|
|
1833
|
+
endDate?: string;
|
|
1834
|
+
}): Promise<ProofOfPlay[]> {
|
|
1835
|
+
let records = Array.from(this.proofOfPlayRecords.values());
|
|
1836
|
+
if (filters) {
|
|
1837
|
+
if (filters.dealId) {
|
|
1838
|
+
records = records.filter(r => r.dealId === filters.dealId);
|
|
1839
|
+
}
|
|
1840
|
+
if (filters.lineItemId) {
|
|
1841
|
+
records = records.filter(r => r.lineItemId === filters.lineItemId);
|
|
1842
|
+
}
|
|
1843
|
+
if (filters.creativeId) {
|
|
1844
|
+
records = records.filter(r => r.creativeId === filters.creativeId);
|
|
1845
|
+
}
|
|
1846
|
+
if (filters.mediaOwnerId) {
|
|
1847
|
+
records = records.filter(r => r.mediaOwnerId === filters.mediaOwnerId);
|
|
1848
|
+
}
|
|
1849
|
+
if (filters.source) {
|
|
1850
|
+
records = records.filter(r => r.source === filters.source);
|
|
1851
|
+
}
|
|
1852
|
+
if (filters.status) {
|
|
1853
|
+
records = records.filter(r => r.status === filters.status);
|
|
1854
|
+
}
|
|
1855
|
+
if (filters.startDate) {
|
|
1856
|
+
records = records.filter(r => r.startTimestamp >= filters.startDate!);
|
|
1857
|
+
}
|
|
1858
|
+
if (filters.endDate) {
|
|
1859
|
+
records = records.filter(r => r.endTimestamp <= filters.endDate!);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return records.sort((a, b) => (b.uploadedAt || '').localeCompare(a.uploadedAt || ''));
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
async getProofOfPlayRecord(id: string): Promise<ProofOfPlay | undefined> {
|
|
1866
|
+
return this.proofOfPlayRecords.get(id);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
async createProofOfPlayRecord(data: InsertProofOfPlay): Promise<ProofOfPlay> {
|
|
1870
|
+
const id = randomUUID();
|
|
1871
|
+
const now = new Date().toISOString();
|
|
1872
|
+
const newRecord: ProofOfPlay = {
|
|
1873
|
+
id,
|
|
1874
|
+
screenId: data.screenId ?? null,
|
|
1875
|
+
playerId: data.playerId ?? null,
|
|
1876
|
+
creativeId: data.creativeId ?? null,
|
|
1877
|
+
dealId: data.dealId ?? null,
|
|
1878
|
+
lineItemId: data.lineItemId ?? null,
|
|
1879
|
+
startTimestamp: data.startTimestamp,
|
|
1880
|
+
endTimestamp: data.endTimestamp,
|
|
1881
|
+
durationSeconds: data.durationSeconds,
|
|
1882
|
+
impressionCount: data.impressionCount ?? 1,
|
|
1883
|
+
mediaFileUrl: data.mediaFileUrl ?? null,
|
|
1884
|
+
proofImageUrl: data.proofImageUrl ?? null,
|
|
1885
|
+
proofVideoUrl: data.proofVideoUrl ?? null,
|
|
1886
|
+
source: data.source ?? "cms",
|
|
1887
|
+
status: data.status ?? "pending",
|
|
1888
|
+
errorMessage: data.errorMessage ?? null,
|
|
1889
|
+
mediaOwnerId: data.mediaOwnerId ?? null,
|
|
1890
|
+
uploadedBy: data.uploadedBy ?? null,
|
|
1891
|
+
uploadedAt: data.uploadedAt ?? now,
|
|
1892
|
+
processedAt: data.processedAt ?? null,
|
|
1893
|
+
syncedToMeasure: data.syncedToMeasure ?? false,
|
|
1894
|
+
syncedAt: data.syncedAt ?? null,
|
|
1895
|
+
};
|
|
1896
|
+
this.proofOfPlayRecords.set(id, newRecord);
|
|
1897
|
+
return newRecord;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
async updateProofOfPlayRecord(id: string, data: Partial<InsertProofOfPlay>): Promise<ProofOfPlay | undefined> {
|
|
1901
|
+
const existing = this.proofOfPlayRecords.get(id);
|
|
1902
|
+
if (!existing) return undefined;
|
|
1903
|
+
const updated: ProofOfPlay = {
|
|
1904
|
+
...existing,
|
|
1905
|
+
...data,
|
|
1906
|
+
};
|
|
1907
|
+
this.proofOfPlayRecords.set(id, updated);
|
|
1908
|
+
return updated;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
async deleteProofOfPlayRecord(id: string): Promise<boolean> {
|
|
1912
|
+
return this.proofOfPlayRecords.delete(id);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
// Playlog Uploads
|
|
1916
|
+
async getPlaylogUploads(filters?: {
|
|
1917
|
+
dealId?: string;
|
|
1918
|
+
lineItemId?: string;
|
|
1919
|
+
mediaOwnerId?: string;
|
|
1920
|
+
status?: string;
|
|
1921
|
+
}): Promise<PlaylogUpload[]> {
|
|
1922
|
+
let uploads = Array.from(this.playlogUploads.values());
|
|
1923
|
+
if (filters) {
|
|
1924
|
+
if (filters.dealId) {
|
|
1925
|
+
uploads = uploads.filter(u => u.dealId === filters.dealId);
|
|
1926
|
+
}
|
|
1927
|
+
if (filters.lineItemId) {
|
|
1928
|
+
uploads = uploads.filter(u => u.lineItemId === filters.lineItemId);
|
|
1929
|
+
}
|
|
1930
|
+
if (filters.mediaOwnerId) {
|
|
1931
|
+
uploads = uploads.filter(u => u.mediaOwnerId === filters.mediaOwnerId);
|
|
1932
|
+
}
|
|
1933
|
+
if (filters.status) {
|
|
1934
|
+
uploads = uploads.filter(u => u.status === filters.status);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return uploads.sort((a, b) => b.uploadedAt.localeCompare(a.uploadedAt));
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
async getPlaylogUpload(id: string): Promise<PlaylogUpload | undefined> {
|
|
1941
|
+
return this.playlogUploads.get(id);
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
async createPlaylogUpload(data: InsertPlaylogUpload): Promise<PlaylogUpload> {
|
|
1945
|
+
const id = randomUUID();
|
|
1946
|
+
const newUpload: PlaylogUpload = {
|
|
1947
|
+
id,
|
|
1948
|
+
dealId: data.dealId ?? null,
|
|
1949
|
+
lineItemId: data.lineItemId ?? null,
|
|
1950
|
+
creativeId: data.creativeId ?? null,
|
|
1951
|
+
mediaOwnerId: data.mediaOwnerId,
|
|
1952
|
+
fileName: data.fileName,
|
|
1953
|
+
fileUrl: data.fileUrl,
|
|
1954
|
+
totalRecords: data.totalRecords ?? 0,
|
|
1955
|
+
validRecords: data.validRecords ?? 0,
|
|
1956
|
+
invalidRecords: data.invalidRecords ?? 0,
|
|
1957
|
+
status: data.status ?? "uploaded",
|
|
1958
|
+
validationErrors: data.validationErrors ?? null,
|
|
1959
|
+
proofMediaUrls: data.proofMediaUrls ?? null,
|
|
1960
|
+
uploadedBy: data.uploadedBy,
|
|
1961
|
+
uploadedAt: data.uploadedAt,
|
|
1962
|
+
processedAt: data.processedAt ?? null,
|
|
1963
|
+
notes: data.notes ?? null,
|
|
1964
|
+
};
|
|
1965
|
+
this.playlogUploads.set(id, newUpload);
|
|
1966
|
+
return newUpload;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
async updatePlaylogUpload(id: string, data: Partial<InsertPlaylogUpload>): Promise<PlaylogUpload | undefined> {
|
|
1970
|
+
const existing = this.playlogUploads.get(id);
|
|
1971
|
+
if (!existing) return undefined;
|
|
1972
|
+
const updated: PlaylogUpload = {
|
|
1973
|
+
...existing,
|
|
1974
|
+
...data,
|
|
1975
|
+
};
|
|
1976
|
+
this.playlogUploads.set(id, updated);
|
|
1977
|
+
return updated;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
async deletePlaylogUpload(id: string): Promise<boolean> {
|
|
1981
|
+
return this.playlogUploads.delete(id);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// Geotargeting Rules
|
|
1985
|
+
async getGeoTargetingRules(entityType: string, entityId: string): Promise<GeoTargetingRule[]> {
|
|
1986
|
+
return Array.from(this.geoTargetingRules.values()).filter(
|
|
1987
|
+
rule => rule.entityType === entityType && rule.entityId === entityId
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
async getGeoTargetingRule(id: string): Promise<GeoTargetingRule | undefined> {
|
|
1992
|
+
return this.geoTargetingRules.get(id);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
async createGeoTargetingRule(data: InsertGeoTargetingRule): Promise<GeoTargetingRule> {
|
|
1996
|
+
const id = randomUUID();
|
|
1997
|
+
const now = new Date().toISOString();
|
|
1998
|
+
const newRule: GeoTargetingRule = {
|
|
1999
|
+
id,
|
|
2000
|
+
entityType: data.entityType,
|
|
2001
|
+
entityId: data.entityId,
|
|
2002
|
+
mode: data.mode,
|
|
2003
|
+
name: data.name ?? null,
|
|
2004
|
+
locations: data.locations ?? null,
|
|
2005
|
+
shapes: data.shapes ?? null,
|
|
2006
|
+
poiCategories: data.poiCategories ?? null,
|
|
2007
|
+
poiLocations: data.poiLocations ?? null,
|
|
2008
|
+
createdAt: data.createdAt ?? now,
|
|
2009
|
+
updatedAt: data.updatedAt ?? now,
|
|
2010
|
+
};
|
|
2011
|
+
this.geoTargetingRules.set(id, newRule);
|
|
2012
|
+
return newRule;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
async updateGeoTargetingRule(id: string, data: Partial<InsertGeoTargetingRule>): Promise<GeoTargetingRule | undefined> {
|
|
2016
|
+
const existing = this.geoTargetingRules.get(id);
|
|
2017
|
+
if (!existing) return undefined;
|
|
2018
|
+
const updated: GeoTargetingRule = {
|
|
2019
|
+
...existing,
|
|
2020
|
+
...data,
|
|
2021
|
+
updatedAt: new Date().toISOString(),
|
|
2022
|
+
};
|
|
2023
|
+
this.geoTargetingRules.set(id, updated);
|
|
2024
|
+
return updated;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
async deleteGeoTargetingRule(id: string): Promise<boolean> {
|
|
2028
|
+
return this.geoTargetingRules.delete(id);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Transit Routes
|
|
2032
|
+
async getTransitRoutes(city?: string): Promise<TransitRoute[]> {
|
|
2033
|
+
const routes = Array.from(this.transitRoutes.values());
|
|
2034
|
+
if (city) {
|
|
2035
|
+
return routes.filter(r => r.city === city);
|
|
2036
|
+
}
|
|
2037
|
+
return routes;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
async getTransitRoute(id: string): Promise<TransitRoute | undefined> {
|
|
2041
|
+
return this.transitRoutes.get(id);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// Transit Zones
|
|
2045
|
+
async getTransitZones(city?: string): Promise<TransitZone[]> {
|
|
2046
|
+
const zones = Array.from(this.transitZones.values());
|
|
2047
|
+
if (city) {
|
|
2048
|
+
return zones.filter(z => z.city === city);
|
|
2049
|
+
}
|
|
2050
|
+
return zones;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
async getTransitZone(id: string): Promise<TransitZone | undefined> {
|
|
2054
|
+
return this.transitZones.get(id);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
export const storage = new MemStorage();
|