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,1117 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
3
|
+
import { useRoute, useLocation, Link } from "wouter";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
Users,
|
|
9
|
+
Footprints,
|
|
10
|
+
Search,
|
|
11
|
+
Cloud,
|
|
12
|
+
Plus,
|
|
13
|
+
Trash2,
|
|
14
|
+
Lightbulb,
|
|
15
|
+
Pencil,
|
|
16
|
+
Loader2,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { Button } from "@/components/ui/button";
|
|
19
|
+
import { Input } from "@/components/ui/input";
|
|
20
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
21
|
+
import { Badge } from "@/components/ui/badge";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
import {
|
|
24
|
+
Form,
|
|
25
|
+
FormControl,
|
|
26
|
+
FormField,
|
|
27
|
+
FormItem,
|
|
28
|
+
FormLabel,
|
|
29
|
+
FormMessage,
|
|
30
|
+
} from "@/components/ui/form";
|
|
31
|
+
import {
|
|
32
|
+
Select,
|
|
33
|
+
SelectContent,
|
|
34
|
+
SelectItem,
|
|
35
|
+
SelectTrigger,
|
|
36
|
+
SelectValue,
|
|
37
|
+
} from "@/components/ui/select";
|
|
38
|
+
import { useToast } from "@/hooks/use-toast";
|
|
39
|
+
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
40
|
+
import type { Signal, SignalRule, Brand } from "@shared/schema";
|
|
41
|
+
import { SIGNAL_TYPES } from "@shared/schema";
|
|
42
|
+
import { SignalVisualization } from "@/components/signal-visualizations";
|
|
43
|
+
import { VendorStoresModal } from "@/components/vendor-stores-modal";
|
|
44
|
+
|
|
45
|
+
const signalFormSchema = z.object({
|
|
46
|
+
name: z.string().min(1, "Signal name is required"),
|
|
47
|
+
signalType: z.string().min(1, "Please select a signal type"),
|
|
48
|
+
status: z.enum(["active", "inactive"]).default("active"),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
type SignalFormData = z.infer<typeof signalFormSchema>;
|
|
52
|
+
|
|
53
|
+
interface RuleData {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
conditionType: string;
|
|
57
|
+
parameters: Record<string, unknown>;
|
|
58
|
+
isNew?: boolean;
|
|
59
|
+
isSaved?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const WEATHER_CONDITIONS = [
|
|
63
|
+
{ value: "temperature_above", label: "Temperature Above" },
|
|
64
|
+
{ value: "temperature_below", label: "Temperature Below" },
|
|
65
|
+
{ value: "weather_condition", label: "Weather Condition" },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const WEATHER_CONDITIONS_OPTIONS = [
|
|
69
|
+
{ value: "sunny", label: "Sunny" },
|
|
70
|
+
{ value: "cloudy", label: "Cloudy" },
|
|
71
|
+
{ value: "rainy", label: "Rainy" },
|
|
72
|
+
{ value: "stormy", label: "Stormy" },
|
|
73
|
+
{ value: "snowy", label: "Snowy" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const AUDIENCE_SEGMENTS = [
|
|
77
|
+
{ value: "high_intent", label: "High Intent Shoppers" },
|
|
78
|
+
{ value: "frequent_visitors", label: "Frequent Visitors" },
|
|
79
|
+
{ value: "new_customers", label: "New Customers" },
|
|
80
|
+
{ value: "loyalty_members", label: "Loyalty Members" },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const SEARCH_TREND_TYPES = [
|
|
84
|
+
{ value: "trending_up", label: "Trending Up" },
|
|
85
|
+
{ value: "trending_down", label: "Trending Down" },
|
|
86
|
+
{ value: "above_threshold", label: "Above Threshold" },
|
|
87
|
+
{ value: "below_threshold", label: "Below Threshold" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const FOOTFALL_TIPS = {
|
|
91
|
+
title: "Footfall Signal - Location-Based Automation",
|
|
92
|
+
tips: "Footfall signals automatically activate your line items based on real-time visitor density at physical locations. Use this to reach audiences when foot traffic matches your campaign goals - whether targeting busy shopping periods or quieter times for specific messaging.",
|
|
93
|
+
examples: [
|
|
94
|
+
"Retail promotion: Activate ads when your store's foot traffic drops below 30% to drive more visitors during slow periods.",
|
|
95
|
+
"Competitor targeting: Show your ads near competitor locations when their foot traffic exceeds 70% to capture overflow audiences.",
|
|
96
|
+
"Peak hours: Target high-traffic periods (above 80%) at transit hubs for maximum brand exposure during rush hours.",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const WEATHER_TIPS = {
|
|
101
|
+
title: "Weather Signal - Condition-Based Automation",
|
|
102
|
+
tips: "Weather signals trigger your line items automatically when specific weather conditions occur in your target locations. This enables highly relevant, contextual advertising that resonates with how people feel and behave during different weather patterns.",
|
|
103
|
+
examples: [
|
|
104
|
+
"Hot weather (above 28C): Promote cold beverages, ice cream, or air conditioning services when temperatures rise.",
|
|
105
|
+
"Rainy conditions: Advertise delivery services, indoor entertainment, or rain gear when precipitation is detected.",
|
|
106
|
+
"Cold weather (below 10C): Highlight warm drinks, heating solutions, or cozy indoor dining options.",
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const AUDIENCES_TIPS = {
|
|
111
|
+
title: "Audience Signal - Segment-Based Automation",
|
|
112
|
+
tips: "Audience signals activate line items when specific customer segments are detected in your target areas. Leverage first-party and third-party audience data to reach the right people at the right moment with personalized messaging.",
|
|
113
|
+
examples: [
|
|
114
|
+
"High-intent shoppers: Activate promotional ads when purchase-ready consumers are detected near your retail locations.",
|
|
115
|
+
"Loyalty members: Trigger exclusive offers when your loyalty program members are in proximity to your stores.",
|
|
116
|
+
"Demographic targeting: Show relevant content when your target age group or income bracket is prevalent in an area.",
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const SEARCH_TIPS = {
|
|
121
|
+
title: "Search Signal - Trend-Based Automation",
|
|
122
|
+
tips: "Search signals monitor keyword trends and automatically activate your line items when search interest spikes for relevant terms. Stay ahead of demand by responding in real-time to what people are actively searching for online.",
|
|
123
|
+
examples: [
|
|
124
|
+
"Brand momentum: Trigger brand awareness ads when searches for your company name increase by 25% or more.",
|
|
125
|
+
"Category trends: Activate product campaigns when searches for your product category are trending upward.",
|
|
126
|
+
"Competitive response: Show your ads when competitor brand searches decline, capturing audience attention during their weak moments.",
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function getTipsForType(type: string | undefined) {
|
|
131
|
+
switch (type) {
|
|
132
|
+
case "footfall":
|
|
133
|
+
return FOOTFALL_TIPS;
|
|
134
|
+
case "weather":
|
|
135
|
+
return WEATHER_TIPS;
|
|
136
|
+
case "audiences":
|
|
137
|
+
return AUDIENCES_TIPS;
|
|
138
|
+
case "search":
|
|
139
|
+
return SEARCH_TIPS;
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface FootfallRuleFormProps {
|
|
146
|
+
rule: RuleData;
|
|
147
|
+
onUpdate: (rule: RuleData) => void;
|
|
148
|
+
onSave: (rule: RuleData) => void;
|
|
149
|
+
onDelete: (id: string) => void;
|
|
150
|
+
index: number;
|
|
151
|
+
brands: Brand[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function FootfallRuleForm({ rule, onUpdate, onSave, onDelete, index, brands }: FootfallRuleFormProps) {
|
|
155
|
+
const [vendorStoresModalOpen, setVendorStoresModalOpen] = useState(false);
|
|
156
|
+
|
|
157
|
+
const params = rule.parameters as {
|
|
158
|
+
brandId?: string;
|
|
159
|
+
storeIds?: string[];
|
|
160
|
+
ruleType?: string;
|
|
161
|
+
minBusy?: number;
|
|
162
|
+
maxBusy?: number;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const selectedBrand = brands.find(b => b.id === params.brandId);
|
|
166
|
+
|
|
167
|
+
const handleSaveStores = (storeIds: string[]) => {
|
|
168
|
+
onUpdate({
|
|
169
|
+
...rule,
|
|
170
|
+
parameters: { ...params, storeIds },
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Card className={cn(
|
|
176
|
+
"border-l-4",
|
|
177
|
+
rule.isSaved ? "border-l-green-500" : "border-l-primary"
|
|
178
|
+
)}>
|
|
179
|
+
<CardContent className="pt-4">
|
|
180
|
+
<div className="flex items-center justify-between mb-4">
|
|
181
|
+
<div className="flex items-center gap-2">
|
|
182
|
+
<h4 className="font-semibold">Rule {index + 1}</h4>
|
|
183
|
+
{rule.isSaved && (
|
|
184
|
+
<Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
|
185
|
+
Saved
|
|
186
|
+
</Badge>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
<Button
|
|
190
|
+
type="button"
|
|
191
|
+
variant="ghost"
|
|
192
|
+
size="icon"
|
|
193
|
+
onClick={() => onDelete(rule.id)}
|
|
194
|
+
data-testid={`button-delete-rule-${index}`}
|
|
195
|
+
>
|
|
196
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div className="space-y-4">
|
|
201
|
+
<div>
|
|
202
|
+
<label className="text-sm font-medium">Brand</label>
|
|
203
|
+
<Select
|
|
204
|
+
value={params.brandId || ""}
|
|
205
|
+
onValueChange={(value) =>
|
|
206
|
+
onUpdate({
|
|
207
|
+
...rule,
|
|
208
|
+
parameters: { ...params, brandId: value, storeIds: [] },
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
>
|
|
212
|
+
<SelectTrigger data-testid={`select-brand-${index}`}>
|
|
213
|
+
<SelectValue placeholder="Select your brand" />
|
|
214
|
+
</SelectTrigger>
|
|
215
|
+
<SelectContent>
|
|
216
|
+
{brands.map((brand) => (
|
|
217
|
+
<SelectItem key={brand.id} value={brand.id}>
|
|
218
|
+
{brand.name}
|
|
219
|
+
</SelectItem>
|
|
220
|
+
))}
|
|
221
|
+
</SelectContent>
|
|
222
|
+
</Select>
|
|
223
|
+
{selectedBrand?.iabCategory && (
|
|
224
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
225
|
+
Category: {selectedBrand.iabCategory}
|
|
226
|
+
</p>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div>
|
|
231
|
+
<label className="text-sm font-medium">Vendor Stores</label>
|
|
232
|
+
<div className="flex items-center gap-2 mt-1">
|
|
233
|
+
<span className="text-sm text-muted-foreground">
|
|
234
|
+
{params.storeIds?.length || 0} Vendor Stores are selected
|
|
235
|
+
</span>
|
|
236
|
+
<Button
|
|
237
|
+
type="button"
|
|
238
|
+
variant="ghost"
|
|
239
|
+
className="text-primary p-0 h-auto"
|
|
240
|
+
onClick={() => setVendorStoresModalOpen(true)}
|
|
241
|
+
data-testid={`button-add-stores-${index}`}
|
|
242
|
+
>
|
|
243
|
+
<Pencil className="h-3 w-3 mr-1" />
|
|
244
|
+
{params.storeIds?.length ? "EDIT STORES" : "+ ADD STORES"}
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
<VendorStoresModal
|
|
248
|
+
open={vendorStoresModalOpen}
|
|
249
|
+
onOpenChange={setVendorStoresModalOpen}
|
|
250
|
+
brandId={params.brandId || null}
|
|
251
|
+
brandName={selectedBrand?.name}
|
|
252
|
+
selectedStoreIds={params.storeIds || []}
|
|
253
|
+
onSave={handleSaveStores}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div>
|
|
258
|
+
<label className="text-sm font-medium">Rule Type</label>
|
|
259
|
+
<Select
|
|
260
|
+
value={params.ruleType || "threshold"}
|
|
261
|
+
onValueChange={(value) =>
|
|
262
|
+
onUpdate({
|
|
263
|
+
...rule,
|
|
264
|
+
parameters: { ...params, ruleType: value },
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
>
|
|
268
|
+
<SelectTrigger data-testid={`select-rule-type-${index}`}>
|
|
269
|
+
<SelectValue placeholder="Select rule type" />
|
|
270
|
+
</SelectTrigger>
|
|
271
|
+
<SelectContent>
|
|
272
|
+
<SelectItem value="threshold">Set your threshold</SelectItem>
|
|
273
|
+
</SelectContent>
|
|
274
|
+
</Select>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div className="grid grid-cols-2 gap-4">
|
|
278
|
+
<div>
|
|
279
|
+
<label className="text-sm font-medium">Min Busy %</label>
|
|
280
|
+
<Input
|
|
281
|
+
type="number"
|
|
282
|
+
min={0}
|
|
283
|
+
max={100}
|
|
284
|
+
value={params.minBusy ?? 0}
|
|
285
|
+
onChange={(e) =>
|
|
286
|
+
onUpdate({
|
|
287
|
+
...rule,
|
|
288
|
+
parameters: { ...params, minBusy: parseInt(e.target.value) || 0 },
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
data-testid={`input-min-busy-${index}`}
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
<div>
|
|
295
|
+
<label className="text-sm font-medium">Max Busy %</label>
|
|
296
|
+
<Input
|
|
297
|
+
type="number"
|
|
298
|
+
min={0}
|
|
299
|
+
max={100}
|
|
300
|
+
value={params.maxBusy ?? 100}
|
|
301
|
+
onChange={(e) =>
|
|
302
|
+
onUpdate({
|
|
303
|
+
...rule,
|
|
304
|
+
parameters: { ...params, maxBusy: parseInt(e.target.value) || 0 },
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
data-testid={`input-max-busy-${index}`}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="flex justify-end">
|
|
313
|
+
<Button
|
|
314
|
+
type="button"
|
|
315
|
+
variant="ghost"
|
|
316
|
+
className="text-primary"
|
|
317
|
+
onClick={() => onSave(rule)}
|
|
318
|
+
data-testid={`button-save-rule-${index}`}
|
|
319
|
+
>
|
|
320
|
+
Save
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</CardContent>
|
|
325
|
+
</Card>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface WeatherRuleFormProps {
|
|
330
|
+
rule: RuleData;
|
|
331
|
+
onUpdate: (rule: RuleData) => void;
|
|
332
|
+
onSave: (rule: RuleData) => void;
|
|
333
|
+
onDelete: (id: string) => void;
|
|
334
|
+
index: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function WeatherRuleForm({ rule, onUpdate, onSave, onDelete, index }: WeatherRuleFormProps) {
|
|
338
|
+
const params = rule.parameters as {
|
|
339
|
+
location?: string;
|
|
340
|
+
condition?: string;
|
|
341
|
+
value?: number | string;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const isWeatherCondition = params.condition === "weather_condition";
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<Card className={cn(
|
|
348
|
+
"border-l-4",
|
|
349
|
+
rule.isSaved ? "border-l-green-500" : "border-l-primary"
|
|
350
|
+
)}>
|
|
351
|
+
<CardContent className="pt-4">
|
|
352
|
+
<div className="flex items-center justify-between mb-4">
|
|
353
|
+
<div className="flex items-center gap-2">
|
|
354
|
+
<h4 className="font-semibold">Rule {index + 1}</h4>
|
|
355
|
+
{rule.isSaved && (
|
|
356
|
+
<Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
|
357
|
+
Saved
|
|
358
|
+
</Badge>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
<Button
|
|
362
|
+
type="button"
|
|
363
|
+
variant="ghost"
|
|
364
|
+
size="icon"
|
|
365
|
+
onClick={() => onDelete(rule.id)}
|
|
366
|
+
data-testid={`button-delete-rule-${index}`}
|
|
367
|
+
>
|
|
368
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
369
|
+
</Button>
|
|
370
|
+
</div>
|
|
371
|
+
|
|
372
|
+
<div className="space-y-4">
|
|
373
|
+
<div>
|
|
374
|
+
<label className="text-sm font-medium">Location</label>
|
|
375
|
+
<Input
|
|
376
|
+
placeholder="Enter location"
|
|
377
|
+
value={params.location || ""}
|
|
378
|
+
onChange={(e) =>
|
|
379
|
+
onUpdate({
|
|
380
|
+
...rule,
|
|
381
|
+
parameters: { ...params, location: e.target.value },
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
data-testid={`input-location-${index}`}
|
|
385
|
+
/>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div>
|
|
389
|
+
<label className="text-sm font-medium">Condition</label>
|
|
390
|
+
<Select
|
|
391
|
+
value={params.condition || ""}
|
|
392
|
+
onValueChange={(value) =>
|
|
393
|
+
onUpdate({
|
|
394
|
+
...rule,
|
|
395
|
+
parameters: { ...params, condition: value, value: undefined },
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
>
|
|
399
|
+
<SelectTrigger data-testid={`select-condition-${index}`}>
|
|
400
|
+
<SelectValue placeholder="Select condition" />
|
|
401
|
+
</SelectTrigger>
|
|
402
|
+
<SelectContent>
|
|
403
|
+
{WEATHER_CONDITIONS.map((cond) => (
|
|
404
|
+
<SelectItem key={cond.value} value={cond.value}>
|
|
405
|
+
{cond.label}
|
|
406
|
+
</SelectItem>
|
|
407
|
+
))}
|
|
408
|
+
</SelectContent>
|
|
409
|
+
</Select>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<div>
|
|
413
|
+
<label className="text-sm font-medium">Value</label>
|
|
414
|
+
{isWeatherCondition ? (
|
|
415
|
+
<Select
|
|
416
|
+
value={(params.value as string) || ""}
|
|
417
|
+
onValueChange={(value) =>
|
|
418
|
+
onUpdate({
|
|
419
|
+
...rule,
|
|
420
|
+
parameters: { ...params, value },
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
>
|
|
424
|
+
<SelectTrigger data-testid={`select-weather-value-${index}`}>
|
|
425
|
+
<SelectValue placeholder="Select weather condition" />
|
|
426
|
+
</SelectTrigger>
|
|
427
|
+
<SelectContent>
|
|
428
|
+
{WEATHER_CONDITIONS_OPTIONS.map((opt) => (
|
|
429
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
430
|
+
{opt.label}
|
|
431
|
+
</SelectItem>
|
|
432
|
+
))}
|
|
433
|
+
</SelectContent>
|
|
434
|
+
</Select>
|
|
435
|
+
) : (
|
|
436
|
+
<Input
|
|
437
|
+
type="number"
|
|
438
|
+
placeholder="Temperature (°C)"
|
|
439
|
+
value={params.value ?? ""}
|
|
440
|
+
onChange={(e) =>
|
|
441
|
+
onUpdate({
|
|
442
|
+
...rule,
|
|
443
|
+
parameters: { ...params, value: parseInt(e.target.value) || 0 },
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
data-testid={`input-value-${index}`}
|
|
447
|
+
/>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<div className="flex justify-end">
|
|
452
|
+
<Button
|
|
453
|
+
type="button"
|
|
454
|
+
variant="ghost"
|
|
455
|
+
className="text-primary"
|
|
456
|
+
onClick={() => onSave(rule)}
|
|
457
|
+
data-testid={`button-save-rule-${index}`}
|
|
458
|
+
>
|
|
459
|
+
Save
|
|
460
|
+
</Button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</CardContent>
|
|
464
|
+
</Card>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface AudiencesRuleFormProps {
|
|
469
|
+
rule: RuleData;
|
|
470
|
+
onUpdate: (rule: RuleData) => void;
|
|
471
|
+
onSave: (rule: RuleData) => void;
|
|
472
|
+
onDelete: (id: string) => void;
|
|
473
|
+
index: number;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function AudiencesRuleForm({ rule, onUpdate, onSave, onDelete, index }: AudiencesRuleFormProps) {
|
|
477
|
+
const params = rule.parameters as {
|
|
478
|
+
segment?: string;
|
|
479
|
+
minThreshold?: number;
|
|
480
|
+
maxThreshold?: number;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<Card className={cn(
|
|
485
|
+
"border-l-4",
|
|
486
|
+
rule.isSaved ? "border-l-green-500" : "border-l-primary"
|
|
487
|
+
)}>
|
|
488
|
+
<CardContent className="pt-4">
|
|
489
|
+
<div className="flex items-center justify-between mb-4">
|
|
490
|
+
<div className="flex items-center gap-2">
|
|
491
|
+
<h4 className="font-semibold">Rule {index + 1}</h4>
|
|
492
|
+
{rule.isSaved && (
|
|
493
|
+
<Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
|
494
|
+
Saved
|
|
495
|
+
</Badge>
|
|
496
|
+
)}
|
|
497
|
+
</div>
|
|
498
|
+
<Button
|
|
499
|
+
type="button"
|
|
500
|
+
variant="ghost"
|
|
501
|
+
size="icon"
|
|
502
|
+
onClick={() => onDelete(rule.id)}
|
|
503
|
+
data-testid={`button-delete-rule-${index}`}
|
|
504
|
+
>
|
|
505
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
506
|
+
</Button>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<div className="space-y-4">
|
|
510
|
+
<div>
|
|
511
|
+
<label className="text-sm font-medium">Audience Segment</label>
|
|
512
|
+
<Select
|
|
513
|
+
value={params.segment || ""}
|
|
514
|
+
onValueChange={(value) =>
|
|
515
|
+
onUpdate({
|
|
516
|
+
...rule,
|
|
517
|
+
parameters: { ...params, segment: value },
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
>
|
|
521
|
+
<SelectTrigger data-testid={`select-segment-${index}`}>
|
|
522
|
+
<SelectValue placeholder="Select audience segment" />
|
|
523
|
+
</SelectTrigger>
|
|
524
|
+
<SelectContent>
|
|
525
|
+
{AUDIENCE_SEGMENTS.map((seg) => (
|
|
526
|
+
<SelectItem key={seg.value} value={seg.value}>
|
|
527
|
+
{seg.label}
|
|
528
|
+
</SelectItem>
|
|
529
|
+
))}
|
|
530
|
+
</SelectContent>
|
|
531
|
+
</Select>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<div className="grid grid-cols-2 gap-4">
|
|
535
|
+
<div>
|
|
536
|
+
<label className="text-sm font-medium">Min Threshold %</label>
|
|
537
|
+
<Input
|
|
538
|
+
type="number"
|
|
539
|
+
min={0}
|
|
540
|
+
max={100}
|
|
541
|
+
value={params.minThreshold ?? 0}
|
|
542
|
+
onChange={(e) =>
|
|
543
|
+
onUpdate({
|
|
544
|
+
...rule,
|
|
545
|
+
parameters: { ...params, minThreshold: parseInt(e.target.value) || 0 },
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
data-testid={`input-min-threshold-${index}`}
|
|
549
|
+
/>
|
|
550
|
+
</div>
|
|
551
|
+
<div>
|
|
552
|
+
<label className="text-sm font-medium">Max Threshold %</label>
|
|
553
|
+
<Input
|
|
554
|
+
type="number"
|
|
555
|
+
min={0}
|
|
556
|
+
max={100}
|
|
557
|
+
value={params.maxThreshold ?? 100}
|
|
558
|
+
onChange={(e) =>
|
|
559
|
+
onUpdate({
|
|
560
|
+
...rule,
|
|
561
|
+
parameters: { ...params, maxThreshold: parseInt(e.target.value) || 0 },
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
data-testid={`input-max-threshold-${index}`}
|
|
565
|
+
/>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<div className="flex justify-end">
|
|
570
|
+
<Button
|
|
571
|
+
type="button"
|
|
572
|
+
variant="ghost"
|
|
573
|
+
className="text-primary"
|
|
574
|
+
onClick={() => onSave(rule)}
|
|
575
|
+
data-testid={`button-save-rule-${index}`}
|
|
576
|
+
>
|
|
577
|
+
Save
|
|
578
|
+
</Button>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
</CardContent>
|
|
582
|
+
</Card>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
interface SearchRuleFormProps {
|
|
587
|
+
rule: RuleData;
|
|
588
|
+
onUpdate: (rule: RuleData) => void;
|
|
589
|
+
onSave: (rule: RuleData) => void;
|
|
590
|
+
onDelete: (id: string) => void;
|
|
591
|
+
index: number;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function SearchRuleForm({ rule, onUpdate, onSave, onDelete, index }: SearchRuleFormProps) {
|
|
595
|
+
const params = rule.parameters as {
|
|
596
|
+
keyword?: string;
|
|
597
|
+
trendType?: string;
|
|
598
|
+
threshold?: number;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
return (
|
|
602
|
+
<Card className={cn(
|
|
603
|
+
"border-l-4",
|
|
604
|
+
rule.isSaved ? "border-l-green-500" : "border-l-primary"
|
|
605
|
+
)}>
|
|
606
|
+
<CardContent className="pt-4">
|
|
607
|
+
<div className="flex items-center justify-between mb-4">
|
|
608
|
+
<div className="flex items-center gap-2">
|
|
609
|
+
<h4 className="font-semibold">Rule {index + 1}</h4>
|
|
610
|
+
{rule.isSaved && (
|
|
611
|
+
<Badge variant="secondary" className="text-xs bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
|
612
|
+
Saved
|
|
613
|
+
</Badge>
|
|
614
|
+
)}
|
|
615
|
+
</div>
|
|
616
|
+
<Button
|
|
617
|
+
type="button"
|
|
618
|
+
variant="ghost"
|
|
619
|
+
size="icon"
|
|
620
|
+
onClick={() => onDelete(rule.id)}
|
|
621
|
+
data-testid={`button-delete-rule-${index}`}
|
|
622
|
+
>
|
|
623
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
624
|
+
</Button>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div className="space-y-4">
|
|
628
|
+
<div>
|
|
629
|
+
<label className="text-sm font-medium">Keyword</label>
|
|
630
|
+
<Input
|
|
631
|
+
placeholder="Enter search keyword"
|
|
632
|
+
value={params.keyword || ""}
|
|
633
|
+
onChange={(e) =>
|
|
634
|
+
onUpdate({
|
|
635
|
+
...rule,
|
|
636
|
+
parameters: { ...params, keyword: e.target.value },
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
data-testid={`input-keyword-${index}`}
|
|
640
|
+
/>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
<div>
|
|
644
|
+
<label className="text-sm font-medium">Trend Type</label>
|
|
645
|
+
<Select
|
|
646
|
+
value={params.trendType || ""}
|
|
647
|
+
onValueChange={(value) =>
|
|
648
|
+
onUpdate({
|
|
649
|
+
...rule,
|
|
650
|
+
parameters: { ...params, trendType: value },
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
>
|
|
654
|
+
<SelectTrigger data-testid={`select-trend-type-${index}`}>
|
|
655
|
+
<SelectValue placeholder="Select trend type" />
|
|
656
|
+
</SelectTrigger>
|
|
657
|
+
<SelectContent>
|
|
658
|
+
{SEARCH_TREND_TYPES.map((trend) => (
|
|
659
|
+
<SelectItem key={trend.value} value={trend.value}>
|
|
660
|
+
{trend.label}
|
|
661
|
+
</SelectItem>
|
|
662
|
+
))}
|
|
663
|
+
</SelectContent>
|
|
664
|
+
</Select>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div>
|
|
668
|
+
<label className="text-sm font-medium">Threshold %</label>
|
|
669
|
+
<Input
|
|
670
|
+
type="number"
|
|
671
|
+
min={0}
|
|
672
|
+
max={100}
|
|
673
|
+
value={params.threshold ?? 0}
|
|
674
|
+
onChange={(e) =>
|
|
675
|
+
onUpdate({
|
|
676
|
+
...rule,
|
|
677
|
+
parameters: { ...params, threshold: parseInt(e.target.value) || 0 },
|
|
678
|
+
})
|
|
679
|
+
}
|
|
680
|
+
data-testid={`input-threshold-${index}`}
|
|
681
|
+
/>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div className="flex justify-end">
|
|
685
|
+
<Button
|
|
686
|
+
type="button"
|
|
687
|
+
variant="ghost"
|
|
688
|
+
className="text-primary"
|
|
689
|
+
onClick={() => onSave(rule)}
|
|
690
|
+
data-testid={`button-save-rule-${index}`}
|
|
691
|
+
>
|
|
692
|
+
Save
|
|
693
|
+
</Button>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</CardContent>
|
|
697
|
+
</Card>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export default function SignalForm() {
|
|
702
|
+
const [, setLocation] = useLocation();
|
|
703
|
+
const [matchCreate] = useRoute("/signals/create");
|
|
704
|
+
const [matchEdit, editParams] = useRoute("/signals/:id/edit");
|
|
705
|
+
const { toast } = useToast();
|
|
706
|
+
|
|
707
|
+
const isEditing = Boolean(matchEdit);
|
|
708
|
+
const signalId = editParams?.id;
|
|
709
|
+
|
|
710
|
+
const [rules, setRules] = useState<RuleData[]>([]);
|
|
711
|
+
|
|
712
|
+
const { data: existingSignal, isLoading: signalLoading } = useQuery<Signal>({
|
|
713
|
+
queryKey: ["/api/signals", signalId],
|
|
714
|
+
enabled: isEditing && Boolean(signalId),
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
const { data: existingRules = [] } = useQuery<SignalRule[]>({
|
|
718
|
+
queryKey: ["/api/signals", signalId, "rules"],
|
|
719
|
+
enabled: isEditing && Boolean(signalId),
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const { data: brands = [] } = useQuery<Brand[]>({
|
|
723
|
+
queryKey: ["/api/brands"],
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const form = useForm<SignalFormData>({
|
|
727
|
+
resolver: zodResolver(signalFormSchema),
|
|
728
|
+
defaultValues: {
|
|
729
|
+
name: "",
|
|
730
|
+
signalType: "",
|
|
731
|
+
status: "active",
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const watchedType = form.watch("signalType");
|
|
736
|
+
|
|
737
|
+
useEffect(() => {
|
|
738
|
+
if (existingSignal) {
|
|
739
|
+
form.reset({
|
|
740
|
+
name: existingSignal.name,
|
|
741
|
+
signalType: existingSignal.signalType,
|
|
742
|
+
status: existingSignal.status as "active" | "inactive",
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}, [existingSignal, form]);
|
|
746
|
+
|
|
747
|
+
useEffect(() => {
|
|
748
|
+
if (existingRules.length > 0) {
|
|
749
|
+
setRules(
|
|
750
|
+
existingRules.map((r) => ({
|
|
751
|
+
id: r.id,
|
|
752
|
+
name: r.name,
|
|
753
|
+
conditionType: r.conditionType,
|
|
754
|
+
parameters: (r.parameters as Record<string, unknown>) || {},
|
|
755
|
+
isSaved: true,
|
|
756
|
+
}))
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}, [existingRules]);
|
|
760
|
+
|
|
761
|
+
const createSignalMutation = useMutation({
|
|
762
|
+
mutationFn: async (data: SignalFormData) => {
|
|
763
|
+
const response = await apiRequest("POST", "/api/signals", data);
|
|
764
|
+
return response.json();
|
|
765
|
+
},
|
|
766
|
+
onSuccess: async (signal: Signal) => {
|
|
767
|
+
for (const rule of rules) {
|
|
768
|
+
await apiRequest("POST", `/api/signals/${signal.id}/rules`, {
|
|
769
|
+
name: rule.name,
|
|
770
|
+
conditionType: rule.conditionType,
|
|
771
|
+
parameters: rule.parameters,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
queryClient.invalidateQueries({ queryKey: ["/api/signals"] });
|
|
775
|
+
toast({ title: "Signal created successfully" });
|
|
776
|
+
setLocation("/signals");
|
|
777
|
+
},
|
|
778
|
+
onError: () => {
|
|
779
|
+
toast({ title: "Failed to create signal", variant: "destructive" });
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const updateSignalMutation = useMutation({
|
|
784
|
+
mutationFn: async (data: SignalFormData) => {
|
|
785
|
+
const response = await apiRequest("PATCH", `/api/signals/${signalId}`, data);
|
|
786
|
+
return response.json();
|
|
787
|
+
},
|
|
788
|
+
onSuccess: async () => {
|
|
789
|
+
for (const rule of rules) {
|
|
790
|
+
if (rule.isNew) {
|
|
791
|
+
await apiRequest("POST", `/api/signals/${signalId}/rules`, {
|
|
792
|
+
name: rule.name,
|
|
793
|
+
conditionType: rule.conditionType,
|
|
794
|
+
parameters: rule.parameters,
|
|
795
|
+
});
|
|
796
|
+
} else {
|
|
797
|
+
await apiRequest("PATCH", `/api/signals/${signalId}/rules/${rule.id}`, {
|
|
798
|
+
name: rule.name,
|
|
799
|
+
conditionType: rule.conditionType,
|
|
800
|
+
parameters: rule.parameters,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
queryClient.invalidateQueries({ queryKey: ["/api/signals"] });
|
|
805
|
+
queryClient.invalidateQueries({ queryKey: ["/api/signals", signalId] });
|
|
806
|
+
toast({ title: "Signal updated successfully" });
|
|
807
|
+
setLocation("/signals");
|
|
808
|
+
},
|
|
809
|
+
onError: () => {
|
|
810
|
+
toast({ title: "Failed to update signal", variant: "destructive" });
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const onSubmit = (data: SignalFormData) => {
|
|
815
|
+
if (isEditing) {
|
|
816
|
+
updateSignalMutation.mutate(data);
|
|
817
|
+
} else {
|
|
818
|
+
createSignalMutation.mutate(data);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const addRule = () => {
|
|
823
|
+
const newRule: RuleData = {
|
|
824
|
+
id: `temp-${Date.now()}`,
|
|
825
|
+
name: `Rule ${rules.length + 1}`,
|
|
826
|
+
conditionType: watchedType,
|
|
827
|
+
parameters: {},
|
|
828
|
+
isNew: true,
|
|
829
|
+
};
|
|
830
|
+
setRules([...rules, newRule]);
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const updateRule = (updatedRule: RuleData) => {
|
|
834
|
+
setRules(rules.map((r) => (r.id === updatedRule.id ? updatedRule : r)));
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const saveRule = (rule: RuleData) => {
|
|
838
|
+
setRules(rules.map((r) => (r.id === rule.id ? { ...rule, isSaved: true } : r)));
|
|
839
|
+
toast({ title: `Rule saved` });
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const deleteRule = async (ruleId: string) => {
|
|
843
|
+
const rule = rules.find((r) => r.id === ruleId);
|
|
844
|
+
if (rule && !rule.isNew && isEditing) {
|
|
845
|
+
try {
|
|
846
|
+
await apiRequest("DELETE", `/api/signals/${signalId}/rules/${ruleId}`);
|
|
847
|
+
} catch {
|
|
848
|
+
toast({ title: "Failed to delete rule", variant: "destructive" });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
setRules(rules.filter((r) => r.id !== ruleId));
|
|
853
|
+
toast({ title: "Rule deleted" });
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const tips = getTipsForType(watchedType);
|
|
857
|
+
const isPending = createSignalMutation.isPending || updateSignalMutation.isPending;
|
|
858
|
+
|
|
859
|
+
if (isEditing && signalLoading) {
|
|
860
|
+
return (
|
|
861
|
+
<div className="flex items-center justify-center h-screen">
|
|
862
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return (
|
|
868
|
+
<div className="flex flex-col h-screen">
|
|
869
|
+
<header className="sticky top-0 z-10 bg-background border-b px-6 py-4">
|
|
870
|
+
<div className="flex items-center justify-between">
|
|
871
|
+
<h1 className="text-xl font-semibold" data-testid="text-page-title">
|
|
872
|
+
{isEditing ? "Edit Signal" : "New Signal"}
|
|
873
|
+
</h1>
|
|
874
|
+
<div className="flex items-center gap-4">
|
|
875
|
+
<FormField
|
|
876
|
+
control={form.control}
|
|
877
|
+
name="status"
|
|
878
|
+
render={({ field }) => (
|
|
879
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
880
|
+
<SelectTrigger className="w-32" data-testid="select-status">
|
|
881
|
+
<SelectValue />
|
|
882
|
+
</SelectTrigger>
|
|
883
|
+
<SelectContent>
|
|
884
|
+
<SelectItem value="active">Active</SelectItem>
|
|
885
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
886
|
+
</SelectContent>
|
|
887
|
+
</Select>
|
|
888
|
+
)}
|
|
889
|
+
/>
|
|
890
|
+
<Link href="/signals">
|
|
891
|
+
<Button variant="outline" className="text-destructive border-destructive hover:bg-destructive/10" data-testid="button-cancel">
|
|
892
|
+
Cancel
|
|
893
|
+
</Button>
|
|
894
|
+
</Link>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
898
|
+
You can add multiple rules to activate the signal. If any of the rules is true, the signal activates the associated line item automatically.
|
|
899
|
+
</p>
|
|
900
|
+
</header>
|
|
901
|
+
|
|
902
|
+
<div className="flex-1 overflow-y-auto">
|
|
903
|
+
<Form {...form}>
|
|
904
|
+
<form onSubmit={form.handleSubmit(onSubmit)} id="signal-form" className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
|
|
905
|
+
<div className="space-y-6">
|
|
906
|
+
<Card>
|
|
907
|
+
<CardHeader>
|
|
908
|
+
<CardTitle>Signal Details</CardTitle>
|
|
909
|
+
</CardHeader>
|
|
910
|
+
<CardContent className="space-y-4">
|
|
911
|
+
<FormField
|
|
912
|
+
control={form.control}
|
|
913
|
+
name="name"
|
|
914
|
+
render={({ field }) => (
|
|
915
|
+
<FormItem>
|
|
916
|
+
<FormLabel>Signal Name</FormLabel>
|
|
917
|
+
<FormControl>
|
|
918
|
+
<Input
|
|
919
|
+
placeholder="Enter a descriptive name for this signal"
|
|
920
|
+
{...field}
|
|
921
|
+
data-testid="input-signal-name"
|
|
922
|
+
/>
|
|
923
|
+
</FormControl>
|
|
924
|
+
<FormMessage />
|
|
925
|
+
</FormItem>
|
|
926
|
+
)}
|
|
927
|
+
/>
|
|
928
|
+
|
|
929
|
+
<FormField
|
|
930
|
+
control={form.control}
|
|
931
|
+
name="signalType"
|
|
932
|
+
render={({ field }) => (
|
|
933
|
+
<FormItem>
|
|
934
|
+
<FormLabel>Signal Type</FormLabel>
|
|
935
|
+
<Select
|
|
936
|
+
value={field.value}
|
|
937
|
+
onValueChange={(value) => {
|
|
938
|
+
field.onChange(value);
|
|
939
|
+
if (watchedType !== value) {
|
|
940
|
+
setRules([]);
|
|
941
|
+
}
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
<SelectTrigger data-testid="select-signal-type">
|
|
945
|
+
<SelectValue placeholder="Select signal type" />
|
|
946
|
+
</SelectTrigger>
|
|
947
|
+
<SelectContent>
|
|
948
|
+
{SIGNAL_TYPES.map((type) => (
|
|
949
|
+
<SelectItem key={type.value} value={type.value}>
|
|
950
|
+
{type.label}
|
|
951
|
+
</SelectItem>
|
|
952
|
+
))}
|
|
953
|
+
</SelectContent>
|
|
954
|
+
</Select>
|
|
955
|
+
<FormMessage />
|
|
956
|
+
</FormItem>
|
|
957
|
+
)}
|
|
958
|
+
/>
|
|
959
|
+
</CardContent>
|
|
960
|
+
</Card>
|
|
961
|
+
|
|
962
|
+
<Card>
|
|
963
|
+
<CardHeader>
|
|
964
|
+
<div className="flex items-center justify-between">
|
|
965
|
+
<div>
|
|
966
|
+
<CardTitle>Rules</CardTitle>
|
|
967
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
968
|
+
Configure when this signal activates
|
|
969
|
+
</p>
|
|
970
|
+
</div>
|
|
971
|
+
<Button
|
|
972
|
+
type="button"
|
|
973
|
+
variant="outline"
|
|
974
|
+
onClick={addRule}
|
|
975
|
+
disabled={!watchedType}
|
|
976
|
+
data-testid="button-add-rule"
|
|
977
|
+
>
|
|
978
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
979
|
+
Add Rule
|
|
980
|
+
</Button>
|
|
981
|
+
</div>
|
|
982
|
+
</CardHeader>
|
|
983
|
+
<CardContent>
|
|
984
|
+
{rules.length === 0 ? (
|
|
985
|
+
<div className="border-2 border-dashed rounded-lg p-8 text-center">
|
|
986
|
+
<div className="mx-auto w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
|
|
987
|
+
<Plus className="h-6 w-6 text-muted-foreground" />
|
|
988
|
+
</div>
|
|
989
|
+
<p className="font-medium mb-1">No rules configured</p>
|
|
990
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
991
|
+
Add rules to define when this signal should activate
|
|
992
|
+
</p>
|
|
993
|
+
<Button
|
|
994
|
+
type="button"
|
|
995
|
+
variant="outline"
|
|
996
|
+
onClick={addRule}
|
|
997
|
+
disabled={!watchedType}
|
|
998
|
+
data-testid="button-add-first-rule"
|
|
999
|
+
>
|
|
1000
|
+
Add Your First Rule
|
|
1001
|
+
</Button>
|
|
1002
|
+
</div>
|
|
1003
|
+
) : (
|
|
1004
|
+
<div className="space-y-4">
|
|
1005
|
+
{rules.map((rule, index) => {
|
|
1006
|
+
switch (watchedType) {
|
|
1007
|
+
case "footfall":
|
|
1008
|
+
return (
|
|
1009
|
+
<FootfallRuleForm
|
|
1010
|
+
key={rule.id}
|
|
1011
|
+
rule={rule}
|
|
1012
|
+
onUpdate={updateRule}
|
|
1013
|
+
onSave={saveRule}
|
|
1014
|
+
onDelete={deleteRule}
|
|
1015
|
+
index={index}
|
|
1016
|
+
brands={brands}
|
|
1017
|
+
/>
|
|
1018
|
+
);
|
|
1019
|
+
case "weather":
|
|
1020
|
+
return (
|
|
1021
|
+
<WeatherRuleForm
|
|
1022
|
+
key={rule.id}
|
|
1023
|
+
rule={rule}
|
|
1024
|
+
onUpdate={updateRule}
|
|
1025
|
+
onSave={saveRule}
|
|
1026
|
+
onDelete={deleteRule}
|
|
1027
|
+
index={index}
|
|
1028
|
+
/>
|
|
1029
|
+
);
|
|
1030
|
+
case "audiences":
|
|
1031
|
+
return (
|
|
1032
|
+
<AudiencesRuleForm
|
|
1033
|
+
key={rule.id}
|
|
1034
|
+
rule={rule}
|
|
1035
|
+
onUpdate={updateRule}
|
|
1036
|
+
onSave={saveRule}
|
|
1037
|
+
onDelete={deleteRule}
|
|
1038
|
+
index={index}
|
|
1039
|
+
/>
|
|
1040
|
+
);
|
|
1041
|
+
case "search":
|
|
1042
|
+
return (
|
|
1043
|
+
<SearchRuleForm
|
|
1044
|
+
key={rule.id}
|
|
1045
|
+
rule={rule}
|
|
1046
|
+
onUpdate={updateRule}
|
|
1047
|
+
onSave={saveRule}
|
|
1048
|
+
onDelete={deleteRule}
|
|
1049
|
+
index={index}
|
|
1050
|
+
/>
|
|
1051
|
+
);
|
|
1052
|
+
default:
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
})}
|
|
1056
|
+
</div>
|
|
1057
|
+
)}
|
|
1058
|
+
</CardContent>
|
|
1059
|
+
</Card>
|
|
1060
|
+
</div>
|
|
1061
|
+
|
|
1062
|
+
<div className="space-y-4">
|
|
1063
|
+
{watchedType && rules.length > 0 && (
|
|
1064
|
+
<Card>
|
|
1065
|
+
<CardContent className="p-6">
|
|
1066
|
+
<SignalVisualization type={watchedType} rules={rules} />
|
|
1067
|
+
</CardContent>
|
|
1068
|
+
</Card>
|
|
1069
|
+
)}
|
|
1070
|
+
<Card>
|
|
1071
|
+
<CardContent className="p-6">
|
|
1072
|
+
<div className="flex items-start gap-3">
|
|
1073
|
+
<div className="p-2 rounded-full bg-yellow-100 dark:bg-yellow-900/30">
|
|
1074
|
+
<Lightbulb className="h-5 w-5 text-yellow-600 dark:text-yellow-500" />
|
|
1075
|
+
</div>
|
|
1076
|
+
<div className="space-y-3">
|
|
1077
|
+
<p className="font-medium">
|
|
1078
|
+
{tips?.title || "Select signal type and add rules to view forecasting."}
|
|
1079
|
+
</p>
|
|
1080
|
+
{tips && (
|
|
1081
|
+
<>
|
|
1082
|
+
<p className="text-sm text-muted-foreground">{tips.tips}</p>
|
|
1083
|
+
<div className="text-sm text-muted-foreground">
|
|
1084
|
+
<p className="font-medium mb-2">Example:</p>
|
|
1085
|
+
<ol className="list-decimal list-inside space-y-1">
|
|
1086
|
+
{tips.examples.map((example, i) => (
|
|
1087
|
+
<li key={i}>{example}</li>
|
|
1088
|
+
))}
|
|
1089
|
+
</ol>
|
|
1090
|
+
</div>
|
|
1091
|
+
</>
|
|
1092
|
+
)}
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
</CardContent>
|
|
1096
|
+
</Card>
|
|
1097
|
+
</div>
|
|
1098
|
+
</form>
|
|
1099
|
+
</Form>
|
|
1100
|
+
</div>
|
|
1101
|
+
|
|
1102
|
+
<footer className="sticky bottom-0 z-10 bg-background border-t px-6 py-4">
|
|
1103
|
+
<div className="flex items-center justify-between gap-4">
|
|
1104
|
+
<Link href="/signals">
|
|
1105
|
+
<Button type="button" variant="ghost" data-testid="button-back">
|
|
1106
|
+
Back
|
|
1107
|
+
</Button>
|
|
1108
|
+
</Link>
|
|
1109
|
+
<Button type="submit" form="signal-form" disabled={isPending} data-testid="button-save">
|
|
1110
|
+
{isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
|
1111
|
+
Save
|
|
1112
|
+
</Button>
|
|
1113
|
+
</div>
|
|
1114
|
+
</footer>
|
|
1115
|
+
</div>
|
|
1116
|
+
);
|
|
1117
|
+
}
|