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,1207 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
2
|
+
import { useParams, useLocation } from "wouter";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Button, cn } from "@moving-walls/design-system";
|
|
5
|
+
import { Card, CardContent } from "@moving-walls/design-system";
|
|
6
|
+
import { Badge } from "@moving-walls/design-system";
|
|
7
|
+
import {
|
|
8
|
+
ArrowLeft,
|
|
9
|
+
Check,
|
|
10
|
+
ChevronLeft,
|
|
11
|
+
ChevronRight,
|
|
12
|
+
Loader2,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import { useToast } from "@/hooks/use-toast";
|
|
15
|
+
|
|
16
|
+
const ENABLE_POI_IN_API = true;
|
|
17
|
+
import {
|
|
18
|
+
InfluenceDealsAPI,
|
|
19
|
+
influenceDealsRequest,
|
|
20
|
+
generateExternalId,
|
|
21
|
+
formatAPIErrorForToast,
|
|
22
|
+
mapInventoryForAPI,
|
|
23
|
+
mapLineItemForAPI,
|
|
24
|
+
type Deal,
|
|
25
|
+
type LineItem,
|
|
26
|
+
} from "@/lib/influence-deals-api";
|
|
27
|
+
import { LineItemDetailsStep } from "@/components/line-items/steps/line-item-details-step";
|
|
28
|
+
import { TargetingStep } from "@/components/line-items/steps/targeting-step";
|
|
29
|
+
import { InventoryStep } from "@/components/line-items/steps/inventory-step";
|
|
30
|
+
import { ScheduleStep } from "@/components/line-items/steps/schedule-step";
|
|
31
|
+
import { CreativesStep } from "@/components/line-items/steps/creatives-step";
|
|
32
|
+
import { SummaryStep } from "@/components/line-items/steps/summary-step";
|
|
33
|
+
import { usePageTitle } from "@/hooks/use-page-title";
|
|
34
|
+
|
|
35
|
+
interface LineItemFormData {
|
|
36
|
+
details: {
|
|
37
|
+
name: string;
|
|
38
|
+
startDate: string;
|
|
39
|
+
endDate: string;
|
|
40
|
+
currency: string;
|
|
41
|
+
creativeType?: "DISPLAY" | "VIDEO" | "AUDIO";
|
|
42
|
+
duration?: number;
|
|
43
|
+
impressions?: number;
|
|
44
|
+
netCostPerDay?: number;
|
|
45
|
+
direct?: {
|
|
46
|
+
budgetSetup: {
|
|
47
|
+
budgetType: string;
|
|
48
|
+
budgetAmount?: number;
|
|
49
|
+
currency: string;
|
|
50
|
+
};
|
|
51
|
+
campaignGoal: {
|
|
52
|
+
type: string;
|
|
53
|
+
targetValue: number;
|
|
54
|
+
};
|
|
55
|
+
pacing?: {
|
|
56
|
+
type: string;
|
|
57
|
+
dailyCap?: number;
|
|
58
|
+
hourlyCap?: number;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
programmatic?: {
|
|
62
|
+
bidFloor?: number;
|
|
63
|
+
netCost?: number;
|
|
64
|
+
impressions?: number;
|
|
65
|
+
impMultiplier?: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
targeting: {
|
|
69
|
+
targeting?: {
|
|
70
|
+
demographics?: {
|
|
71
|
+
ageGroups?: string[];
|
|
72
|
+
genders?: string[];
|
|
73
|
+
incomeGroups?: string[];
|
|
74
|
+
interests?: string[];
|
|
75
|
+
audienceBehaviour?: string[];
|
|
76
|
+
};
|
|
77
|
+
venueTypes?: string[];
|
|
78
|
+
geofencing?: {
|
|
79
|
+
geometrics?: any[];
|
|
80
|
+
locations?: any[];
|
|
81
|
+
radiusKm?: number;
|
|
82
|
+
pois?: string[];
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
deliveryTargeting?: {
|
|
86
|
+
signals?: Record<string, any>;
|
|
87
|
+
};
|
|
88
|
+
recommendationRunId?: string;
|
|
89
|
+
};
|
|
90
|
+
inventory: {
|
|
91
|
+
inventories: string[];
|
|
92
|
+
inventoryObjects?: any[];
|
|
93
|
+
planning?: {
|
|
94
|
+
capacity?: {
|
|
95
|
+
campaignDays?: number;
|
|
96
|
+
available?: { slots?: number; playTimeSec?: number; maxImpressions?: number };
|
|
97
|
+
};
|
|
98
|
+
allocation?: { slots?: number; playTimeSec?: number; sov?: number; sot?: number };
|
|
99
|
+
estimates?: { impressions?: number; reach?: number; frequency?: number };
|
|
100
|
+
pricing?: { cpm?: number; estimatedCost?: number };
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
schedule: {
|
|
104
|
+
scheduleGrids?: Record<string, any>;
|
|
105
|
+
schedulePreset?: string;
|
|
106
|
+
scheduleSelection?: string;
|
|
107
|
+
scheduleData?: any;
|
|
108
|
+
apiSchedules?: any[];
|
|
109
|
+
};
|
|
110
|
+
creatives: Array<{
|
|
111
|
+
creativeId: string;
|
|
112
|
+
creativeUri: string;
|
|
113
|
+
creativeType: "DISPLAY" | "VIDEO" | "AUDIO";
|
|
114
|
+
resolution?: string;
|
|
115
|
+
thumbnail?: string;
|
|
116
|
+
mimeType?: string;
|
|
117
|
+
duration?: number;
|
|
118
|
+
inventoryIds?: string[];
|
|
119
|
+
metadata?: Record<string, any>;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const initialFormData: LineItemFormData = {
|
|
124
|
+
details: {
|
|
125
|
+
name: "",
|
|
126
|
+
startDate: "",
|
|
127
|
+
endDate: "",
|
|
128
|
+
currency: "USD",
|
|
129
|
+
direct: {
|
|
130
|
+
budgetSetup: {
|
|
131
|
+
budgetType: "TOTAL",
|
|
132
|
+
budgetAmount: undefined,
|
|
133
|
+
currency: "USD",
|
|
134
|
+
},
|
|
135
|
+
campaignGoal: {
|
|
136
|
+
type: "IMPRESSIONS",
|
|
137
|
+
targetValue: 0,
|
|
138
|
+
},
|
|
139
|
+
pacing: {
|
|
140
|
+
type: "even",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
programmatic: {
|
|
144
|
+
bidFloor: undefined,
|
|
145
|
+
netCost: undefined,
|
|
146
|
+
impressions: undefined,
|
|
147
|
+
impMultiplier: undefined,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
targeting: {},
|
|
151
|
+
inventory: {
|
|
152
|
+
inventories: [],
|
|
153
|
+
inventoryObjects: [],
|
|
154
|
+
planning: undefined,
|
|
155
|
+
},
|
|
156
|
+
schedule: {
|
|
157
|
+
scheduleGrids: {},
|
|
158
|
+
schedulePreset: "custom",
|
|
159
|
+
},
|
|
160
|
+
creatives: [],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
interface Step {
|
|
164
|
+
id: number;
|
|
165
|
+
key: keyof LineItemFormData | "creatives" | "summary";
|
|
166
|
+
label: string;
|
|
167
|
+
description: string;
|
|
168
|
+
optional?: boolean;
|
|
169
|
+
formId?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const steps: Step[] = [
|
|
173
|
+
{
|
|
174
|
+
id: 1,
|
|
175
|
+
key: "details",
|
|
176
|
+
label: "Line Item Details",
|
|
177
|
+
description: "Basic information and pricing",
|
|
178
|
+
formId: "line-item-details-form",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 2,
|
|
182
|
+
key: "targeting",
|
|
183
|
+
label: "Targeting",
|
|
184
|
+
description: "Audience and location targeting",
|
|
185
|
+
formId: "targeting-step-form",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 3,
|
|
189
|
+
key: "inventory",
|
|
190
|
+
label: "Inventory",
|
|
191
|
+
description: "Select screens and packages",
|
|
192
|
+
formId: "inventory-step-form",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 4,
|
|
196
|
+
key: "schedule",
|
|
197
|
+
label: "Schedule",
|
|
198
|
+
description: "Configure schedule and forecasting",
|
|
199
|
+
formId: "schedule-step-form",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 5,
|
|
203
|
+
key: "creatives",
|
|
204
|
+
label: "Creatives",
|
|
205
|
+
description: "Assign creative assets",
|
|
206
|
+
optional: true,
|
|
207
|
+
formId: "creatives-step-form",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 6,
|
|
211
|
+
key: "summary",
|
|
212
|
+
label: "Summary",
|
|
213
|
+
description: "Review and submit",
|
|
214
|
+
optional: true,
|
|
215
|
+
formId: "summary-step-form",
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
export default function LineItemWizardPage() {
|
|
220
|
+
usePageTitle("Line Item");
|
|
221
|
+
const { dealId, lineItemId: routeLineItemId } = useParams<{ dealId: string; lineItemId?: string }>();
|
|
222
|
+
const [location, setLocation] = useLocation();
|
|
223
|
+
const queryClient = useQueryClient();
|
|
224
|
+
|
|
225
|
+
// Check if we're on the /new route by looking at the actual path
|
|
226
|
+
const isNewRoute = location.includes('/line-items/new');
|
|
227
|
+
const lineItemId = isNewRoute ? undefined : routeLineItemId;
|
|
228
|
+
const isEditMode = !isNewRoute && !!lineItemId;
|
|
229
|
+
|
|
230
|
+
// Check for plannerRestricted mode (only pacing and creatives are editable)
|
|
231
|
+
// Use window.location.search since wouter's location doesn't include query params
|
|
232
|
+
const isPlannerRestricted = typeof window !== 'undefined' && window.location.search.includes('plannerRestricted=true');
|
|
233
|
+
|
|
234
|
+
console.log('📍 Route check - location:', location, 'search:', window.location.search, 'isPlannerRestricted:', isPlannerRestricted);
|
|
235
|
+
|
|
236
|
+
const [currentStep, setCurrentStep] = useState(1);
|
|
237
|
+
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
|
238
|
+
const [formData, setFormData] = useState<LineItemFormData>(initialFormData);
|
|
239
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
240
|
+
const [savedLineItemId, setSavedLineItemId] = useState<string | undefined>(lineItemId);
|
|
241
|
+
const [externalId, setExternalId] = useState<string | undefined>();
|
|
242
|
+
const totalSteps = steps.length;
|
|
243
|
+
const { toast } = useToast();
|
|
244
|
+
|
|
245
|
+
const { data: dealData, isLoading: isLoadingDeal } = useQuery({
|
|
246
|
+
queryKey: ["deal", dealId],
|
|
247
|
+
queryFn: async () => {
|
|
248
|
+
const response = await influenceDealsRequest<Deal>(
|
|
249
|
+
InfluenceDealsAPI.deals.get(dealId!)
|
|
250
|
+
);
|
|
251
|
+
return response;
|
|
252
|
+
},
|
|
253
|
+
enabled: !!dealId,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const { data: existingLineItem, isLoading: isLoadingLineItem } = useQuery({
|
|
257
|
+
queryKey: ["lineItem", dealId, lineItemId],
|
|
258
|
+
queryFn: async () => {
|
|
259
|
+
const response = await influenceDealsRequest<any>(
|
|
260
|
+
InfluenceDealsAPI.lineItems.get(dealId!, lineItemId!)
|
|
261
|
+
);
|
|
262
|
+
return response;
|
|
263
|
+
},
|
|
264
|
+
enabled: !!dealId && !!lineItemId,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Helper function to fetch all inventories with pagination
|
|
268
|
+
const fetchAllInventories = async (dealId: string, lineItemId: string) => {
|
|
269
|
+
const allInventories: any[] = [];
|
|
270
|
+
let page = 1;
|
|
271
|
+
const limit = 50;
|
|
272
|
+
let totalPages = 1;
|
|
273
|
+
|
|
274
|
+
do {
|
|
275
|
+
const response = await influenceDealsRequest<any>(
|
|
276
|
+
InfluenceDealsAPI.lineItems.inventories(dealId, lineItemId, { page, limit })
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Handle response structure with data array and pagination
|
|
280
|
+
const data = response?.data || response || [];
|
|
281
|
+
const pagination = response?.pagination;
|
|
282
|
+
|
|
283
|
+
if (Array.isArray(data)) {
|
|
284
|
+
allInventories.push(...data);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (pagination?.totalPages) {
|
|
288
|
+
totalPages = pagination.totalPages;
|
|
289
|
+
} else {
|
|
290
|
+
// If no pagination info, assume single page
|
|
291
|
+
totalPages = 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
page++;
|
|
295
|
+
} while (page <= totalPages);
|
|
296
|
+
|
|
297
|
+
console.log('📦 Fetched all inventories (paginated):', allInventories.length, 'items');
|
|
298
|
+
return allInventories;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Fetch line item inventories from dedicated endpoint with pagination
|
|
302
|
+
const { data: lineItemInventories } = useQuery({
|
|
303
|
+
queryKey: ["lineItemInventories", dealId, lineItemId],
|
|
304
|
+
queryFn: async () => {
|
|
305
|
+
const inventories = await fetchAllInventories(dealId!, lineItemId!);
|
|
306
|
+
console.log('📦 Fetched line item inventories:', inventories);
|
|
307
|
+
return inventories;
|
|
308
|
+
},
|
|
309
|
+
enabled: !!dealId && !!lineItemId,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Reset form when creating a new line item (not edit mode) or when deal changes
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
console.log('🔄 Reset check - lineItemId:', lineItemId, 'isEditMode:', isEditMode);
|
|
315
|
+
if (!lineItemId) {
|
|
316
|
+
console.log('🔄 Resetting form for new line item');
|
|
317
|
+
setFormData(initialFormData);
|
|
318
|
+
setCurrentStep(1);
|
|
319
|
+
setCompletedSteps([]);
|
|
320
|
+
setSavedLineItemId(undefined);
|
|
321
|
+
}
|
|
322
|
+
}, [dealId, lineItemId, isEditMode]);
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
// Only load existing line item data if we have a lineItemId (edit mode)
|
|
326
|
+
if (!lineItemId) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (existingLineItem && isEditMode) {
|
|
330
|
+
const li = existingLineItem as any;
|
|
331
|
+
console.log('📦 Loading existing line item:', li);
|
|
332
|
+
// Store the external ID for use in payload building during updates
|
|
333
|
+
if (li.externalId) {
|
|
334
|
+
setExternalId(li.externalId);
|
|
335
|
+
}
|
|
336
|
+
// Pacing can be at root level or in direct object
|
|
337
|
+
const pacingData = li.pacing || li.direct?.pacing || { type: "asap" };
|
|
338
|
+
console.log('📦 Pacing from API:', pacingData);
|
|
339
|
+
|
|
340
|
+
// Get inventories from the dedicated endpoint response (already fetched with pagination)
|
|
341
|
+
const inventories = lineItemInventories || [];
|
|
342
|
+
console.log('📦 Inventories from dedicated endpoint:', inventories);
|
|
343
|
+
|
|
344
|
+
// Map inventories with all required fields for edit mode
|
|
345
|
+
const inventoryObjects = Array.isArray(inventories)
|
|
346
|
+
? inventories.map((inv: any) => ({
|
|
347
|
+
id: inv.id || inv.inventoryId,
|
|
348
|
+
inventoryId: inv.inventoryId || inv.id,
|
|
349
|
+
name: inv.name || inv.screen?.name || 'Unknown',
|
|
350
|
+
resolution: inv.resolution || inv.screen?.resolution || '1920x1080',
|
|
351
|
+
width: inv.width || inv.screen?.width,
|
|
352
|
+
height: inv.height || inv.screen?.height,
|
|
353
|
+
location: inv.location || inv.screen?.location,
|
|
354
|
+
publisher: inv.publisher,
|
|
355
|
+
publisherName: inv.publisherName || inv.publisher?.name,
|
|
356
|
+
tags: inv.tags || [],
|
|
357
|
+
operatingHours: inv.operatingHours,
|
|
358
|
+
estimatedCostPerDay: inv.estimatedCostPerDay || inv.planning?.estimatedCostPerDay,
|
|
359
|
+
currency: inv.currency || li.currency || 'USD',
|
|
360
|
+
screen: inv.screen,
|
|
361
|
+
...inv,
|
|
362
|
+
}))
|
|
363
|
+
: [];
|
|
364
|
+
|
|
365
|
+
// Extract inventory IDs
|
|
366
|
+
const inventoryIds = inventoryObjects.map((inv: any) => inv.id || inv.inventoryId);
|
|
367
|
+
|
|
368
|
+
setFormData((prev) => ({
|
|
369
|
+
details: {
|
|
370
|
+
name: li.name || "",
|
|
371
|
+
startDate: li.startDate || "",
|
|
372
|
+
endDate: li.endDate || "",
|
|
373
|
+
currency: li.currency || "USD",
|
|
374
|
+
creativeType: li.creativeType || "VIDEO",
|
|
375
|
+
impressions: li.impressions,
|
|
376
|
+
netCostPerDay: li.netCostPerDay,
|
|
377
|
+
direct: {
|
|
378
|
+
budgetSetup: li.direct?.budgetSetup || {
|
|
379
|
+
budgetType: "TOTAL",
|
|
380
|
+
budgetAmount: undefined,
|
|
381
|
+
currency: "USD",
|
|
382
|
+
},
|
|
383
|
+
campaignGoal: li.direct?.campaignGoal || {
|
|
384
|
+
type: "IMPRESSIONS",
|
|
385
|
+
targetValue: 0,
|
|
386
|
+
},
|
|
387
|
+
pacing: pacingData,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
targeting: {
|
|
391
|
+
targeting: li.targeting || undefined,
|
|
392
|
+
deliveryTargeting: li.deliveryTargeting || undefined,
|
|
393
|
+
// Preserve recommendationRunId from current session if API doesn't have it
|
|
394
|
+
recommendationRunId: li.recommendationRunId || prev.targeting.recommendationRunId || undefined,
|
|
395
|
+
},
|
|
396
|
+
inventory: {
|
|
397
|
+
inventories: inventoryIds,
|
|
398
|
+
inventoryObjects: inventoryObjects,
|
|
399
|
+
},
|
|
400
|
+
schedule: {
|
|
401
|
+
scheduleGrids: li.scheduleGrids || {},
|
|
402
|
+
schedulePreset: li.schedulePreset || "custom",
|
|
403
|
+
// Convert existing schedule from API to scheduleData format
|
|
404
|
+
scheduleData: li.schedule && Array.isArray(li.schedule) && li.schedule.length > 0
|
|
405
|
+
? {
|
|
406
|
+
schedules: li.schedule.map((s: any, idx: number) => ({
|
|
407
|
+
id: s.id || `schedule-${Date.now()}-${idx}`,
|
|
408
|
+
type: s.type || "DEFAULT",
|
|
409
|
+
validity: {
|
|
410
|
+
startDate: s.validity?.startDate || li.startDate,
|
|
411
|
+
endDate: s.validity?.endDate || li.endDate,
|
|
412
|
+
},
|
|
413
|
+
hours: s.hours || [{ start: 7, end: 23 }],
|
|
414
|
+
priority: s.priority ?? 10,
|
|
415
|
+
daysOfWeek: s.daysOfWeek || [1, 2, 3, 4, 5, 6, 7],
|
|
416
|
+
date: s.date,
|
|
417
|
+
name: s.name || `Schedule ${idx + 1}`,
|
|
418
|
+
}))
|
|
419
|
+
}
|
|
420
|
+
: undefined,
|
|
421
|
+
apiSchedules: li.schedule || [],
|
|
422
|
+
},
|
|
423
|
+
creatives: li.creatives || [],
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
}, [existingLineItem, lineItemInventories, isEditMode, lineItemId]);
|
|
427
|
+
|
|
428
|
+
const handlePrevious = useCallback(() => {
|
|
429
|
+
if (currentStep > 1) {
|
|
430
|
+
setCurrentStep((prev) => prev - 1);
|
|
431
|
+
}
|
|
432
|
+
}, [currentStep]);
|
|
433
|
+
|
|
434
|
+
const handleNext = useCallback(() => {
|
|
435
|
+
if (currentStep < totalSteps) {
|
|
436
|
+
setCompletedSteps((prev) =>
|
|
437
|
+
prev.includes(currentStep) ? prev : [...prev, currentStep]
|
|
438
|
+
);
|
|
439
|
+
setCurrentStep((prev) => prev + 1);
|
|
440
|
+
}
|
|
441
|
+
}, [currentStep, totalSteps]);
|
|
442
|
+
|
|
443
|
+
const handleStepClick = (stepId: number) => {
|
|
444
|
+
if (stepId <= currentStep || completedSteps.includes(stepId - 1)) {
|
|
445
|
+
setCurrentStep(stepId);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const buildLineItemPayload = useCallback((isUpdate: boolean = false) => {
|
|
450
|
+
const mode = dealData?.mode || "DIRECT";
|
|
451
|
+
const dealType = dealData?.dealType || "DIRECT";
|
|
452
|
+
|
|
453
|
+
// Prepare targeting data with geofencing cleanup
|
|
454
|
+
let targeting = undefined;
|
|
455
|
+
if (formData.targeting?.targeting) {
|
|
456
|
+
targeting = { ...formData.targeting.targeting };
|
|
457
|
+
if (targeting.geofencing) {
|
|
458
|
+
// Remove radiusKm and pois from top level - only geometrics and locations are allowed
|
|
459
|
+
const { radiusKm, pois, ...cleanGeofencing } = targeting.geofencing;
|
|
460
|
+
// Also strip pois from each geometry unless the API supports it
|
|
461
|
+
if (!ENABLE_POI_IN_API && cleanGeofencing.geometrics) {
|
|
462
|
+
cleanGeofencing.geometrics = cleanGeofencing.geometrics.map((geom: any) => {
|
|
463
|
+
const { pois: geomPois, ...cleanGeom } = geom;
|
|
464
|
+
return cleanGeom;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
targeting.geofencing = cleanGeofencing;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Prepare deliveryTargeting (DIRECT mode only)
|
|
472
|
+
// Note: timeOfDay is not yet supported by the backend API, so we filter it out
|
|
473
|
+
let deliveryTargeting = undefined;
|
|
474
|
+
if (mode === "DIRECT" && formData.targeting?.deliveryTargeting && Object.keys(formData.targeting.deliveryTargeting).length > 0) {
|
|
475
|
+
deliveryTargeting = { ...formData.targeting.deliveryTargeting };
|
|
476
|
+
if (deliveryTargeting.signals) {
|
|
477
|
+
const { timeOfDay, ...supportedSignals } = deliveryTargeting.signals;
|
|
478
|
+
if (Object.keys(supportedSignals).length > 0) {
|
|
479
|
+
deliveryTargeting.signals = supportedSignals;
|
|
480
|
+
} else {
|
|
481
|
+
deliveryTargeting = undefined;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Prepare pacing with lowercase type
|
|
487
|
+
let pacing = undefined;
|
|
488
|
+
if (formData.details.direct?.pacing) {
|
|
489
|
+
pacing = {
|
|
490
|
+
...formData.details.direct.pacing,
|
|
491
|
+
type: formData.details.direct.pacing.type?.toLowerCase() || 'asap',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Prepare the line item data for mapping
|
|
496
|
+
const lineItemData: any = {
|
|
497
|
+
name: formData.details.name,
|
|
498
|
+
status: "GENERATED",
|
|
499
|
+
startDate: formData.details.startDate,
|
|
500
|
+
endDate: formData.details.endDate,
|
|
501
|
+
currency: formData.details.currency,
|
|
502
|
+
pacing: pacing,
|
|
503
|
+
targeting: targeting,
|
|
504
|
+
deliveryTargeting: deliveryTargeting,
|
|
505
|
+
direct: formData.details.direct,
|
|
506
|
+
programmatic: formData.details.programmatic,
|
|
507
|
+
planning: formData.inventory.planning,
|
|
508
|
+
creativeType: formData.details.creativeType || "VIDEO",
|
|
509
|
+
duration: formData.details.duration || 10,
|
|
510
|
+
resolutions: ["1920x1080", "1280x720", "3840x2160", "4096x2160"],
|
|
511
|
+
inventories: formData.inventory.inventories || [],
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// Add schedule data if available
|
|
515
|
+
if (formData.schedule.apiSchedules && formData.schedule.apiSchedules.length > 0) {
|
|
516
|
+
lineItemData.schedule = formData.schedule.apiSchedules;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// For programmatic mode, add impressions and netCostPerDay
|
|
520
|
+
if (mode === "PROGRAMMATIC" || mode?.toUpperCase() === "PROGRAMMATIC") {
|
|
521
|
+
if (formData.details.impressions) {
|
|
522
|
+
lineItemData.impressions = formData.details.impressions;
|
|
523
|
+
}
|
|
524
|
+
if (formData.details.netCostPerDay) {
|
|
525
|
+
lineItemData.netCostPerDay = formData.details.netCostPerDay;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Use the mode-aware mapping function
|
|
530
|
+
const mappedLineItem = mapLineItemForAPI(
|
|
531
|
+
lineItemData,
|
|
532
|
+
mode,
|
|
533
|
+
dealType,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
return mappedLineItem;
|
|
537
|
+
}, [formData, dealData]);
|
|
538
|
+
|
|
539
|
+
const handleSave = useCallback(async (creativesToSave?: any[]) => {
|
|
540
|
+
setIsSaving(true);
|
|
541
|
+
try {
|
|
542
|
+
// If line item was already saved during inventory step, just navigate away
|
|
543
|
+
// Creatives are already assigned via drag-drop in edit mode
|
|
544
|
+
const effectiveLineItemId = savedLineItemId || lineItemId;
|
|
545
|
+
|
|
546
|
+
let finalLineItemId = effectiveLineItemId;
|
|
547
|
+
|
|
548
|
+
if (effectiveLineItemId) {
|
|
549
|
+
// Line item already exists, update it if needed
|
|
550
|
+
const payload = buildLineItemPayload(true);
|
|
551
|
+
await influenceDealsRequest(
|
|
552
|
+
InfluenceDealsAPI.lineItems.update(dealId!, effectiveLineItemId),
|
|
553
|
+
"PUT",
|
|
554
|
+
payload
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
// Create new line item (shouldn't happen as we save after inventory step)
|
|
558
|
+
const payload = buildLineItemPayload(false);
|
|
559
|
+
const response = await influenceDealsRequest<any>(
|
|
560
|
+
InfluenceDealsAPI.lineItems.create(dealId!),
|
|
561
|
+
"POST",
|
|
562
|
+
payload
|
|
563
|
+
);
|
|
564
|
+
finalLineItemId = response?.id || response?.data?.id;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Update inventories via separate endpoint
|
|
568
|
+
if (finalLineItemId && formData.inventory.inventoryObjects && formData.inventory.inventoryObjects.length > 0) {
|
|
569
|
+
try {
|
|
570
|
+
// Format inventory objects for the API - include all available fields from JAD API
|
|
571
|
+
const inventoryPayload = formData.inventory.inventoryObjects.map((inv: any) => mapInventoryForAPI(inv));
|
|
572
|
+
|
|
573
|
+
await influenceDealsRequest(
|
|
574
|
+
InfluenceDealsAPI.lineItems.updateInventories(dealId!, finalLineItemId),
|
|
575
|
+
"PUT",
|
|
576
|
+
{ inventories: inventoryPayload }
|
|
577
|
+
);
|
|
578
|
+
console.log('📦 Successfully updated inventories via separate endpoint');
|
|
579
|
+
} catch (inventoryError: unknown) {
|
|
580
|
+
console.error("Failed to update inventories:", inventoryError);
|
|
581
|
+
// Re-throw to prevent save completion - toast handled by caller
|
|
582
|
+
throw inventoryError;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
toast({
|
|
587
|
+
title: "Success",
|
|
588
|
+
description: `Successfully ${effectiveLineItemId ? 'updated' : 'created'} the line item "${formData.details.name}"`,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
queryClient.invalidateQueries({ queryKey: ["lineItems", dealId] });
|
|
592
|
+
queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
|
|
593
|
+
// Invalidate individual line item cache
|
|
594
|
+
if (effectiveLineItemId) {
|
|
595
|
+
queryClient.invalidateQueries({ queryKey: ["lineItem", dealId, effectiveLineItemId] });
|
|
596
|
+
queryClient.invalidateQueries({ queryKey: ["lineItemInventories", dealId, effectiveLineItemId] });
|
|
597
|
+
}
|
|
598
|
+
// Clear form data after successful save
|
|
599
|
+
setFormData(initialFormData);
|
|
600
|
+
setCurrentStep(1);
|
|
601
|
+
setCompletedSteps([]);
|
|
602
|
+
setSavedLineItemId(undefined);
|
|
603
|
+
setLocation(`/deals/${dealId}/line-items`);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error("Failed to save line item:", error);
|
|
606
|
+
toast({
|
|
607
|
+
title: "Error",
|
|
608
|
+
description: formatAPIErrorForToast(error),
|
|
609
|
+
variant: "destructive",
|
|
610
|
+
});
|
|
611
|
+
} finally {
|
|
612
|
+
setIsSaving(false);
|
|
613
|
+
}
|
|
614
|
+
}, [buildLineItemPayload, dealId, lineItemId, savedLineItemId, dealData, queryClient, setLocation, toast, formData.details.name]);
|
|
615
|
+
|
|
616
|
+
// Save line item without creatives (used after schedule step to ensure resolutions are saved)
|
|
617
|
+
const saveLineItemOnly = useCallback(async (scheduleData: any) => {
|
|
618
|
+
setIsSaving(true);
|
|
619
|
+
try {
|
|
620
|
+
// Build payload with the new schedule data
|
|
621
|
+
const updatedFormData = {
|
|
622
|
+
...formData,
|
|
623
|
+
schedule: scheduleData,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const isDirectDeal = dealData?.mode === "DIRECT";
|
|
627
|
+
const isGuaranteed = dealData?.dealType === "GUARANTEED";
|
|
628
|
+
const payload: any = {
|
|
629
|
+
name: updatedFormData.details.name,
|
|
630
|
+
status: "GENERATED",
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const isCreatingNew = !isEditMode && !savedLineItemId;
|
|
634
|
+
if (isCreatingNew) {
|
|
635
|
+
payload.externalId = generateExternalId();
|
|
636
|
+
payload.startDate = updatedFormData.details.startDate;
|
|
637
|
+
payload.endDate = updatedFormData.details.endDate;
|
|
638
|
+
payload.currency = updatedFormData.details.currency;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Add thresholdCountPerDay for GUARANTEED deals (required by API)
|
|
642
|
+
if (isGuaranteed) {
|
|
643
|
+
payload.thresholdCountPerDay = updatedFormData.details.thresholdCountPerDay || 1;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Pacing is always at root level for both direct and programmatic
|
|
647
|
+
// Ensure pacing type is lowercase as required by API
|
|
648
|
+
if (updatedFormData.details.direct?.pacing) {
|
|
649
|
+
payload.pacing = {
|
|
650
|
+
...updatedFormData.details.direct.pacing,
|
|
651
|
+
type: updatedFormData.details.direct.pacing.type?.toLowerCase() || 'asap',
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (isDirectDeal) {
|
|
656
|
+
payload.direct = {
|
|
657
|
+
budgetSetup: updatedFormData.details.direct?.budgetSetup,
|
|
658
|
+
campaignGoal: updatedFormData.details.direct?.campaignGoal,
|
|
659
|
+
};
|
|
660
|
+
} else {
|
|
661
|
+
if (updatedFormData.details.impressions) {
|
|
662
|
+
payload.impressions = updatedFormData.details.impressions;
|
|
663
|
+
}
|
|
664
|
+
if (updatedFormData.details.netCostPerDay) {
|
|
665
|
+
payload.netCostPerDay = updatedFormData.details.netCostPerDay;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (updatedFormData.targeting?.targeting) {
|
|
670
|
+
const targeting = { ...updatedFormData.targeting.targeting };
|
|
671
|
+
if (targeting.geofencing) {
|
|
672
|
+
// Remove radiusKm and pois from top level - only geometrics and locations are allowed
|
|
673
|
+
const { radiusKm, pois, ...cleanGeofencing } = targeting.geofencing;
|
|
674
|
+
// Also strip pois from each geometry unless the API supports it
|
|
675
|
+
if (!ENABLE_POI_IN_API && cleanGeofencing.geometrics) {
|
|
676
|
+
cleanGeofencing.geometrics = cleanGeofencing.geometrics.map((geom: any) => {
|
|
677
|
+
const { pois: geomPois, ...cleanGeom } = geom;
|
|
678
|
+
return cleanGeom;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
targeting.geofencing = cleanGeofencing;
|
|
682
|
+
}
|
|
683
|
+
payload.targeting = targeting;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Include deliveryTargeting for signals (weather, traffic, aqi)
|
|
687
|
+
// Note: timeOfDay is not yet supported by the backend API, so we filter it out
|
|
688
|
+
if (updatedFormData.targeting?.deliveryTargeting && Object.keys(updatedFormData.targeting.deliveryTargeting).length > 0) {
|
|
689
|
+
const deliveryTargeting = { ...updatedFormData.targeting.deliveryTargeting };
|
|
690
|
+
if (deliveryTargeting.signals) {
|
|
691
|
+
const { timeOfDay, ...supportedSignals } = deliveryTargeting.signals;
|
|
692
|
+
if (Object.keys(supportedSignals).length > 0) {
|
|
693
|
+
deliveryTargeting.signals = supportedSignals;
|
|
694
|
+
payload.deliveryTargeting = deliveryTargeting;
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
payload.deliveryTargeting = deliveryTargeting;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Use hardcoded standard resolutions for line item payload
|
|
702
|
+
payload.resolutions = ["1920x1080", "1280x720", "3840x2160", "4096x2160"];
|
|
703
|
+
|
|
704
|
+
// Set creativeType from form data (default to VIDEO)
|
|
705
|
+
payload.creativeType = updatedFormData.details.creativeType || "VIDEO";
|
|
706
|
+
|
|
707
|
+
// Set duration from form data (default to 10 seconds)
|
|
708
|
+
payload.duration = updatedFormData.details.duration || 10;
|
|
709
|
+
|
|
710
|
+
// Add schedule data if available
|
|
711
|
+
if (scheduleData.apiSchedules && scheduleData.apiSchedules.length > 0) {
|
|
712
|
+
payload.schedule = scheduleData.apiSchedules;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let newLineItemId = savedLineItemId || lineItemId;
|
|
716
|
+
|
|
717
|
+
if (isEditMode && lineItemId) {
|
|
718
|
+
await influenceDealsRequest(
|
|
719
|
+
InfluenceDealsAPI.lineItems.update(dealId!, lineItemId),
|
|
720
|
+
"PUT",
|
|
721
|
+
payload
|
|
722
|
+
);
|
|
723
|
+
newLineItemId = lineItemId;
|
|
724
|
+
} else if (savedLineItemId) {
|
|
725
|
+
// Update existing saved line item
|
|
726
|
+
await influenceDealsRequest(
|
|
727
|
+
InfluenceDealsAPI.lineItems.update(dealId!, savedLineItemId),
|
|
728
|
+
"PUT",
|
|
729
|
+
payload
|
|
730
|
+
);
|
|
731
|
+
newLineItemId = savedLineItemId;
|
|
732
|
+
} else {
|
|
733
|
+
// Create new line item
|
|
734
|
+
const response = await influenceDealsRequest<any>(
|
|
735
|
+
InfluenceDealsAPI.lineItems.create(dealId!),
|
|
736
|
+
"POST",
|
|
737
|
+
payload
|
|
738
|
+
);
|
|
739
|
+
newLineItemId = response?.id || response?.data?.id;
|
|
740
|
+
setSavedLineItemId(newLineItemId);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Update inventories via separate endpoint
|
|
744
|
+
if (newLineItemId && formData.inventory.inventoryObjects && formData.inventory.inventoryObjects.length > 0) {
|
|
745
|
+
try {
|
|
746
|
+
// Format inventory objects for the API - include all available fields from JAD API
|
|
747
|
+
const inventoryPayload = formData.inventory.inventoryObjects.map((inv: any) => mapInventoryForAPI(inv));
|
|
748
|
+
|
|
749
|
+
await influenceDealsRequest(
|
|
750
|
+
InfluenceDealsAPI.lineItems.updateInventories(dealId!, newLineItemId),
|
|
751
|
+
"PUT",
|
|
752
|
+
{ inventories: inventoryPayload }
|
|
753
|
+
);
|
|
754
|
+
console.log('📦 Successfully updated inventories via separate endpoint');
|
|
755
|
+
} catch (inventoryError: unknown) {
|
|
756
|
+
console.error("Failed to update inventories:", inventoryError);
|
|
757
|
+
// Re-throw to prevent step advancement - toast handled by caller
|
|
758
|
+
throw inventoryError;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
toast({
|
|
763
|
+
title: "Success",
|
|
764
|
+
description: `Successfully ${isEditMode || savedLineItemId ? 'updated' : 'created'} the line item "${updatedFormData.details.name}"`,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
queryClient.invalidateQueries({ queryKey: ["lineItems", dealId] });
|
|
768
|
+
queryClient.invalidateQueries({ queryKey: ["deal-line-items", dealId] });
|
|
769
|
+
queryClient.invalidateQueries({ queryKey: ["lineItem", dealId, newLineItemId] });
|
|
770
|
+
queryClient.invalidateQueries({ queryKey: ["lineItemInventories", dealId, newLineItemId] });
|
|
771
|
+
|
|
772
|
+
// Update deal status to GENERATED when completing the first line item
|
|
773
|
+
// Only do this for new line items (not edit mode) and when deal is still in DRAFT status
|
|
774
|
+
// Note: dealData?.status === "DRAFT" check prevents duplicate updates
|
|
775
|
+
if (!isEditMode && dealData?.status === "DRAFT") {
|
|
776
|
+
try {
|
|
777
|
+
console.log('[LineItemWizard] Line item completed, updating deal status to GENERATED');
|
|
778
|
+
await influenceDealsRequest(
|
|
779
|
+
InfluenceDealsAPI.deals.update(dealId!),
|
|
780
|
+
"PUT",
|
|
781
|
+
{ status: "GENERATED" }
|
|
782
|
+
);
|
|
783
|
+
queryClient.invalidateQueries({ queryKey: ["deal", dealId] });
|
|
784
|
+
} catch (statusError) {
|
|
785
|
+
console.error("Failed to update deal status to GENERATED:", statusError);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return newLineItemId;
|
|
790
|
+
} catch (error) {
|
|
791
|
+
console.error("Failed to save line item:", error);
|
|
792
|
+
throw error;
|
|
793
|
+
} finally {
|
|
794
|
+
setIsSaving(false);
|
|
795
|
+
}
|
|
796
|
+
}, [formData, dealData, dealId, lineItemId, isEditMode, savedLineItemId, queryClient, toast]);
|
|
797
|
+
|
|
798
|
+
// Create initial line item after details step (for new line items only)
|
|
799
|
+
// This ensures we have a lineItemId for the recommendation engine in the targeting step
|
|
800
|
+
const createInitialLineItem = useCallback(async (detailsData: any) => {
|
|
801
|
+
if (isEditMode || savedLineItemId || lineItemId) {
|
|
802
|
+
// Already have a line item, no need to create
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
setIsSaving(true);
|
|
807
|
+
try {
|
|
808
|
+
const isDirectDeal = dealData?.mode === "DIRECT";
|
|
809
|
+
const isGuaranteed = dealData?.dealType === "GUARANTEED";
|
|
810
|
+
const payload: any = {
|
|
811
|
+
name: detailsData.name,
|
|
812
|
+
status: "GENERATED",
|
|
813
|
+
externalId: generateExternalId(),
|
|
814
|
+
startDate: detailsData.startDate,
|
|
815
|
+
endDate: detailsData.endDate,
|
|
816
|
+
currency: detailsData.currency,
|
|
817
|
+
creativeType: detailsData.creativeType || "VIDEO",
|
|
818
|
+
duration: detailsData.duration || 10,
|
|
819
|
+
resolutions: ["1920x1080", "1280x720", "3840x2160", "4096x2160"],
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// Add thresholdCountPerDay for GUARANTEED deals (required by API)
|
|
823
|
+
if (isGuaranteed) {
|
|
824
|
+
payload.thresholdCountPerDay = detailsData.thresholdCountPerDay || 1;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Pacing at root level
|
|
828
|
+
if (detailsData.direct?.pacing) {
|
|
829
|
+
payload.pacing = {
|
|
830
|
+
...detailsData.direct.pacing,
|
|
831
|
+
type: detailsData.direct.pacing.type?.toLowerCase() || 'asap',
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (isDirectDeal) {
|
|
836
|
+
payload.direct = {
|
|
837
|
+
budgetSetup: detailsData.direct?.budgetSetup,
|
|
838
|
+
campaignGoal: detailsData.direct?.campaignGoal,
|
|
839
|
+
};
|
|
840
|
+
} else {
|
|
841
|
+
if (detailsData.impressions) {
|
|
842
|
+
payload.impressions = detailsData.impressions;
|
|
843
|
+
}
|
|
844
|
+
if (detailsData.netCostPerDay) {
|
|
845
|
+
payload.netCostPerDay = detailsData.netCostPerDay;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
console.log("🆕 Creating initial line item for recommendation engine:", payload);
|
|
850
|
+
const response = await influenceDealsRequest<any>(
|
|
851
|
+
InfluenceDealsAPI.lineItems.create(dealId!),
|
|
852
|
+
"POST",
|
|
853
|
+
payload
|
|
854
|
+
);
|
|
855
|
+
const newLineItemId = response?.id || response?.data?.id;
|
|
856
|
+
console.log("✅ Initial line item created:", newLineItemId);
|
|
857
|
+
setSavedLineItemId(newLineItemId);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.error("Failed to create initial line item:", error);
|
|
860
|
+
// Don't block the user - they can still proceed, just without recommendations
|
|
861
|
+
toast({
|
|
862
|
+
title: "Note",
|
|
863
|
+
description: "Could not initialize line item for AI recommendations. You can still select inventory manually.",
|
|
864
|
+
variant: "default",
|
|
865
|
+
});
|
|
866
|
+
} finally {
|
|
867
|
+
setIsSaving(false);
|
|
868
|
+
}
|
|
869
|
+
}, [isEditMode, savedLineItemId, lineItemId, dealData, dealId, toast]);
|
|
870
|
+
|
|
871
|
+
const handleStepComplete = useCallback(async (stepKey: string, data: any) => {
|
|
872
|
+
let shouldProceed = true;
|
|
873
|
+
|
|
874
|
+
if (stepKey === "details") {
|
|
875
|
+
setFormData((prev) => ({
|
|
876
|
+
...prev,
|
|
877
|
+
details: data,
|
|
878
|
+
}));
|
|
879
|
+
|
|
880
|
+
// Create initial line item for new line items so targeting step has an ID for recommendations
|
|
881
|
+
await createInitialLineItem(data);
|
|
882
|
+
} else if (stepKey === "targeting") {
|
|
883
|
+
console.log('[Wizard] Targeting step completed with data:', data);
|
|
884
|
+
console.log('[Wizard] recommendationRunId:', data?.recommendationRunId);
|
|
885
|
+
setFormData((prev) => ({
|
|
886
|
+
...prev,
|
|
887
|
+
targeting: data,
|
|
888
|
+
}));
|
|
889
|
+
} else if (stepKey === "inventory") {
|
|
890
|
+
setFormData((prev) => ({
|
|
891
|
+
...prev,
|
|
892
|
+
inventory: data,
|
|
893
|
+
}));
|
|
894
|
+
} else if (stepKey === "schedule") {
|
|
895
|
+
setFormData((prev) => ({
|
|
896
|
+
...prev,
|
|
897
|
+
schedule: data,
|
|
898
|
+
}));
|
|
899
|
+
|
|
900
|
+
// Save line item when completing schedule step to ensure resolutions are saved
|
|
901
|
+
try {
|
|
902
|
+
await saveLineItemOnly(data);
|
|
903
|
+
} catch (error: unknown) {
|
|
904
|
+
console.error("Failed to save line item after schedule step:", error);
|
|
905
|
+
toast({
|
|
906
|
+
title: "Error",
|
|
907
|
+
description: formatAPIErrorForToast(error),
|
|
908
|
+
variant: "destructive",
|
|
909
|
+
});
|
|
910
|
+
// Stay on current step - don't proceed
|
|
911
|
+
shouldProceed = false;
|
|
912
|
+
}
|
|
913
|
+
} else if (stepKey === "creatives") {
|
|
914
|
+
setFormData((prev) => ({
|
|
915
|
+
...prev,
|
|
916
|
+
creatives: data,
|
|
917
|
+
}));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Only proceed to next step if no errors occurred
|
|
921
|
+
if (!shouldProceed) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (currentStep < totalSteps) {
|
|
926
|
+
setCompletedSteps((prev) =>
|
|
927
|
+
prev.includes(currentStep) ? prev : [...prev, currentStep]
|
|
928
|
+
);
|
|
929
|
+
setCurrentStep((prev) => prev + 1);
|
|
930
|
+
} else {
|
|
931
|
+
// Pass creatives data directly to handleSave to avoid React state async issue
|
|
932
|
+
handleSave(stepKey === "creatives" ? data : undefined);
|
|
933
|
+
}
|
|
934
|
+
}, [currentStep, totalSteps, handleSave, saveLineItemOnly, createInitialLineItem, toast]);
|
|
935
|
+
|
|
936
|
+
const handleCancel = () => {
|
|
937
|
+
setLocation(`/deals/${dealId}/line-items`);
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const renderStepContent = () => {
|
|
941
|
+
const currentStepData = steps[currentStep - 1];
|
|
942
|
+
|
|
943
|
+
switch (currentStepData.key) {
|
|
944
|
+
case "details":
|
|
945
|
+
return (
|
|
946
|
+
<LineItemDetailsStep
|
|
947
|
+
data={formData.details}
|
|
948
|
+
dealData={dealData}
|
|
949
|
+
onComplete={(data) => handleStepComplete("details", data)}
|
|
950
|
+
isLoading={isSaving}
|
|
951
|
+
formId={currentStepData.formId}
|
|
952
|
+
isPlannerRestricted={isPlannerRestricted}
|
|
953
|
+
dealMode={dealData?.mode}
|
|
954
|
+
dealType={dealData?.dealType}
|
|
955
|
+
/>
|
|
956
|
+
);
|
|
957
|
+
case "targeting":
|
|
958
|
+
return (
|
|
959
|
+
<TargetingStep
|
|
960
|
+
data={formData.targeting}
|
|
961
|
+
campaignData={dealData}
|
|
962
|
+
lineItemId={savedLineItemId || lineItemId}
|
|
963
|
+
lineItemData={formData.details}
|
|
964
|
+
onComplete={(data) => handleStepComplete("targeting", data)}
|
|
965
|
+
onBack={handlePrevious}
|
|
966
|
+
isLoading={isSaving}
|
|
967
|
+
formId={currentStepData.formId}
|
|
968
|
+
isPlannerRestricted={isPlannerRestricted}
|
|
969
|
+
dealMode={dealData?.mode}
|
|
970
|
+
/>
|
|
971
|
+
);
|
|
972
|
+
case "inventory":
|
|
973
|
+
return (
|
|
974
|
+
<InventoryStep
|
|
975
|
+
data={formData.inventory}
|
|
976
|
+
lineItemData={formData.details}
|
|
977
|
+
lineItemId={savedLineItemId || lineItemId}
|
|
978
|
+
targeting={formData.targeting}
|
|
979
|
+
recommendationRunId={formData.targeting.recommendationRunId}
|
|
980
|
+
country={dealData?.country || "JP"}
|
|
981
|
+
onComplete={(data) => handleStepComplete("inventory", data)}
|
|
982
|
+
onBack={handlePrevious}
|
|
983
|
+
isLoading={isSaving}
|
|
984
|
+
formId={currentStepData.formId}
|
|
985
|
+
isPlannerRestricted={isPlannerRestricted}
|
|
986
|
+
dealMode={dealData?.mode}
|
|
987
|
+
/>
|
|
988
|
+
);
|
|
989
|
+
case "schedule":
|
|
990
|
+
return (
|
|
991
|
+
<ScheduleStep
|
|
992
|
+
data={formData.inventory}
|
|
993
|
+
lineItemData={formData.details}
|
|
994
|
+
scheduleData={formData.schedule.scheduleData}
|
|
995
|
+
onComplete={(data) => handleStepComplete("schedule", data)}
|
|
996
|
+
onBack={handlePrevious}
|
|
997
|
+
isLoading={isSaving}
|
|
998
|
+
formId={currentStepData.formId}
|
|
999
|
+
isPlannerRestricted={isPlannerRestricted}
|
|
1000
|
+
/>
|
|
1001
|
+
);
|
|
1002
|
+
case "creatives":
|
|
1003
|
+
return (
|
|
1004
|
+
<CreativesStep
|
|
1005
|
+
data={formData.creatives}
|
|
1006
|
+
lineItemId={savedLineItemId || lineItemId}
|
|
1007
|
+
lineItemName={formData.details.name || "Line Item 01"}
|
|
1008
|
+
dealId={dealId}
|
|
1009
|
+
dealDealId={dealData?.dealId}
|
|
1010
|
+
allowedResolutions={["1920x1080", "1280x720", "3840x2160", "4096x2160"]}
|
|
1011
|
+
creativeType={formData.details.creativeType || "VIDEO"}
|
|
1012
|
+
isEditMode={!!(savedLineItemId || lineItemId)}
|
|
1013
|
+
onComplete={(data) => handleStepComplete("creatives", data)}
|
|
1014
|
+
onBack={handlePrevious}
|
|
1015
|
+
onSkip={() => handleSave()}
|
|
1016
|
+
isLoading={isSaving}
|
|
1017
|
+
formId={currentStepData.formId}
|
|
1018
|
+
/>
|
|
1019
|
+
);
|
|
1020
|
+
case "summary":
|
|
1021
|
+
return (
|
|
1022
|
+
<SummaryStep
|
|
1023
|
+
formData={formData}
|
|
1024
|
+
dealData={dealData}
|
|
1025
|
+
dealId={dealId}
|
|
1026
|
+
dealDealId={dealData?.dealId}
|
|
1027
|
+
isEditMode={!!(savedLineItemId || lineItemId)}
|
|
1028
|
+
/>
|
|
1029
|
+
);
|
|
1030
|
+
default:
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const isLoading = isLoadingDeal || (isEditMode && isLoadingLineItem);
|
|
1036
|
+
|
|
1037
|
+
if (isLoading) {
|
|
1038
|
+
return (
|
|
1039
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
1040
|
+
<Loader2 className="h-8 w-8 animate-spin text-mw-primary-600" />
|
|
1041
|
+
</div>
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const dealName = typeof dealData?.name === "string" ? dealData.name : (dealData?.name as any)?.name || "Deal";
|
|
1046
|
+
|
|
1047
|
+
return (
|
|
1048
|
+
<div className="h-[calc(100vh-64px)] bg-mw-gray-50 dark:bg-mw-gray-900 flex flex-col overflow-hidden">
|
|
1049
|
+
{/* Title Header */}
|
|
1050
|
+
<div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-b border-mw-gray-200 dark:border-mw-gray-700 px-6 py-3">
|
|
1051
|
+
<div className="flex items-center gap-2">
|
|
1052
|
+
<Button
|
|
1053
|
+
variant="ghost"
|
|
1054
|
+
size="sm"
|
|
1055
|
+
isIconOnly
|
|
1056
|
+
onClick={handleCancel}
|
|
1057
|
+
>
|
|
1058
|
+
<ArrowLeft className="h-4 w-4" />
|
|
1059
|
+
</Button>
|
|
1060
|
+
<div>
|
|
1061
|
+
<div className="flex items-center gap-2">
|
|
1062
|
+
<h1 className="text-lg font-semibold text-mw-neutral-900 dark:text-white">
|
|
1063
|
+
{isEditMode ? "Edit Line Item" : "Add Line Item"}
|
|
1064
|
+
</h1>
|
|
1065
|
+
{isPlannerRestricted && (
|
|
1066
|
+
<span className="text-xs px-2 py-0.5 bg-purple-100 text-purple-700 rounded-full font-medium">
|
|
1067
|
+
Pacing & Creatives Only
|
|
1068
|
+
</span>
|
|
1069
|
+
)}
|
|
1070
|
+
</div>
|
|
1071
|
+
<p className="text-sm font-normal leading-normal text-mw-neutral-500 dark:text-mw-neutral-400">
|
|
1072
|
+
{isPlannerRestricted
|
|
1073
|
+
? "Only pacing and creatives can be modified for this Planner deal."
|
|
1074
|
+
: "Follow the steps below to set up your line item."}
|
|
1075
|
+
</p>
|
|
1076
|
+
</div>
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
{/* Stepper - fixed at top of content area */}
|
|
1081
|
+
<div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-b border-mw-gray-100 dark:border-mw-gray-800">
|
|
1082
|
+
<div className="w-full px-6 py-4">
|
|
1083
|
+
<div className="flex items-start justify-between">
|
|
1084
|
+
{steps.map((step, index) => {
|
|
1085
|
+
const isActive = currentStep === step.id;
|
|
1086
|
+
const isCompleted = completedSteps.includes(step.id);
|
|
1087
|
+
const isAccessible = step.id <= currentStep || completedSteps.includes(step.id - 1);
|
|
1088
|
+
|
|
1089
|
+
return (
|
|
1090
|
+
<div key={step.id} className="flex items-center flex-1">
|
|
1091
|
+
<Button
|
|
1092
|
+
variant="ghost"
|
|
1093
|
+
size="sm"
|
|
1094
|
+
onClick={() => handleStepClick(step.id)}
|
|
1095
|
+
disabled={!isAccessible}
|
|
1096
|
+
className={cn(
|
|
1097
|
+
"-mx-2 -my-1",
|
|
1098
|
+
isAccessible && "hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-800",
|
|
1099
|
+
!isAccessible && "opacity-50"
|
|
1100
|
+
)}
|
|
1101
|
+
>
|
|
1102
|
+
{isCompleted ? (
|
|
1103
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-full shrink-0 bg-mw-success-600 text-white">
|
|
1104
|
+
<Check className="h-4 w-4" />
|
|
1105
|
+
</div>
|
|
1106
|
+
) : isActive ? (
|
|
1107
|
+
<div className="flex items-center justify-center w-8 h-8 rounded-full shrink-0 bg-mw-primary-500 text-white text-sm font-medium">
|
|
1108
|
+
{step.id}
|
|
1109
|
+
</div>
|
|
1110
|
+
) : (
|
|
1111
|
+
<span
|
|
1112
|
+
className={cn(
|
|
1113
|
+
"flex items-center justify-center w-8 h-8 text-sm font-medium shrink-0 text-mw-neutral-500 dark:text-mw-neutral-400"
|
|
1114
|
+
)}
|
|
1115
|
+
>
|
|
1116
|
+
{step.id}
|
|
1117
|
+
</span>
|
|
1118
|
+
)}
|
|
1119
|
+
<div className="text-left">
|
|
1120
|
+
<p
|
|
1121
|
+
className={cn(
|
|
1122
|
+
"text-sm font-semibold whitespace-nowrap",
|
|
1123
|
+
isCompleted && "text-mw-success-800 dark:text-mw-success-400",
|
|
1124
|
+
isActive && !isCompleted && "text-mw-neutral-900 dark:text-mw-neutral-100",
|
|
1125
|
+
!isActive && !isCompleted && "text-mw-neutral-500 dark:text-mw-neutral-400"
|
|
1126
|
+
)}
|
|
1127
|
+
>
|
|
1128
|
+
{step.label}
|
|
1129
|
+
</p>
|
|
1130
|
+
{step.optional && (
|
|
1131
|
+
<p className="text-xs text-mw-neutral-400 dark:text-mw-neutral-500">
|
|
1132
|
+
Optional
|
|
1133
|
+
</p>
|
|
1134
|
+
)}
|
|
1135
|
+
</div>
|
|
1136
|
+
</Button>
|
|
1137
|
+
{index < steps.length - 1 && (
|
|
1138
|
+
<div className="flex-1 mx-4 border-t-2 border-dashed border-mw-gray-200 dark:border-mw-gray-700" />
|
|
1139
|
+
)}
|
|
1140
|
+
</div>
|
|
1141
|
+
);
|
|
1142
|
+
})}
|
|
1143
|
+
</div>
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
|
|
1147
|
+
{/* Content Area - scrollable */}
|
|
1148
|
+
<div className="flex-1 overflow-y-auto">
|
|
1149
|
+
<div className="w-full px-6 py-6">
|
|
1150
|
+
<div className="bg-white dark:bg-mw-gray-800 rounded-xl border border-mw-gray-200 dark:border-mw-gray-700 p-6">
|
|
1151
|
+
{renderStepContent()}
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
|
|
1156
|
+
{/* Fixed Footer with Navigation Buttons */}
|
|
1157
|
+
<div className="flex-shrink-0 bg-white dark:bg-mw-gray-800 border-t border-mw-gray-200 dark:border-mw-gray-700 px-6 py-4">
|
|
1158
|
+
<div className="flex justify-between items-center">
|
|
1159
|
+
<div>
|
|
1160
|
+
{currentStep > 1 && (
|
|
1161
|
+
<Button
|
|
1162
|
+
type="button"
|
|
1163
|
+
variant="outline"
|
|
1164
|
+
onClick={handlePrevious}
|
|
1165
|
+
className="border border-mw-neutral-200 dark:border-mw-neutral-700 text-mw-neutral-700 dark:text-mw-neutral-300 hover:bg-mw-neutral-50 dark:hover:bg-mw-neutral-800 px-5 py-2"
|
|
1166
|
+
>
|
|
1167
|
+
<ChevronLeft className="mr-1.5 h-4 w-4" />
|
|
1168
|
+
Previous Step
|
|
1169
|
+
</Button>
|
|
1170
|
+
)}
|
|
1171
|
+
</div>
|
|
1172
|
+
<div className="flex items-center gap-3">
|
|
1173
|
+
{currentStep === totalSteps ? (
|
|
1174
|
+
<Button
|
|
1175
|
+
type="button"
|
|
1176
|
+
onClick={() => setLocation(`/deals/${dealId}/line-items`)}
|
|
1177
|
+
className="bg-mw-primary-500 hover:bg-mw-primary-600 text-white px-5 py-2"
|
|
1178
|
+
>
|
|
1179
|
+
Finish
|
|
1180
|
+
</Button>
|
|
1181
|
+
) : (
|
|
1182
|
+
<Button
|
|
1183
|
+
type="submit"
|
|
1184
|
+
form={steps[currentStep - 1].formId}
|
|
1185
|
+
variant="outline"
|
|
1186
|
+
disabled={isSaving}
|
|
1187
|
+
className="border border-mw-primary-300 dark:border-mw-primary-600 text-mw-primary-600 dark:text-mw-primary-400 hover:bg-mw-primary-50 dark:hover:bg-mw-primary-900/20 px-5 py-2"
|
|
1188
|
+
>
|
|
1189
|
+
{isSaving ? (
|
|
1190
|
+
<>
|
|
1191
|
+
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
|
|
1192
|
+
Saving...
|
|
1193
|
+
</>
|
|
1194
|
+
) : (
|
|
1195
|
+
<>
|
|
1196
|
+
Next Step
|
|
1197
|
+
<ChevronRight className="ml-1.5 h-4 w-4" />
|
|
1198
|
+
</>
|
|
1199
|
+
)}
|
|
1200
|
+
</Button>
|
|
1201
|
+
)}
|
|
1202
|
+
</div>
|
|
1203
|
+
</div>
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
);
|
|
1207
|
+
}
|