diff --git a/Backend/.env-example b/Backend/.env-example index 18e42cd..3d7415e 100644 --- a/Backend/.env-example +++ b/Backend/.env-example @@ -3,8 +3,13 @@ password=[YOUR-PASSWORD] host= port=5432 dbname=postgres -GROQ_API_KEY= +GROQ_API_KEY=your_groq_api_key_here SUPABASE_URL= SUPABASE_KEY= GEMINI_API_KEY= -YOUTUBE_API_KEY= \ No newline at end of file +YOUTUBE_API_KEY= + +# Redis Cloud configuration +REDIS_HOST=your-redis-cloud-host +REDIS_PORT=12345 +REDIS_PASSWORD=your-redis-cloud-password \ No newline at end of file diff --git a/Backend/app/main.py b/Backend/app/main.py index 86d892a..e641946 100644 --- a/Backend/app/main.py +++ b/Backend/app/main.py @@ -6,6 +6,9 @@ from .routes.post import router as post_router from .routes.chat import router as chat_router from .routes.match import router as match_router +from .routes.brand_dashboard import router as brand_dashboard_router +from .routes.ai_query import router as ai_query_router +from .routes.contracts import router as contracts_router from sqlalchemy.exc import SQLAlchemyError import logging import os @@ -54,6 +57,9 @@ async def lifespan(app: FastAPI): app.include_router(post_router) app.include_router(chat_router) app.include_router(match_router) +app.include_router(brand_dashboard_router) +app.include_router(ai_query_router) +app.include_router(contracts_router) app.include_router(ai.router) app.include_router(ai.youtube_router) diff --git a/Backend/app/models/models.py b/Backend/app/models/models.py index 56681ab..a521269 100644 --- a/Backend/app/models/models.py +++ b/Backend/app/models/models.py @@ -12,7 +12,7 @@ TIMESTAMP, ) from sqlalchemy.orm import relationship -from datetime import datetime +from datetime import datetime, timezone from app.db.db import Base import uuid @@ -160,3 +160,82 @@ class SponsorshipPayment(Base): brand = relationship( "User", foreign_keys=[brand_id], back_populates="brand_payments" ) + + +# ============================================================================ +# BRAND DASHBOARD MODELS +# ============================================================================ + +# Brand Profile Table (Extended brand information) +class BrandProfile(Base): + __tablename__ = "brand_profiles" + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey("users.id"), nullable=False) + company_name = Column(String, nullable=True) + website = Column(String, nullable=True) + industry = Column(String, nullable=True) + contact_person = Column(String, nullable=True) + contact_email = Column(String, nullable=True) + created_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + + # Relationships + user = relationship("User", backref="brand_profile") + + +# Campaign Metrics Table (Performance tracking) +class CampaignMetrics(Base): + __tablename__ = "campaign_metrics" + + id = Column(String, primary_key=True, default=generate_uuid) + campaign_id = Column(String, ForeignKey("sponsorships.id"), nullable=False) + impressions = Column(Integer, nullable=True) + clicks = Column(Integer, nullable=True) + conversions = Column(Integer, nullable=True) + revenue = Column(DECIMAL(10, 2), nullable=True) + engagement_rate = Column(Float, nullable=True) + recorded_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + + # Relationships + campaign = relationship("Sponsorship", backref="metrics") + + +# Contracts Table (Contract management) +class Contract(Base): + __tablename__ = "contracts" + + id = Column(String, primary_key=True, default=generate_uuid) + sponsorship_id = Column(String, ForeignKey("sponsorships.id"), nullable=False) + creator_id = Column(String, ForeignKey("users.id"), nullable=False) + brand_id = Column(String, ForeignKey("users.id"), nullable=False) + contract_url = Column(String, nullable=True) + status = Column(String, default="draft") # draft, signed, completed, cancelled + created_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + + # Relationships + sponsorship = relationship("Sponsorship", backref="contracts") + creator = relationship("User", foreign_keys=[creator_id], backref="creator_contracts") + brand = relationship("User", foreign_keys=[brand_id], backref="brand_contracts") + + +# Creator Matches Table (AI-powered matching) +class CreatorMatch(Base): + __tablename__ = "creator_matches" + + id = Column(String, primary_key=True, default=generate_uuid) + brand_id = Column(String, ForeignKey("users.id"), nullable=False) + creator_id = Column(String, ForeignKey("users.id"), nullable=False) + match_score = Column(Float, nullable=True) + matched_at = Column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + + # Relationships + brand = relationship("User", foreign_keys=[brand_id], backref="creator_matches") + creator = relationship("User", foreign_keys=[creator_id], backref="brand_matches") diff --git a/Backend/app/routes/ai_query.py b/Backend/app/routes/ai_query.py new file mode 100644 index 0000000..6022305 --- /dev/null +++ b/Backend/app/routes/ai_query.py @@ -0,0 +1,244 @@ +from fastapi import APIRouter, HTTPException, Query, Depends, Request +from typing import Dict, Any, Optional +from pydantic import BaseModel +import logging +from ..services.ai_router import ai_router +from ..services.redis_client import get_session_state, save_session_state +import uuid + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Define Router +router = APIRouter(prefix="/api/ai", tags=["AI Query"]) + +# Pydantic models for request/response +class AIQueryRequest(BaseModel): + query: str + brand_id: Optional[str] = None + context: Optional[Dict[str, Any]] = None + +class AIQueryResponse(BaseModel): + intent: str + route: Optional[str] = None + parameters: Dict[str, Any] = {} + follow_up_needed: bool = False + follow_up_question: Optional[str] = None + explanation: str + original_query: str + timestamp: str + +@router.post("/query", response_model=AIQueryResponse) +async def process_ai_query(request: AIQueryRequest, http_request: Request): + """ + Process a natural language query through AI and return routing information + """ + try: + # Validate input + if not request.query or len(request.query.strip()) == 0: + raise HTTPException(status_code=400, detail="Query cannot be empty") + + # Process query through AI router + result = await ai_router.process_query( + query=request.query.strip(), + brand_id=request.brand_id + ) + + # --- Hybrid Orchestration Logic --- + # Extended intent-to-parameter mapping for all available routes + intent_param_map = { + "dashboard_overview": {"required": ["brand_id"], "optional": []}, + "brand_profile": {"required": ["user_id"], "optional": []}, + "campaigns": {"required": ["brand_id"], "optional": ["campaign_id"]}, + "creator_matches": {"required": ["brand_id"], "optional": []}, + "creator_search": {"required": ["brand_id"], "optional": ["industry", "min_engagement", "location"]}, + "creator_profile": {"required": ["creator_id", "brand_id"], "optional": []}, + "analytics_performance": {"required": ["brand_id"], "optional": []}, + "analytics_revenue": {"required": ["brand_id"], "optional": []}, + "contracts": {"required": ["brand_id"], "optional": ["contract_id"]}, + } + intent = result.get("route") + params = result.get("parameters", {}) + + # Debug: Log the parameters to understand the type issue + logger.info(f"Intent: {intent}") + logger.info(f"Params: {params}") + logger.info(f"Params type: {type(params)}") + for key, value in params.items(): + logger.info(f" {key}: {value} (type: {type(value)})") + + api_result = None + api_error = None + # Prepare arguments for API calls, including optional params if present + def get_api_args(intent, params): + args = {} + if intent in intent_param_map: + # Add required params + for param in intent_param_map[intent]["required"]: + if params.get(param) is not None: + args[param] = params[param] + # Add optional params if present + for param in intent_param_map[intent]["optional"]: + if params.get(param) is not None: + args[param] = params[param] + return args + + # Check if all required params are present + all_params_present = True + missing_params = [] + if intent in intent_param_map: + for param in intent_param_map[intent]["required"]: + if not params.get(param): + all_params_present = False + missing_params.append(param) + + # Allow queries with only optional params if API supports it (e.g., creator_search with filters) + only_optional_params = False + if intent in intent_param_map and not all_params_present: + # If at least one optional param is present and no required params are present + if ( + len(intent_param_map[intent]["optional"]) > 0 and + all(params.get(p) is None for p in intent_param_map[intent]["required"]) and + any(params.get(p) is not None for p in intent_param_map[intent]["optional"]) + ): + only_optional_params = True + + if (intent and all_params_present) or (intent and only_optional_params): + try: + api_args = get_api_args(intent, params) + # Use aliases for get_campaigns and get_contracts + if intent == "creator_search": + from ..routes.brand_dashboard import search_creators + api_result = await search_creators(**api_args) + elif intent == "dashboard_overview": + from ..routes.brand_dashboard import get_dashboard_overview + api_result = await get_dashboard_overview(**api_args) + elif intent == "creator_matches": + from ..routes.brand_dashboard import get_creator_matches + api_result = await get_creator_matches(**api_args) + elif intent == "brand_profile": + from ..routes.brand_dashboard import get_brand_profile + api_result = await get_brand_profile(**api_args) + elif intent == "campaigns": + from ..routes.brand_dashboard import get_brand_campaigns as get_campaigns + api_result = await get_campaigns(**api_args) + elif intent == "creator_profile": + from ..routes.brand_dashboard import get_creator_profile + api_result = await get_creator_profile(**api_args) + elif intent == "analytics_performance": + from ..routes.brand_dashboard import get_campaign_performance + api_result = await get_campaign_performance(**api_args) + elif intent == "analytics_revenue": + from ..routes.brand_dashboard import get_revenue_analytics + api_result = await get_revenue_analytics(**api_args) + elif intent == "contracts": + from ..routes.brand_dashboard import get_brand_contracts as get_contracts + api_result = await get_contracts(**api_args) + except Exception as api_exc: + logger.error(f"API call failed for intent '{intent}': {api_exc}") + api_error = str(api_exc) + + # Convert to response model, add 'result' field for actual data + response = AIQueryResponse( + intent=result.get("intent", "unknown"), + route=result.get("route"), + parameters=params, + follow_up_needed=not all_params_present and not only_optional_params or api_error is not None, + follow_up_question=(result.get("follow_up_question") if not all_params_present and not only_optional_params else None), + explanation=(result.get("explanation", "") if not api_error else f"An error occurred while processing your request: {api_error}"), + original_query=result.get("original_query", request.query), + timestamp=result.get("timestamp", ""), + ) + # Attach result if available + response_dict = response.dict() + # 1. Get or generate session_id + session_id = http_request.headers.get("X-Session-ID") + if not session_id and request.context: + session_id = request.context.get("session_id") + if not session_id: + session_id = str(uuid.uuid4()) + + # 2. Load previous state from Redis + state = await get_session_state(session_id) + prev_params = state.get("params", {}) + prev_intent = state.get("intent") + + # 3. Merge new params and intent + # Use new intent if present, else previous + intent = result.get("route") or prev_intent + params = {**prev_params, **result.get("parameters", {})} + state["params"] = params + state["intent"] = intent + + # 4. Save updated state to Redis + await save_session_state(session_id, state) + + response_dict["session_id"] = session_id + if api_result is not None: + response_dict["result"] = api_result + if api_error is not None: + response_dict["error"] = api_error + return response_dict + except HTTPException: + raise + except Exception as e: + logger.error(f"Error processing AI query: {e}") + raise HTTPException(status_code=500, detail="Failed to process AI query") + +@router.get("/routes") +async def get_available_routes(): + """ + Get list of available routes that the AI can route to + """ + try: + routes = ai_router.list_available_routes() + return { + "available_routes": routes, + "total_routes": len(routes) + } + except Exception as e: + logger.error(f"Error fetching available routes: {e}") + raise HTTPException(status_code=500, detail="Failed to fetch routes") + +@router.get("/route/{route_name}") +async def get_route_info(route_name: str): + """ + Get detailed information about a specific route + """ + try: + route_info = ai_router.get_route_info(route_name) + if not route_info: + raise HTTPException(status_code=404, detail=f"Route '{route_name}' not found") + + return { + "route_name": route_name, + "info": route_info + } + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching route info: {e}") + raise HTTPException(status_code=500, detail="Failed to fetch route info") + +@router.post("/test") +async def test_ai_query(query: str = Query(..., description="Test query")): + """ + Test endpoint for AI query processing (for development) + """ + try: + # Process test query + result = await ai_router.process_query(query=query) + + return { + "test_query": query, + "result": result, + "status": "success" + } + except Exception as e: + logger.error(f"Error in test AI query: {e}") + return { + "test_query": query, + "error": str(e), + "status": "error" + } \ No newline at end of file diff --git a/Backend/app/routes/brand_dashboard.py b/Backend/app/routes/brand_dashboard.py new file mode 100644 index 0000000..8bf52dd --- /dev/null +++ b/Backend/app/routes/brand_dashboard.py @@ -0,0 +1,1627 @@ +from fastapi import APIRouter, HTTPException, Depends, Query +from typing import List, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from ..db.db import AsyncSessionLocal +from ..models.models import ( + User, Sponsorship, BrandProfile, CampaignMetrics, + Contract, CreatorMatch, SponsorshipApplication +) +from ..schemas.schema import ( + BrandProfileCreate, BrandProfileUpdate, BrandProfileResponse, + CampaignMetricsCreate, CampaignMetricsResponse, + ContractCreate, ContractUpdate, ContractResponse, + CreatorMatchResponse, DashboardOverviewResponse, + CampaignAnalyticsResponse, CreatorMatchAnalyticsResponse, + SponsorshipApplicationResponse, ApplicationUpdateRequest, ApplicationSummaryResponse, + PaymentResponse, PaymentStatusUpdate, PaymentAnalyticsResponse, + CampaignMetricsUpdate, SponsorshipCreate +) + +import os +from supabase import create_client, Client +from dotenv import load_dotenv +import uuid +from datetime import datetime, timezone +import logging + +# Load environment variables +load_dotenv() +url: str = os.getenv("SUPABASE_URL") +key: str = os.getenv("SUPABASE_KEY") +supabase: Client = create_client(url, key) + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Define Router +router = APIRouter(prefix="/api/brand", tags=["Brand Dashboard"]) + +# Helper Functions +def generate_uuid(): + return str(uuid.uuid4()) + +def current_timestamp(): + return datetime.now(timezone.utc).isoformat() + +# Security Helper Functions +def validate_brand_access(brand_id: str, current_user_id: str): + """Validate that the current user can access the brand data""" + if brand_id != current_user_id: + raise HTTPException(status_code=403, detail="Access denied: You can only access your own data") + return True + +def require_brand_role(user_role: str): + """Ensure user has brand role""" + if user_role != "brand": + raise HTTPException(status_code=403, detail="Access denied: Brand role required") + return True + +def validate_uuid_format(id_value: str, field_name: str = "ID"): + """Validate UUID format""" + if not id_value or len(id_value) != 36: + raise HTTPException(status_code=400, detail=f"Invalid {field_name} format") + return True + +def safe_supabase_query(query_func, error_message: str = "Database operation failed"): + """Safely execute Supabase queries with proper error handling""" + try: + result = query_func() + return result.data if result.data else [] + except Exception as e: + logger.error(f"Supabase error in {error_message}: {e}") + raise HTTPException(status_code=500, detail=error_message) + +# Simple in-memory rate limiting (for development) +request_counts = {} + +def check_rate_limit(user_id: str, max_requests: int = 100, window_seconds: int = 60): + """Simple rate limiting check (in production, use Redis)""" + current_time = datetime.now(timezone.utc) + key = f"{user_id}:{current_time.minute}" + + if key not in request_counts: + request_counts[key] = 0 + + request_counts[key] += 1 + + if request_counts[key] > max_requests: + raise HTTPException(status_code=429, detail="Rate limit exceeded") + + return True + +# ============================================================================ +# DASHBOARD OVERVIEW ROUTES +# ============================================================================ + +@router.get("/dashboard/overview", response_model=DashboardOverviewResponse) +async def get_dashboard_overview(brand_id: str = Query(..., description="Brand user ID")): + """ + Get dashboard overview with key metrics for a brand + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get brand's campaigns + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch campaigns" + ) + + # Get brand's profile + profile_result = supabase.table("brand_profiles").select("*").eq("user_id", brand_id).execute() + profile = profile_result.data[0] if profile_result.data else None + + # Get recent applications (only if campaigns exist) + applications = [] + if campaigns: + campaign_ids = [campaign["id"] for campaign in campaigns] + applications = safe_supabase_query( + lambda: supabase.table("sponsorship_applications").select("*").in_("sponsorship_id", campaign_ids).execute(), + "Failed to fetch applications" + ) + + # Calculate metrics + total_campaigns = len(campaigns) + active_campaigns = len([c for c in campaigns if c.get("status") == "open"]) + + # Calculate total revenue from completed payments + payments = safe_supabase_query( + lambda: supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).eq("status", "completed").execute(), + "Failed to fetch payments" + ) + total_revenue = sum(float(payment.get("amount", 0)) for payment in payments) + + # Get creator matches + matches = safe_supabase_query( + lambda: supabase.table("creator_matches").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch creator matches" + ) + total_creators_matched = len(matches) + + # Recent activity (last 5 applications) + recent_activity = applications[:5] if applications else [] + + return DashboardOverviewResponse( + total_campaigns=total_campaigns, + active_campaigns=active_campaigns, + total_revenue=total_revenue, + total_creators_matched=total_creators_matched, + recent_activity=recent_activity + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in dashboard overview: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/kpis") +async def get_dashboard_kpis(brand_id: str = Query(..., description="Brand user ID")): + """ + Get comprehensive KPI data for brand dashboard + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get brand's campaigns + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch campaigns" + ) + + # Calculate campaign metrics + total_campaigns = len(campaigns) + active_campaigns = len([c for c in campaigns if c.get("status") == "open"]) + + # Get campaign metrics for engagement and reach calculations + campaign_metrics = [] + total_reach = 0 + total_engagement = 0 + total_impressions = 0 + + if campaigns: + campaign_ids = [campaign["id"] for campaign in campaigns] + campaign_metrics = safe_supabase_query( + lambda: supabase.table("campaign_metrics").select("*").in_("campaign_id", campaign_ids).execute(), + "Failed to fetch campaign metrics" + ) + + # Calculate total reach and engagement + for metric in campaign_metrics: + total_impressions += metric.get("impressions", 0) + total_engagement += metric.get("engagement_rate", 0) * metric.get("impressions", 0) + + # Calculate average engagement rate (cap at 100%) + avg_engagement_rate = min((total_engagement / total_impressions * 100) if total_impressions > 0 else 0, 100) + + # Get payment data for financial metrics + all_payments = safe_supabase_query( + lambda: supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch payments" + ) + + completed_payments = [p for p in all_payments if p.get("status") == "completed"] + pending_payments = [p for p in all_payments if p.get("status") == "pending"] + + # Calculate financial metrics + total_spent = sum(float(payment.get("amount", 0)) for payment in completed_payments) + pending_amount = sum(float(payment.get("amount", 0)) for payment in pending_payments) + + # Calculate ROI (assuming revenue is tracked in campaign_metrics) + total_revenue = sum(float(metric.get("revenue", 0)) for metric in campaign_metrics) + roi_percentage = ((total_revenue - total_spent) / total_spent * 100) if total_spent > 0 else 0 + + # Calculate cost per engagement + total_engagements = sum(metric.get("clicks", 0) for metric in campaign_metrics) + cost_per_engagement = (total_spent / total_engagements) if total_engagements > 0 else 0 + + # Get creator matches for creator metrics + creator_matches = safe_supabase_query( + lambda: supabase.table("creator_matches").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch creator matches" + ) + + # Get applications for activity metrics + applications = [] + if campaigns: + campaign_ids = [campaign["id"] for campaign in campaigns] + applications = safe_supabase_query( + lambda: supabase.table("sponsorship_applications").select("*").in_("sponsorship_id", campaign_ids).execute(), + "Failed to fetch applications" + ) + + pending_applications = len([app for app in applications if app.get("status") == "pending"]) + + # Format reach for display (convert to K/M format) + def format_reach(number): + if number >= 1000000: + return f"{number/1000000:.1f}M" + elif number >= 1000: + return f"{number/1000:.1f}K" + else: + return str(number) + + return { + "kpis": { + "activeCampaigns": active_campaigns, + "totalReach": format_reach(total_impressions), + "engagementRate": round(avg_engagement_rate, 1), + "roi": round(roi_percentage, 1), + "budgetSpent": total_spent, + "budgetAllocated": total_spent + pending_amount, + "costPerEngagement": round(cost_per_engagement, 2) + }, + "creators": { + "totalConnected": len(creator_matches), + "pendingApplications": pending_applications, + "topPerformers": len([m for m in creator_matches if m.get("match_score", 0) > 0.8]), + "newRecommendations": len([m for m in creator_matches if m.get("match_score", 0) > 0.9]) + }, + "financial": { + "monthlySpend": total_spent, + "pendingPayments": pending_amount, + "costPerEngagement": cost_per_engagement, + "budgetUtilization": round((total_spent / (total_spent + pending_amount)) * 100, 1) if (total_spent + pending_amount) > 0 else 0 + }, + "analytics": { + "audienceGrowth": 12.5, # Will be replaced by real analytics endpoint + "bestContentType": "Video", # Will be replaced by real analytics endpoint + "topGeographicMarket": "United States", # Will be replaced by real analytics endpoint + "trendingTopics": ["Sustainability", "Tech Reviews", "Fitness"] # Will be replaced by real analytics endpoint + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in dashboard KPIs: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/campaigns/overview") +async def get_campaigns_overview(brand_id: str = Query(..., description="Brand user ID")): + """ + Get campaigns overview for dashboard + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get brand's campaigns + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch campaigns" + ) + + # Get campaign metrics for each campaign + campaigns_with_metrics = [] + + for campaign in campaigns: + campaign_metrics = safe_supabase_query( + lambda: supabase.table("campaign_metrics").select("*").eq("campaign_id", campaign["id"]).execute(), + f"Failed to fetch metrics for campaign {campaign['id']}" + ) + + # Get latest metrics for this campaign + latest_metrics = campaign_metrics[-1] if campaign_metrics else {} + + # Calculate performance rating + engagement_rate = latest_metrics.get("engagement_rate", 0) + if engagement_rate >= 5.0: + performance = "excellent" + elif engagement_rate >= 4.0: + performance = "good" + elif engagement_rate >= 3.0: + performance = "average" + else: + performance = "poor" + + # Format reach + impressions = latest_metrics.get("impressions", 0) + if impressions >= 1000000: + reach = f"{impressions/1000000:.1f}M" + elif impressions >= 1000: + reach = f"{impressions/1000:.1f}K" + else: + reach = str(impressions) + + campaigns_with_metrics.append({ + "id": campaign["id"], + "name": campaign["title"], + "status": campaign.get("status", "draft"), + "performance": performance, + "reach": reach, + "engagement": round(engagement_rate, 1), + "deadline": campaign.get("deadline", campaign.get("created_at", "")), + "budget": campaign.get("budget", 0) + }) + + # Sort by recent campaigns first + campaigns_with_metrics.sort(key=lambda x: x["deadline"], reverse=True) + + return { + "campaigns": campaigns_with_metrics[:5] # Return top 5 recent campaigns + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in campaigns overview: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/notifications") +async def get_dashboard_notifications(brand_id: str = Query(..., description="Brand user ID")): + """ + Get notifications for brand dashboard + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + notifications = [] + + # Get pending applications + applications = safe_supabase_query( + lambda: supabase.table("sponsorship_applications").select("*").eq("status", "pending").execute(), + "Failed to fetch applications" + ) + + # Filter applications for this brand's campaigns + brand_campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("id").eq("brand_id", brand_id).execute(), + "Failed to fetch brand campaigns" + ) + + brand_campaign_ids = [campaign["id"] for campaign in brand_campaigns] + pending_applications = [app for app in applications if app.get("sponsorship_id") in brand_campaign_ids] + + if pending_applications: + notifications.append({ + "id": "1", + "type": "urgent", + "message": f"{len(pending_applications)} applications need review", + "time": "2 hours ago" + }) + + # Check for underperforming campaigns + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).eq("status", "open").execute(), + "Failed to fetch campaigns" + ) + + for campaign in campaigns: + campaign_metrics = safe_supabase_query( + lambda: supabase.table("campaign_metrics").select("*").eq("campaign_id", campaign["id"]).execute(), + f"Failed to fetch metrics for campaign {campaign['id']}" + ) + + if campaign_metrics: + latest_metrics = campaign_metrics[-1] + engagement_rate = latest_metrics.get("engagement_rate", 0) + + if engagement_rate < 3.0: # Underperforming threshold + notifications.append({ + "id": f"campaign_{campaign['id']}", + "type": "alert", + "message": f"Campaign '{campaign['title']}' underperforming", + "time": "4 hours ago" + }) + + # Check for new creator recommendations + creator_matches = safe_supabase_query( + lambda: supabase.table("creator_matches").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch creator matches" + ) + + high_score_matches = [m for m in creator_matches if m.get("match_score", 0) > 0.9] + + if high_score_matches: + notifications.append({ + "id": "3", + "type": "info", + "message": "New creator recommendations available", + "time": "1 day ago" + }) + + # Add some mock notifications for demonstration + if not notifications: + notifications = [ + { + "id": "1", + "type": "urgent", + "message": "3 applications need review", + "time": "2 hours ago" + }, + { + "id": "2", + "type": "alert", + "message": "Campaign 'Tech Review' underperforming", + "time": "4 hours ago" + }, + { + "id": "3", + "type": "info", + "message": "New creator recommendations available", + "time": "1 day ago" + } + ] + + return { + "notifications": notifications + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in notifications: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/timeline") +async def get_dashboard_timeline(brand_id: str = Query(..., description="Brand user ID")): + """ + Get timeline data for dashboard + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + timeline_items = [] + + # Get campaigns with deadlines + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).eq("status", "open").execute(), + "Failed to fetch campaigns" + ) + + for campaign in campaigns: + if campaign.get("deadline"): + timeline_items.append({ + "id": f"campaign_{campaign['id']}", + "type": "campaign_deadline", + "title": "Campaign Deadline", + "description": f"{campaign['title']} - {campaign['deadline'][:10]}", + "date": campaign["deadline"], + "priority": "high" + }) + + # Get payments with due dates + payments = safe_supabase_query( + lambda: supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).eq("status", "pending").execute(), + "Failed to fetch payments" + ) + + for payment in payments: + if payment.get("due_date"): + timeline_items.append({ + "id": f"payment_{payment['id']}", + "type": "payment_due", + "title": "Payment Due", + "description": f"Creator Payment - ${payment['amount']}", + "date": payment["due_date"], + "priority": "medium" + }) + + # Get content review deadlines (mock data for now) + timeline_items.append({ + "id": "content_review_1", + "type": "content_review", + "title": "Content Review", + "description": "Tech Review Video - Aug 14", + "date": "2024-08-14", + "priority": "medium" + }) + + # Sort by date + timeline_items.sort(key=lambda x: x["date"]) + + return { + "timeline": timeline_items[:5] # Return top 5 items + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in timeline: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/test-brand") +async def get_test_brand(): + """ + Get a test brand ID for testing dashboard endpoints + """ + try: + # Get the first brand user + brand_result = supabase.table("users").select("id, username").eq("role", "brand").limit(1).execute() + + if brand_result.data: + brand = brand_result.data[0] + return { + "brand_id": brand["id"], + "username": brand["username"], + "message": "Use this brand_id for testing dashboard endpoints" + } + else: + return { + "message": "No brand users found in database", + "brand_id": None + } + + except Exception as e: + logger.error(f"Error getting test brand: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +# ============================================================================ +# BRAND PROFILE ROUTES +# ============================================================================ + +@router.post("/profile", response_model=BrandProfileResponse) +async def create_brand_profile(profile: BrandProfileCreate): + """ + Create a new brand profile + """ + try: + profile_id = generate_uuid() + t = current_timestamp() + + response = supabase.table("brand_profiles").insert({ + "id": profile_id, + "user_id": profile.user_id, + "company_name": profile.company_name, + "website": profile.website, + "industry": profile.industry, + "contact_person": profile.contact_person, + "contact_email": profile.contact_email, + "created_at": t + }).execute() + + if response.data: + return BrandProfileResponse(**response.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create brand profile") + + except Exception as e: + logger.error(f"Error creating brand profile: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/profile/{user_id}", response_model=BrandProfileResponse) +async def get_brand_profile(user_id: str): + """ + Get brand profile by user ID + """ + try: + result = supabase.table("brand_profiles").select("*").eq("user_id", user_id).execute() + + if result.data: + return BrandProfileResponse(**result.data[0]) + else: + raise HTTPException(status_code=404, detail="Brand profile not found") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching brand profile: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/profile/{user_id}", response_model=BrandProfileResponse) +async def update_brand_profile(user_id: str, profile_update: BrandProfileUpdate): + """ + Update brand profile + """ + try: + update_data = profile_update.dict(exclude_unset=True) + + response = supabase.table("brand_profiles").update(update_data).eq("user_id", user_id).execute() + + if response.data: + return BrandProfileResponse(**response.data[0]) + else: + raise HTTPException(status_code=404, detail="Brand profile not found") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating brand profile: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +# ============================================================================ +# CAMPAIGN MANAGEMENT ROUTES +# ============================================================================ + +@router.get("/campaigns") +async def get_brand_campaigns(brand_id: str = Query(..., description="Brand user ID")): + """ + Get all campaigns for a brand + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch brand campaigns" + ) + + return campaigns + +@router.get("/campaigns/{campaign_id}") +async def get_campaign_details(campaign_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Get specific campaign details + """ + # Validate IDs format + validate_uuid_format(campaign_id, "campaign_id") + validate_uuid_format(brand_id, "brand_id") + + try: + result = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + + if result.data: + return result.data[0] + else: + raise HTTPException(status_code=404, detail="Campaign not found") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching campaign details: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.post("/campaigns") +async def create_campaign(campaign: SponsorshipCreate): + """ + Create a new campaign + """ + # Validate brand_id format + validate_uuid_format(campaign.brand_id, "brand_id") + + # Additional business logic validation + if campaign.budget and campaign.budget < 0: + raise HTTPException(status_code=400, detail="Budget cannot be negative") + + if campaign.engagement_minimum and campaign.engagement_minimum < 0: + raise HTTPException(status_code=400, detail="Engagement minimum cannot be negative") + + try: + campaign_id = generate_uuid() + t = current_timestamp() + + response = supabase.table("sponsorships").insert({ + "id": campaign_id, + "brand_id": campaign.brand_id, + "title": campaign.title, + "description": campaign.description, + "required_audience": campaign.required_audience, + "budget": campaign.budget, + "engagement_minimum": campaign.engagement_minimum, + "status": "open", + "created_at": t + }).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to create campaign") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error creating campaign: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/campaigns/{campaign_id}") +async def update_campaign(campaign_id: str, campaign_update: dict, brand_id: str = Query(..., description="Brand user ID")): + """ + Update campaign details + """ + try: + # Verify campaign belongs to brand + existing = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + if not existing.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + response = supabase.table("sponsorships").update(campaign_update).eq("id", campaign_id).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to update campaign") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating campaign: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.delete("/campaigns/{campaign_id}") +async def delete_campaign(campaign_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Delete a campaign + """ + try: + # Verify campaign belongs to brand + existing = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + if not existing.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + response = supabase.table("sponsorships").delete().eq("id", campaign_id).execute() + + return {"message": "Campaign deleted successfully"} + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting campaign: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +# ============================================================================ +# CREATOR MATCHING ROUTES +# ============================================================================ + +@router.get("/creators/matches", response_model=List[CreatorMatchResponse]) +async def get_creator_matches(brand_id: str = Query(..., description="Brand user ID")): + """ + Get AI-matched creators for a brand + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + result = supabase.table("creator_matches").select("*").eq("brand_id", brand_id).order("match_score", desc=True).execute() + + matches = [] + if result.data: + for match in result.data: + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", match["creator_id"]).execute() + if creator_result.data: + creator = creator_result.data[0] + match["creator_name"] = creator.get("username", "Unknown") + match["creator_role"] = creator.get("role", "creator") + + matches.append(CreatorMatchResponse(**match)) + + return matches + + except Exception as e: + logger.error(f"Error fetching creator matches: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/creators/search") +async def search_creators( + brand_id: str = Query(..., description="Brand user ID"), + industry: Optional[str] = Query(None, description="Industry filter"), + min_engagement: Optional[float] = Query(None, description="Minimum engagement rate"), + location: Optional[str] = Query(None, description="Location filter") +): + """ + Search for creators based on criteria + """ + try: + # Get all creators + creators_result = supabase.table("users").select("*").eq("role", "creator").execute() + creators = creators_result.data if creators_result.data else [] + + # Get audience insights for filtering + insights_result = supabase.table("audience_insights").select("*").execute() + insights = insights_result.data if insights_result.data else [] + + # Create insights lookup + insights_lookup = {insight["user_id"]: insight for insight in insights} + + # Filter creators based on criteria + filtered_creators = [] + for creator in creators: + creator_insights = insights_lookup.get(creator["id"]) + + # Apply filters + if min_engagement and creator_insights: + if creator_insights.get("engagement_rate", 0) < min_engagement: + continue + + # Add creator with insights + creator_data = { + **creator, + "audience_insights": creator_insights + } + filtered_creators.append(creator_data) + + return filtered_creators + + except Exception as e: + logger.error(f"Error searching creators: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/creators/{creator_id}/profile") +async def get_creator_profile(creator_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Get detailed creator profile + """ + try: + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", creator_id).eq("role", "creator").execute() + if not creator_result.data: + raise HTTPException(status_code=404, detail="Creator not found") + + creator = creator_result.data[0] + + # Get creator's audience insights + insights_result = supabase.table("audience_insights").select("*").eq("user_id", creator_id).execute() + insights = insights_result.data[0] if insights_result.data else None + + # Get creator's posts + posts_result = supabase.table("user_posts").select("*").eq("user_id", creator_id).execute() + posts = posts_result.data if posts_result.data else [] + + # Calculate match score (simplified algorithm) + match_score = 0.85 # Placeholder - would implement actual AI matching + + return { + "creator": creator, + "audience_insights": insights, + "posts": posts, + "match_score": match_score + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching creator profile: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +# ============================================================================ +# ANALYTICS ROUTES +# ============================================================================ + +@router.get("/analytics/performance") +async def get_campaign_performance(brand_id: str = Query(..., description="Brand user ID")): + """ + Get campaign performance analytics + """ + try: + # Get brand's campaigns + campaigns_result = supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute() + campaigns = campaigns_result.data if campaigns_result.data else [] + + # Get campaign metrics + metrics_result = supabase.table("campaign_metrics").select("*").execute() + metrics = metrics_result.data if metrics_result.data else [] + + # Create metrics lookup + metrics_lookup = {metric["campaign_id"]: metric for metric in metrics} + + # Calculate performance for each campaign + performance_data = [] + for campaign in campaigns: + campaign_metrics = metrics_lookup.get(campaign["id"], {}) + + performance = { + "campaign_id": campaign["id"], + "campaign_title": campaign["title"], + "impressions": campaign_metrics.get("impressions", 0), + "clicks": campaign_metrics.get("clicks", 0), + "conversions": campaign_metrics.get("conversions", 0), + "revenue": float(campaign_metrics.get("revenue", 0)), + "engagement_rate": campaign_metrics.get("engagement_rate", 0), + "roi": 0.0 # Calculate ROI based on budget and revenue + } + + # Calculate ROI + if campaign.get("budget") and performance["revenue"]: + performance["roi"] = (performance["revenue"] - float(campaign["budget"])) / float(campaign["budget"]) * 100 + + performance_data.append(performance) + + return performance_data + + except Exception as e: + logger.error(f"Error fetching campaign performance: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/analytics/revenue") +async def get_revenue_analytics(brand_id: str = Query(..., description="Brand user ID")): + """ + Get revenue analytics + """ + try: + # Get completed payments + payments_result = supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).eq("status", "completed").execute() + payments = payments_result.data if payments_result.data else [] + + # Calculate revenue metrics + total_revenue = sum(float(payment.get("amount", 0)) for payment in payments) + avg_payment = total_revenue / len(payments) if payments else 0 + + # Get pending payments + pending_result = supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).eq("status", "pending").execute() + pending_payments = pending_result.data if pending_result.data else [] + pending_revenue = sum(float(payment.get("amount", 0)) for payment in pending_payments) + + return { + "total_revenue": total_revenue, + "average_payment": avg_payment, + "pending_revenue": pending_revenue, + "total_payments": len(payments), + "pending_payments": len(pending_payments) + } + + except Exception as e: + logger.error(f"Error fetching revenue analytics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +# ============================================================================ +# CONTRACT MANAGEMENT ROUTES +# ============================================================================ + +@router.get("/contracts") +async def get_brand_contracts(brand_id: str = Query(..., description="Brand user ID")): + """ + Get all contracts for a brand + """ + try: + result = supabase.table("contracts").select("*").eq("brand_id", brand_id).execute() + return result.data if result.data else [] + + except Exception as e: + logger.error(f"Error fetching brand contracts: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.post("/contracts") +async def create_contract(contract: ContractCreate): + """ + Create a new contract + """ + try: + contract_id = generate_uuid() + t = current_timestamp() + + response = supabase.table("contracts").insert({ + "id": contract_id, + "sponsorship_id": contract.sponsorship_id, + "creator_id": contract.creator_id, + "brand_id": contract.brand_id, + "contract_url": contract.contract_url, + "status": contract.status, + "created_at": t + }).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to create contract") + + except Exception as e: + logger.error(f"Error creating contract: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/contracts/{contract_id}/status") +async def update_contract_status( + contract_id: str, + status: str = Query(..., description="New contract status"), + brand_id: str = Query(..., description="Brand user ID") +): + """ + Update contract status + """ + try: + # Verify contract belongs to brand + existing = supabase.table("contracts").select("*").eq("id", contract_id).eq("brand_id", brand_id).execute() + if not existing.data: + raise HTTPException(status_code=404, detail="Contract not found") + + response = supabase.table("contracts").update({"status": status}).eq("id", contract_id).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to update contract status") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating contract status: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + + +# ============================================================================ +# APPLICATION MANAGEMENT ROUTES +# ============================================================================ + +@router.get("/applications", response_model=List[SponsorshipApplicationResponse]) +async def get_brand_applications(brand_id: str = Query(..., description="Brand user ID")): + """ + Get all applications for brand's campaigns + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get brand's campaigns first + campaigns = safe_supabase_query( + lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch campaigns" + ) + + if not campaigns: + return [] + + # Get applications for these campaigns + campaign_ids = [campaign["id"] for campaign in campaigns] + applications = safe_supabase_query( + lambda: supabase.table("sponsorship_applications").select("*").in_("sponsorship_id", campaign_ids).execute(), + "Failed to fetch applications" + ) + + # Enhance applications with creator and campaign details + enhanced_applications = [] + for application in applications: + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", application["creator_id"]).execute() + creator = creator_result.data[0] if creator_result.data else None + + # Get campaign details + campaign_result = supabase.table("sponsorships").select("*").eq("id", application["sponsorship_id"]).execute() + campaign = campaign_result.data[0] if campaign_result.data else None + + enhanced_application = { + **application, + "creator": creator, + "campaign": campaign + } + enhanced_applications.append(enhanced_application) + + return enhanced_applications + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching brand applications: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/applications/{application_id}", response_model=SponsorshipApplicationResponse) +async def get_application_details(application_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Get specific application details + """ + # Validate IDs format + validate_uuid_format(application_id, "application_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Get application + application_result = supabase.table("sponsorship_applications").select("*").eq("id", application_id).execute() + if not application_result.data: + raise HTTPException(status_code=404, detail="Application not found") + + application = application_result.data[0] + + # Verify this application belongs to brand's campaign + campaign_result = supabase.table("sponsorships").select("*").eq("id", application["sponsorship_id"]).eq("brand_id", brand_id).execute() + if not campaign_result.data: + raise HTTPException(status_code=403, detail="Access denied: Application not found in your campaigns") + + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", application["creator_id"]).execute() + creator = creator_result.data[0] if creator_result.data else None + + # Get campaign details + campaign = campaign_result.data[0] + + enhanced_application = { + **application, + "creator": creator, + "campaign": campaign + } + + return enhanced_application + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching application details: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/applications/{application_id}") +async def update_application_status( + application_id: str, + update_data: ApplicationUpdateRequest, + brand_id: str = Query(..., description="Brand user ID") +): + """ + Update application status (accept/reject) + """ + # Validate IDs format + validate_uuid_format(application_id, "application_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Verify application belongs to brand's campaign + application_result = supabase.table("sponsorship_applications").select("*").eq("id", application_id).execute() + if not application_result.data: + raise HTTPException(status_code=404, detail="Application not found") + + application = application_result.data[0] + campaign_result = supabase.table("sponsorships").select("*").eq("id", application["sponsorship_id"]).eq("brand_id", brand_id).execute() + if not campaign_result.data: + raise HTTPException(status_code=403, detail="Access denied: Application not found in your campaigns") + + # Update application status + update_payload = {"status": update_data.status} + if update_data.notes: + update_payload["notes"] = update_data.notes + + response = supabase.table("sponsorship_applications").update(update_payload).eq("id", application_id).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to update application") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating application status: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/applications/summary", response_model=ApplicationSummaryResponse) +async def get_applications_summary(brand_id: str = Query(..., description="Brand user ID")): + """ + Get applications summary and statistics + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get all applications for brand's campaigns + applications = await get_brand_applications(brand_id) + + # Calculate summary + total_applications = len(applications) + pending_applications = len([app for app in applications if app["status"] == "pending"]) + accepted_applications = len([app for app in applications if app["status"] == "accepted"]) + rejected_applications = len([app for app in applications if app["status"] == "rejected"]) + + # Group by campaign + applications_by_campaign = {} + for app in applications: + campaign_title = app.get("campaign", {}).get("title", "Unknown Campaign") + applications_by_campaign[campaign_title] = applications_by_campaign.get(campaign_title, 0) + 1 + + # Recent applications (last 5) + recent_applications = applications[:5] if applications else [] + + return ApplicationSummaryResponse( + total_applications=total_applications, + pending_applications=pending_applications, + accepted_applications=accepted_applications, + rejected_applications=rejected_applications, + applications_by_campaign=applications_by_campaign, + recent_applications=recent_applications + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching applications summary: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + + +# ============================================================================ +# PAYMENT MANAGEMENT ROUTES +# ============================================================================ + +@router.get("/payments", response_model=List[PaymentResponse]) +async def get_brand_payments(brand_id: str = Query(..., description="Brand user ID")): + """ + Get all payments for brand + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + payments = safe_supabase_query( + lambda: supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).execute(), + "Failed to fetch payments" + ) + + # Enhance payments with creator and campaign details + enhanced_payments = [] + for payment in payments: + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", payment["creator_id"]).execute() + creator = creator_result.data[0] if creator_result.data else None + + # Get campaign details + campaign_result = supabase.table("sponsorships").select("*").eq("id", payment["sponsorship_id"]).execute() + campaign = campaign_result.data[0] if campaign_result.data else None + + enhanced_payment = { + **payment, + "creator": creator, + "campaign": campaign + } + enhanced_payments.append(enhanced_payment) + + return enhanced_payments + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching brand payments: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/payments/{payment_id}", response_model=PaymentResponse) +async def get_payment_details(payment_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Get specific payment details + """ + # Validate IDs format + validate_uuid_format(payment_id, "payment_id") + validate_uuid_format(brand_id, "brand_id") + + try: + payment_result = supabase.table("sponsorship_payments").select("*").eq("id", payment_id).eq("brand_id", brand_id).execute() + if not payment_result.data: + raise HTTPException(status_code=404, detail="Payment not found") + + payment = payment_result.data[0] + + # Get creator details + creator_result = supabase.table("users").select("*").eq("id", payment["creator_id"]).execute() + creator = creator_result.data[0] if creator_result.data else None + + # Get campaign details + campaign_result = supabase.table("sponsorships").select("*").eq("id", payment["sponsorship_id"]).execute() + campaign = campaign_result.data[0] if campaign_result.data else None + + enhanced_payment = { + **payment, + "creator": creator, + "campaign": campaign + } + + return enhanced_payment + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching payment details: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/payments/{payment_id}/status") +async def update_payment_status( + payment_id: str, + status_update: PaymentStatusUpdate, + brand_id: str = Query(..., description="Brand user ID") +): + """ + Update payment status + """ + # Validate IDs format + validate_uuid_format(payment_id, "payment_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Verify payment belongs to brand + payment_result = supabase.table("sponsorship_payments").select("*").eq("id", payment_id).eq("brand_id", brand_id).execute() + if not payment_result.data: + raise HTTPException(status_code=404, detail="Payment not found") + + # Update payment status + response = supabase.table("sponsorship_payments").update({"status": status_update.status}).eq("id", payment_id).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to update payment status") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating payment status: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/payments/analytics", response_model=PaymentAnalyticsResponse) +async def get_payment_analytics(brand_id: str = Query(..., description="Brand user ID")): + """ + Get payment analytics + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + payments = await get_brand_payments(brand_id) + + # Calculate analytics + total_payments = len(payments) + completed_payments = len([p for p in payments if p["status"] == "completed"]) + pending_payments = len([p for p in payments if p["status"] == "pending"]) + total_amount = sum(float(p["amount"]) for p in payments if p["status"] == "completed") + average_payment = total_amount / completed_payments if completed_payments > 0 else 0 + + # Group by month (simplified) + payments_by_month = {} + for payment in payments: + if payment["status"] == "completed": + month = payment["transaction_date"][:7] if payment["transaction_date"] else "unknown" + payments_by_month[month] = payments_by_month.get(month, 0) + float(payment["amount"]) + + return PaymentAnalyticsResponse( + total_payments=total_payments, + completed_payments=completed_payments, + pending_payments=pending_payments, + total_amount=total_amount, + average_payment=average_payment, + payments_by_month=payments_by_month + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching payment analytics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + + +# ============================================================================ +# CAMPAIGN METRICS MANAGEMENT ROUTES +# ============================================================================ + +@router.post("/campaigns/{campaign_id}/metrics") +async def add_campaign_metrics( + campaign_id: str, + metrics: CampaignMetricsUpdate, + brand_id: str = Query(..., description="Brand user ID") +): + """ + Add metrics to a campaign + """ + # Validate IDs format + validate_uuid_format(campaign_id, "campaign_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Verify campaign belongs to brand + campaign_result = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + if not campaign_result.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Create metrics record + metrics_id = generate_uuid() + t = current_timestamp() + + metrics_data = { + "id": metrics_id, + "campaign_id": campaign_id, + "impressions": metrics.impressions, + "clicks": metrics.clicks, + "conversions": metrics.conversions, + "revenue": metrics.revenue, + "engagement_rate": metrics.engagement_rate, + "recorded_at": t + } + + response = supabase.table("campaign_metrics").insert(metrics_data).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=400, detail="Failed to add campaign metrics") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error adding campaign metrics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/campaigns/{campaign_id}/metrics") +async def get_campaign_metrics(campaign_id: str, brand_id: str = Query(..., description="Brand user ID")): + """ + Get metrics for a specific campaign + """ + # Validate IDs format + validate_uuid_format(campaign_id, "campaign_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Verify campaign belongs to brand + campaign_result = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + if not campaign_result.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Get campaign metrics + metrics = safe_supabase_query( + lambda: supabase.table("campaign_metrics").select("*").eq("campaign_id", campaign_id).execute(), + "Failed to fetch campaign metrics" + ) + + return metrics + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error fetching campaign metrics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.put("/campaigns/{campaign_id}/metrics/{metrics_id}") +async def update_campaign_metrics( + campaign_id: str, + metrics_id: str, + metrics_update: CampaignMetricsUpdate, + brand_id: str = Query(..., description="Brand user ID") +): + """ + Update campaign metrics + """ + # Validate IDs format + validate_uuid_format(campaign_id, "campaign_id") + validate_uuid_format(metrics_id, "metrics_id") + validate_uuid_format(brand_id, "brand_id") + + try: + # Verify campaign belongs to brand + campaign_result = supabase.table("sponsorships").select("*").eq("id", campaign_id).eq("brand_id", brand_id).execute() + if not campaign_result.data: + raise HTTPException(status_code=404, detail="Campaign not found") + + # Update metrics + update_data = metrics_update.dict(exclude_unset=True) + response = supabase.table("campaign_metrics").update(update_data).eq("id", metrics_id).eq("campaign_id", campaign_id).execute() + + if response.data: + return response.data[0] + else: + raise HTTPException(status_code=404, detail="Metrics not found") + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating campaign metrics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@router.get("/dashboard/analytics") +async def get_dashboard_analytics(brand_id: str = Query(..., description="Brand user ID")): + """ + Get real analytics data for brand dashboard + """ + # Validate brand_id format + validate_uuid_format(brand_id, "brand_id") + + try: + # Get creator matches for this brand + creator_matches = safe_supabase_query( + lambda: supabase.table("creator_matches").select("creator_id").eq("brand_id", brand_id).execute(), + "Failed to fetch creator matches" + ) + + creator_ids = [match["creator_id"] for match in creator_matches] + + if not creator_ids: + return { + "analytics": { + "audienceGrowth": 0, + "bestContentType": "No data", + "topGeographicMarket": "No data", + "trendingTopics": [] + } + } + + # 1. Calculate Audience Growth + audience_growth = 0 + try: + # Get audience insights for creators + audience_data = safe_supabase_query( + lambda: supabase.table("audience_insights").select("*").in_("user_id", creator_ids).execute(), + "Failed to fetch audience insights" + ) + + if audience_data: + # Calculate growth from engagement rates + total_engagement = sum(float(insight.get("engagement_rate", 0)) for insight in audience_data) + avg_engagement = total_engagement / len(audience_data) if audience_data else 0 + audience_growth = min(avg_engagement * 2.5, 25.0) # Realistic growth calculation + except Exception as e: + logger.error(f"Error calculating audience growth: {e}") + audience_growth = 12.5 # Fallback + + # 2. Analyze Best Content Type + best_content_type = "Video" # Default + try: + # Get posts from creators + posts_data = safe_supabase_query( + lambda: supabase.table("user_posts").select("*").in_("user_id", creator_ids).execute(), + "Failed to fetch posts" + ) + + if posts_data: + # Analyze content type performance + content_performance = {} + for post in posts_data: + content_type = post.get("content_type", "post") + engagement = post.get("engagement_metrics", {}) + likes = int(engagement.get("likes", 0)) + + if content_type not in content_performance: + content_performance[content_type] = {"total_likes": 0, "count": 0} + + content_performance[content_type]["total_likes"] += likes + content_performance[content_type]["count"] += 1 + + # Find best performing content type + if content_performance: + best_type = max(content_performance.keys(), + key=lambda x: content_performance[x]["total_likes"] / content_performance[x]["count"]) + best_content_type = best_type.title() + except Exception as e: + logger.error(f"Error analyzing content types: {e}") + + # 3. Analyze Top Geographic Market + top_market = "United States" # Default + try: + # Get audience insights with geographic data + audience_insights = safe_supabase_query( + lambda: supabase.table("audience_insights").select("top_markets").in_("user_id", creator_ids).execute(), + "Failed to fetch audience insights" + ) + + if audience_insights: + market_totals = {} + for insight in audience_insights: + top_markets = insight.get("top_markets", {}) + if isinstance(top_markets, dict): + for market, percentage in top_markets.items(): + if market not in market_totals: + market_totals[market] = 0 + market_totals[market] += float(percentage) + + if market_totals: + top_market = max(market_totals.keys(), key=lambda x: market_totals[x]) + except Exception as e: + logger.error(f"Error analyzing geographic markets: {e}") + + # 4. Analyze Trending Topics + trending_topics = [] + try: + # Get posts and analyze categories + posts_data = safe_supabase_query( + lambda: supabase.table("user_posts").select("category, engagement_metrics").in_("user_id", creator_ids).execute(), + "Failed to fetch posts for trending analysis" + ) + + if posts_data: + category_performance = {} + for post in posts_data: + category = post.get("category", "General") + engagement = post.get("engagement_metrics", {}) + likes = int(engagement.get("likes", 0)) + + if category not in category_performance: + category_performance[category] = {"total_likes": 0, "count": 0} + + category_performance[category]["total_likes"] += likes + category_performance[category]["count"] += 1 + + # Get top 3 trending categories + if category_performance: + sorted_categories = sorted(category_performance.keys(), + key=lambda x: category_performance[x]["total_likes"] / category_performance[x]["count"], + reverse=True) + trending_topics = sorted_categories[:3] + except Exception as e: + logger.error(f"Error analyzing trending topics: {e}") + trending_topics = ["Tech Reviews", "Fashion", "Fitness"] # Fallback + + return { + "analytics": { + "audienceGrowth": round(audience_growth, 1), + "bestContentType": best_content_type, + "topGeographicMarket": top_market, + "trendingTopics": trending_topics + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Unexpected error in analytics: {e}") + raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file diff --git a/Backend/app/routes/contracts.py b/Backend/app/routes/contracts.py new file mode 100644 index 0000000..1f2621c --- /dev/null +++ b/Backend/app/routes/contracts.py @@ -0,0 +1,774 @@ +from fastapi import APIRouter, HTTPException, Depends, Query +from typing import List, Optional, Dict, Any +from datetime import datetime, date +from pydantic import BaseModel +import httpx +import os +from supabase import create_client, Client +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() +url: str = os.getenv("SUPABASE_URL") +key: str = os.getenv("SUPABASE_KEY") +supabase: Client = create_client(url, key) + +router = APIRouter(prefix="/api/contracts", tags=["contracts"]) + +# ============================================================================ +# PYDANTIC MODELS FOR CONTRACTS +# ============================================================================ + +class ContractBase(BaseModel): + sponsorship_id: Optional[str] = None + creator_id: str + brand_id: str + contract_title: Optional[str] = None + contract_type: str = "one-time" + terms_and_conditions: Optional[Dict[str, Any]] = None + payment_terms: Optional[Dict[str, Any]] = None + deliverables: Optional[Dict[str, Any]] = None + start_date: Optional[date] = None + end_date: Optional[date] = None + total_budget: Optional[float] = None + payment_schedule: Optional[Dict[str, Any]] = None + legal_compliance: Optional[Dict[str, Any]] = None + +class ContractCreate(ContractBase): + pass + +class ContractUpdate(BaseModel): + contract_title: Optional[str] = None + contract_type: Optional[str] = None + terms_and_conditions: Optional[Dict[str, Any]] = None + payment_terms: Optional[Dict[str, Any]] = None + deliverables: Optional[Dict[str, Any]] = None + start_date: Optional[date] = None + end_date: Optional[date] = None + total_budget: Optional[float] = None + payment_schedule: Optional[Dict[str, Any]] = None + legal_compliance: Optional[Dict[str, Any]] = None + status: Optional[str] = None + +class ContractResponse(ContractBase): + id: str + contract_url: Optional[str] = None + status: str + created_at: datetime + updated_at: Optional[datetime] = None + +class ContractTemplateBase(BaseModel): + template_name: str + template_type: str + industry: Optional[str] = None + terms_template: Optional[Dict[str, Any]] = None + payment_terms_template: Optional[Dict[str, Any]] = None + deliverables_template: Optional[Dict[str, Any]] = None + is_public: bool = False + +class ContractTemplateCreate(ContractTemplateBase): + pass + +class ContractTemplateResponse(ContractTemplateBase): + id: str + created_by: Optional[str] = None + is_active: bool + created_at: datetime + updated_at: datetime + +class MilestoneBase(BaseModel): + milestone_name: str + description: Optional[str] = None + due_date: date + payment_amount: float + completion_criteria: Optional[Dict[str, Any]] = None + +class MilestoneCreate(MilestoneBase): + pass + +class MilestoneUpdate(BaseModel): + milestone_name: Optional[str] = None + description: Optional[str] = None + due_date: Optional[date] = None + payment_amount: Optional[float] = None + status: Optional[str] = None + completion_criteria: Optional[Dict[str, Any]] = None + +class MilestoneResponse(MilestoneBase): + id: str + contract_id: str + status: str + completed_at: Optional[datetime] = None + created_at: datetime + updated_at: datetime + +class DeliverableBase(BaseModel): + deliverable_type: str + description: Optional[str] = None + platform: str + requirements: Optional[Dict[str, Any]] = None + due_date: date + +class DeliverableCreate(DeliverableBase): + pass + +class DeliverableUpdate(BaseModel): + deliverable_type: Optional[str] = None + description: Optional[str] = None + platform: Optional[str] = None + requirements: Optional[Dict[str, Any]] = None + due_date: Optional[date] = None + status: Optional[str] = None + content_url: Optional[str] = None + approval_status: Optional[str] = None + approval_notes: Optional[str] = None + +class DeliverableResponse(DeliverableBase): + id: str + contract_id: str + status: str + content_url: Optional[str] = None + approval_status: str + approval_notes: Optional[str] = None + submitted_at: Optional[datetime] = None + approved_at: Optional[datetime] = None + created_at: datetime + updated_at: datetime + +class PaymentBase(BaseModel): + amount: float + payment_type: str + due_date: date + payment_method: Optional[str] = None + payment_notes: Optional[str] = None + +class PaymentCreate(PaymentBase): + pass + +class PaymentUpdate(BaseModel): + amount: Optional[float] = None + payment_type: Optional[str] = None + status: Optional[str] = None + due_date: Optional[date] = None + paid_date: Optional[datetime] = None + payment_method: Optional[str] = None + transaction_id: Optional[str] = None + payment_notes: Optional[str] = None + +class PaymentResponse(PaymentBase): + id: str + contract_id: str + milestone_id: Optional[str] = None + status: str + paid_date: Optional[datetime] = None + transaction_id: Optional[str] = None + created_at: datetime + updated_at: datetime + +class CommentBase(BaseModel): + comment: str + comment_type: str = "general" + is_internal: bool = False + parent_comment_id: Optional[str] = None + +class CommentCreate(CommentBase): + pass + +class CommentResponse(CommentBase): + id: str + contract_id: str + user_id: str + created_at: datetime + +class AnalyticsResponse(BaseModel): + id: str + contract_id: str + performance_metrics: Optional[Dict[str, Any]] = None + engagement_data: Optional[Dict[str, Any]] = None + revenue_generated: float = 0 + roi_percentage: float = 0 + cost_per_engagement: float = 0 + cost_per_click: float = 0 + recorded_at: datetime + +class NotificationResponse(BaseModel): + id: str + contract_id: str + user_id: str + notification_type: str + title: str + message: str + is_read: bool + created_at: datetime + +# ============================================================================ +# CONTRACT CRUD OPERATIONS +# ============================================================================ + +@router.post("/", response_model=ContractResponse) +async def create_contract(contract: ContractCreate): + """Create a new contract""" + try: + # Insert contract + result = supabase.table("contracts").insert({ + "sponsorship_id": contract.sponsorship_id, + "creator_id": contract.creator_id, + "brand_id": contract.brand_id, + "contract_title": contract.contract_title, + "contract_type": contract.contract_type, + "terms_and_conditions": contract.terms_and_conditions, + "payment_terms": contract.payment_terms, + "deliverables": contract.deliverables, + "start_date": contract.start_date.isoformat() if contract.start_date else None, + "end_date": contract.end_date.isoformat() if contract.end_date else None, + "total_budget": contract.total_budget, + "payment_schedule": contract.payment_schedule, + "legal_compliance": contract.legal_compliance, + "status": "draft" + }).execute() + + if result.data: + return ContractResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create contract") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating contract: {str(e)}") + +@router.get("/", response_model=List[ContractResponse]) +async def get_contracts( + brand_id: Optional[str] = Query(None, description="Filter by brand ID"), + creator_id: Optional[str] = Query(None, description="Filter by creator ID"), + status: Optional[str] = Query(None, description="Filter by status"), + limit: int = Query(50, description="Number of contracts to return"), + offset: int = Query(0, description="Number of contracts to skip") +): + """Get all contracts with optional filtering""" + try: + query = supabase.table("contracts").select("*") + + if brand_id: + query = query.eq("brand_id", brand_id) + if creator_id: + query = query.eq("creator_id", creator_id) + if status: + query = query.eq("status", status) + + query = query.range(offset, offset + limit - 1) + result = query.execute() + + return [ContractResponse(**contract) for contract in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching contracts: {str(e)}") + +@router.get("/{contract_id}", response_model=ContractResponse) +async def get_contract(contract_id: str): + """Get a specific contract by ID""" + try: + result = supabase.table("contracts").select("*").eq("id", contract_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Contract not found") + + return ContractResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching contract: {str(e)}") + +@router.put("/{contract_id}", response_model=ContractResponse) +async def update_contract(contract_id: str, contract_update: ContractUpdate): + """Update a contract""" + try: + update_data = contract_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow().isoformat() + + result = supabase.table("contracts").update(update_data).eq("id", contract_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Contract not found") + + return ContractResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating contract: {str(e)}") + +@router.delete("/{contract_id}") +async def delete_contract(contract_id: str): + """Delete a contract""" + try: + result = supabase.table("contracts").delete().eq("id", contract_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Contract not found") + + return {"message": "Contract deleted successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting contract: {str(e)}") + +# ============================================================================ +# CONTRACT TEMPLATES +# ============================================================================ + +@router.post("/templates", response_model=ContractTemplateResponse) +async def create_contract_template(template: ContractTemplateCreate, user_id: str): + """Create a new contract template""" + try: + result = supabase.table("contract_templates").insert({ + **template.dict(), + "created_by": user_id + }).execute() + + if result.data: + return ContractTemplateResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create template") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating template: {str(e)}") + +@router.get("/templates", response_model=List[ContractTemplateResponse]) +async def get_contract_templates( + template_type: Optional[str] = Query(None, description="Filter by template type"), + industry: Optional[str] = Query(None, description="Filter by industry"), + is_public: Optional[bool] = Query(None, description="Filter by public status"), + limit: int = Query(50, description="Number of templates to return"), + offset: int = Query(0, description="Number of templates to skip") +): + """Get all contract templates with optional filtering""" + try: + query = supabase.table("contract_templates").select("*") + + if template_type: + query = query.eq("template_type", template_type) + if industry: + query = query.eq("industry", industry) + if is_public is not None: + query = query.eq("is_public", is_public) + + query = query.eq("is_active", True).range(offset, offset + limit - 1) + result = query.execute() + + return [ContractTemplateResponse(**template) for template in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching templates: {str(e)}") + +@router.get("/templates/{template_id}", response_model=ContractTemplateResponse) +async def get_contract_template(template_id: str): + """Get a specific contract template by ID""" + try: + result = supabase.table("contract_templates").select("*").eq("id", template_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Template not found") + + return ContractTemplateResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching template: {str(e)}") + +# ============================================================================ +# CONTRACT MILESTONES +# ============================================================================ + +@router.post("/{contract_id}/milestones", response_model=MilestoneResponse) +async def create_milestone(contract_id: str, milestone: MilestoneCreate): + """Create a new milestone for a contract""" + try: + result = supabase.table("contract_milestones").insert({ + "contract_id": contract_id, + **milestone.dict() + }).execute() + + if result.data: + return MilestoneResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create milestone") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating milestone: {str(e)}") + +@router.get("/{contract_id}/milestones", response_model=List[MilestoneResponse]) +async def get_contract_milestones(contract_id: str): + """Get all milestones for a contract""" + try: + result = supabase.table("contract_milestones").select("*").eq("contract_id", contract_id).execute() + + return [MilestoneResponse(**milestone) for milestone in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching milestones: {str(e)}") + +@router.put("/milestones/{milestone_id}", response_model=MilestoneResponse) +async def update_milestone(milestone_id: str, milestone_update: MilestoneUpdate): + """Update a milestone""" + try: + update_data = milestone_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow().isoformat() + + result = supabase.table("contract_milestones").update(update_data).eq("id", milestone_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Milestone not found") + + return MilestoneResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating milestone: {str(e)}") + +@router.delete("/milestones/{milestone_id}") +async def delete_milestone(milestone_id: str): + """Delete a milestone""" + try: + result = supabase.table("contract_milestones").delete().eq("id", milestone_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Milestone not found") + + return {"message": "Milestone deleted successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting milestone: {str(e)}") + +# ============================================================================ +# CONTRACT DELIVERABLES +# ============================================================================ + +@router.post("/{contract_id}/deliverables", response_model=DeliverableResponse) +async def create_deliverable(contract_id: str, deliverable: DeliverableCreate): + """Create a new deliverable for a contract""" + try: + result = supabase.table("contract_deliverables").insert({ + "contract_id": contract_id, + **deliverable.dict() + }).execute() + + if result.data: + return DeliverableResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create deliverable") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating deliverable: {str(e)}") + +@router.get("/{contract_id}/deliverables", response_model=List[DeliverableResponse]) +async def get_contract_deliverables(contract_id: str): + """Get all deliverables for a contract""" + try: + result = supabase.table("contract_deliverables").select("*").eq("contract_id", contract_id).execute() + + return [DeliverableResponse(**deliverable) for deliverable in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching deliverables: {str(e)}") + +@router.put("/deliverables/{deliverable_id}", response_model=DeliverableResponse) +async def update_deliverable(deliverable_id: str, deliverable_update: DeliverableUpdate): + """Update a deliverable""" + try: + update_data = deliverable_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow().isoformat() + + result = supabase.table("contract_deliverables").update(update_data).eq("id", deliverable_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Deliverable not found") + + return DeliverableResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating deliverable: {str(e)}") + +@router.delete("/deliverables/{deliverable_id}") +async def delete_deliverable(deliverable_id: str): + """Delete a deliverable""" + try: + result = supabase.table("contract_deliverables").delete().eq("id", deliverable_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Deliverable not found") + + return {"message": "Deliverable deleted successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting deliverable: {str(e)}") + +# ============================================================================ +# CONTRACT PAYMENTS +# ============================================================================ + +@router.post("/{contract_id}/payments", response_model=PaymentResponse) +async def create_payment(contract_id: str, payment: PaymentCreate, milestone_id: Optional[str] = None): + """Create a new payment for a contract""" + try: + result = supabase.table("contract_payments").insert({ + "contract_id": contract_id, + "milestone_id": milestone_id, + **payment.dict() + }).execute() + + if result.data: + return PaymentResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create payment") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating payment: {str(e)}") + +@router.get("/{contract_id}/payments", response_model=List[PaymentResponse]) +async def get_contract_payments(contract_id: str): + """Get all payments for a contract""" + try: + result = supabase.table("contract_payments").select("*").eq("contract_id", contract_id).execute() + + return [PaymentResponse(**payment) for payment in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching payments: {str(e)}") + +@router.put("/payments/{payment_id}", response_model=PaymentResponse) +async def update_payment(payment_id: str, payment_update: PaymentUpdate): + """Update a payment""" + try: + update_data = payment_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow().isoformat() + + result = supabase.table("contract_payments").update(update_data).eq("id", payment_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Payment not found") + + return PaymentResponse(**result.data[0]) + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating payment: {str(e)}") + +@router.delete("/payments/{payment_id}") +async def delete_payment(payment_id: str): + """Delete a payment""" + try: + result = supabase.table("contract_payments").delete().eq("id", payment_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Payment not found") + + return {"message": "Payment deleted successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting payment: {str(e)}") + +# ============================================================================ +# CONTRACT COMMENTS +# ============================================================================ + +@router.post("/{contract_id}/comments", response_model=CommentResponse) +async def create_comment(contract_id: str, comment: CommentCreate, user_id: str): + """Create a new comment for a contract""" + try: + result = supabase.table("contract_comments").insert({ + "contract_id": contract_id, + "user_id": user_id, + **comment.dict() + }).execute() + + if result.data: + return CommentResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create comment") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating comment: {str(e)}") + +@router.get("/{contract_id}/comments", response_model=List[CommentResponse]) +async def get_contract_comments(contract_id: str): + """Get all comments for a contract""" + try: + result = supabase.table("contract_comments").select("*").eq("contract_id", contract_id).order("created_at", desc=True).execute() + + return [CommentResponse(**comment) for comment in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching comments: {str(e)}") + +@router.delete("/comments/{comment_id}") +async def delete_comment(comment_id: str): + """Delete a comment""" + try: + result = supabase.table("contract_comments").delete().eq("id", comment_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Comment not found") + + return {"message": "Comment deleted successfully"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting comment: {str(e)}") + +# ============================================================================ +# CONTRACT ANALYTICS +# ============================================================================ + +@router.get("/{contract_id}/analytics", response_model=List[AnalyticsResponse]) +async def get_contract_analytics(contract_id: str): + """Get analytics for a contract""" + try: + result = supabase.table("contract_analytics").select("*").eq("contract_id", contract_id).order("recorded_at", desc=True).execute() + + return [AnalyticsResponse(**analytics) for analytics in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching analytics: {str(e)}") + +@router.post("/{contract_id}/analytics", response_model=AnalyticsResponse) +async def create_contract_analytics(contract_id: str, analytics_data: Dict[str, Any]): + """Create analytics entry for a contract""" + try: + result = supabase.table("contract_analytics").insert({ + "contract_id": contract_id, + **analytics_data + }).execute() + + if result.data: + return AnalyticsResponse(**result.data[0]) + else: + raise HTTPException(status_code=400, detail="Failed to create analytics entry") + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating analytics: {str(e)}") + +# ============================================================================ +# CONTRACT NOTIFICATIONS +# ============================================================================ + +@router.get("/{contract_id}/notifications", response_model=List[NotificationResponse]) +async def get_contract_notifications(contract_id: str, user_id: Optional[str] = None): + """Get notifications for a contract""" + try: + query = supabase.table("contract_notifications").select("*").eq("contract_id", contract_id) + + if user_id: + query = query.eq("user_id", user_id) + + result = query.order("created_at", desc=True).execute() + + return [NotificationResponse(**notification) for notification in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching notifications: {str(e)}") + +@router.put("/notifications/{notification_id}/read") +async def mark_notification_read(notification_id: str): + """Mark a notification as read""" + try: + result = supabase.table("contract_notifications").update({"is_read": True}).eq("id", notification_id).execute() + + if not result.data: + raise HTTPException(status_code=404, detail="Notification not found") + + return {"message": "Notification marked as read"} + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating notification: {str(e)}") + +# ============================================================================ +# CONTRACT STATISTICS +# ============================================================================ + +@router.get("/stats/overview") +async def get_contracts_overview(brand_id: Optional[str] = None, creator_id: Optional[str] = None): + """Get overview statistics for contracts""" + try: + # Base query + query = supabase.table("contracts").select("*") + + if brand_id: + query = query.eq("brand_id", brand_id) + if creator_id: + query = query.eq("creator_id", creator_id) + + result = query.execute() + contracts = result.data + + # Calculate statistics + total_contracts = len(contracts) + active_contracts = len([c for c in contracts if c.get("status") in ["signed", "active"]]) + completed_contracts = len([c for c in contracts if c.get("status") == "completed"]) + draft_contracts = len([c for c in contracts if c.get("status") == "draft"]) + + total_budget = sum(c.get("total_budget", 0) for c in contracts if c.get("total_budget")) + + return { + "total_contracts": total_contracts, + "active_contracts": active_contracts, + "completed_contracts": completed_contracts, + "draft_contracts": draft_contracts, + "total_budget": total_budget, + "average_contract_value": total_budget / total_contracts if total_contracts > 0 else 0 + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching contract statistics: {str(e)}") + +# ============================================================================ +# CONTRACT SEARCH +# ============================================================================ + +@router.get("/search") +async def search_contracts( + query: str = Query(..., description="Search term"), + brand_id: Optional[str] = Query(None, description="Filter by brand ID"), + creator_id: Optional[str] = Query(None, description="Filter by creator ID"), + status: Optional[str] = Query(None, description="Filter by status"), + limit: int = Query(20, description="Number of results to return") +): + """Search contracts by title, description, or other fields""" + try: + # Build search query + search_query = supabase.table("contracts").select("*") + + # Add filters + if brand_id: + search_query = search_query.eq("brand_id", brand_id) + if creator_id: + search_query = search_query.eq("creator_id", creator_id) + if status: + search_query = search_query.eq("status", status) + + # Add text search (this is a simplified version - you might want to use full-text search) + # For now, we'll search in contract_title + result = search_query.ilike("contract_title", f"%{query}%").limit(limit).execute() + + return [ContractResponse(**contract) for contract in result.data] + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error searching contracts: {str(e)}") \ No newline at end of file diff --git a/Backend/app/schemas/schema.py b/Backend/app/schemas/schema.py index 7389488..e1b2d75 100644 --- a/Backend/app/schemas/schema.py +++ b/Backend/app/schemas/schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Optional, Dict +from typing import Optional, Dict, List from datetime import datetime class UserCreate(BaseModel): @@ -51,3 +51,192 @@ class CollaborationCreate(BaseModel): creator_1_id: str creator_2_id: str collaboration_details: str + + +# ============================================================================ +# BRAND DASHBOARD SCHEMAS +# ============================================================================ + +# Brand Profile Schemas +class BrandProfileCreate(BaseModel): + user_id: str + company_name: Optional[str] = None + website: Optional[str] = None + industry: Optional[str] = None + contact_person: Optional[str] = None + contact_email: Optional[str] = None + +class BrandProfileUpdate(BaseModel): + company_name: Optional[str] = None + website: Optional[str] = None + industry: Optional[str] = None + contact_person: Optional[str] = None + contact_email: Optional[str] = None + +class BrandProfileResponse(BaseModel): + id: str + user_id: str + company_name: Optional[str] = None + website: Optional[str] = None + industry: Optional[str] = None + contact_person: Optional[str] = None + contact_email: Optional[str] = None + created_at: datetime + + class Config: + from_attributes = True + + +# Campaign Metrics Schemas +class CampaignMetricsCreate(BaseModel): + campaign_id: str + impressions: Optional[int] = None + clicks: Optional[int] = None + conversions: Optional[int] = None + revenue: Optional[float] = None + engagement_rate: Optional[float] = None + +class CampaignMetricsResponse(BaseModel): + id: str + campaign_id: str + impressions: Optional[int] = None + clicks: Optional[int] = None + conversions: Optional[int] = None + revenue: Optional[float] = None + engagement_rate: Optional[float] = None + recorded_at: datetime + + class Config: + from_attributes = True + + +# Contract Schemas +class ContractCreate(BaseModel): + sponsorship_id: str + creator_id: str + brand_id: str + contract_url: Optional[str] = None + status: str = "draft" + +class ContractUpdate(BaseModel): + contract_url: Optional[str] = None + status: Optional[str] = None + +class ContractResponse(BaseModel): + id: str + sponsorship_id: str + creator_id: str + brand_id: str + contract_url: Optional[str] = None + status: str + created_at: datetime + + class Config: + from_attributes = True + + +# Creator Match Schemas +class CreatorMatchResponse(BaseModel): + id: str + brand_id: str + creator_id: str + match_score: Optional[float] = None + matched_at: datetime + + class Config: + from_attributes = True + + +# Dashboard Analytics Schemas +class DashboardOverviewResponse(BaseModel): + total_campaigns: int + active_campaigns: int + total_revenue: float + total_creators_matched: int + recent_activity: list + +class CampaignAnalyticsResponse(BaseModel): + campaign_id: str + campaign_title: str + impressions: int + clicks: int + conversions: int + revenue: float + engagement_rate: float + roi: float + +class CreatorMatchAnalyticsResponse(BaseModel): + creator_id: str + creator_name: str + match_score: float + audience_overlap: float + engagement_rate: float + estimated_reach: int + + +# ============================================================================ +# ADDITIONAL SCHEMAS FOR EXISTING TABLES +# ============================================================================ + +# Application Management Schemas +class SponsorshipApplicationResponse(BaseModel): + id: str + creator_id: str + sponsorship_id: str + post_id: Optional[str] = None + proposal: str + status: str + applied_at: datetime + creator: Optional[Dict] = None # From users table + campaign: Optional[Dict] = None # From sponsorships table + + class Config: + from_attributes = True + +class ApplicationUpdateRequest(BaseModel): + status: str # "accepted", "rejected", "pending" + notes: Optional[str] = None + +class ApplicationSummaryResponse(BaseModel): + total_applications: int + pending_applications: int + accepted_applications: int + rejected_applications: int + applications_by_campaign: Dict[str, int] + recent_applications: List[Dict] + + +# Payment Management Schemas +class PaymentResponse(BaseModel): + id: str + creator_id: str + brand_id: str + sponsorship_id: str + amount: float + status: str + transaction_date: datetime + creator: Optional[Dict] = None # From users table + campaign: Optional[Dict] = None # From sponsorships table + + class Config: + from_attributes = True + +class PaymentStatusUpdate(BaseModel): + status: str # "pending", "completed", "failed", "cancelled" + +class PaymentAnalyticsResponse(BaseModel): + total_payments: int + completed_payments: int + pending_payments: int + total_amount: float + average_payment: float + payments_by_month: Dict[str, float] + + +# Campaign Metrics Management Schemas +class CampaignMetricsUpdate(BaseModel): + impressions: Optional[int] = None + clicks: Optional[int] = None + conversions: Optional[int] = None + revenue: Optional[float] = None + engagement_rate: Optional[float] = None diff --git a/Backend/app/services/ai_router.py b/Backend/app/services/ai_router.py new file mode 100644 index 0000000..c5c8492 --- /dev/null +++ b/Backend/app/services/ai_router.py @@ -0,0 +1,343 @@ +import os +import json +import logging +from datetime import datetime +from typing import Dict, List, Optional, Any +from groq import Groq +from fastapi import HTTPException +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class AIRouter: + def __init__(self): + """Initialize AI Router with Groq client""" + self.groq_api_key = os.getenv("GROQ_API_KEY") + if not self.groq_api_key: + raise ValueError("GROQ_API_KEY environment variable is required") + + self.client = Groq(api_key=self.groq_api_key) + + # Available API routes and their descriptions + self.available_routes = { + "dashboard_overview": { + "endpoint": "/api/brand/dashboard/overview", + "description": "Get dashboard overview with key metrics (total campaigns, revenue, creator matches, recent activity)", + "parameters": ["brand_id"], + "method": "GET" + }, + "brand_profile": { + "endpoint": "/api/brand/profile/{user_id}", + "description": "Get or update brand profile information", + "parameters": ["user_id"], + "method": "GET/PUT" + }, + "campaigns": { + "endpoint": "/api/brand/campaigns", + "description": "Manage campaigns (list, create, update, delete)", + "parameters": ["brand_id", "campaign_id (optional)"], + "method": "GET/POST/PUT/DELETE" + }, + "creator_matches": { + "endpoint": "/api/brand/creators/matches", + "description": "Get AI-matched creators for the brand", + "parameters": ["brand_id"], + "method": "GET" + }, + "creator_search": { + "endpoint": "/api/brand/creators/search", + "description": "Search for creators based on criteria (industry, engagement, location)", + "parameters": ["brand_id", "industry (optional)", "min_engagement (optional)", "location (optional)"], + "method": "GET" + }, + "creator_profile": { + "endpoint": "/api/brand/creators/{creator_id}/profile", + "description": "Get detailed creator profile with insights and posts", + "parameters": ["creator_id", "brand_id"], + "method": "GET" + }, + "analytics_performance": { + "endpoint": "/api/brand/analytics/performance", + "description": "Get campaign performance analytics and ROI", + "parameters": ["brand_id"], + "method": "GET" + }, + "analytics_revenue": { + "endpoint": "/api/brand/analytics/revenue", + "description": "Get revenue analytics and payment statistics", + "parameters": ["brand_id"], + "method": "GET" + }, + "contracts": { + "endpoint": "/api/brand/contracts", + "description": "Manage contracts (list, create, update status)", + "parameters": ["brand_id", "contract_id (optional)"], + "method": "GET/POST/PUT" + } + } + + def create_system_prompt(self) -> str: + """Create the system prompt for the LLM""" + routes_info = "\n".join([ + f"- {route_name}: {info['description']} (Parameters: {', '.join(info['parameters'])})" + for route_name, info in self.available_routes.items() + ]) + + return f"""You are an intelligent AI assistant for a brand dashboard. Your job is to understand user queries and route them to the appropriate API endpoints. + +Available API Routes: +{routes_info} + +IMPORTANT: You MUST respond with valid JSON only. No additional text before or after the JSON. + +Your tasks: +1. Understand the user's intent from their natural language query +2. Identify which API route(s) should be called +3. Extract required parameters from the query +4. If information is missing, ask follow-up questions +5. Return a structured response with the action to take + +Response format (MUST be valid JSON): +{{ + "intent": "what the user wants to do", + "route": "route_name or null if follow_up_needed", + "parameters": {{"param_name": "value"}}, + "follow_up_needed": true/false, + "follow_up_question": "question to ask if more info needed", + "explanation": "brief explanation of what you understood" +}} + +Examples of valid responses: + +Query: "Show me my dashboard" +Response: {{"intent": "View dashboard overview", "route": "dashboard_overview", "parameters": {{}}, "follow_up_needed": false, "follow_up_question": null, "explanation": "User wants to see dashboard overview with metrics"}} + +Query: "Find creators in tech" +Response: {{"intent": "Search for creators", "route": "creator_search", "parameters": {{"industry": "tech"}}, "follow_up_needed": false, "follow_up_question": null, "explanation": "User wants to find creators in tech industry"}} + +Query: "Show campaigns" +Response: {{"intent": "List campaigns", "route": "campaigns", "parameters": {{}}, "follow_up_needed": false, "follow_up_question": null, "explanation": "User wants to see their campaigns"}} + +Query: "What's my revenue?" +Response: {{"intent": "View revenue analytics", "route": "analytics_revenue", "parameters": {{}}, "follow_up_needed": false, "follow_up_question": null, "explanation": "User wants to see revenue analytics"}} + +Remember: Always return valid JSON, no extra text.""" + + async def process_query(self, query: str, brand_id: str = None) -> Dict[str, Any]: + """Process a natural language query and return routing information""" + try: + # Create the conversation with system prompt + messages = [ + {"role": "system", "content": self.create_system_prompt()}, + {"role": "user", "content": f"User query: {query}"} + ] + + # Add brand_id context if available + if brand_id: + messages.append({ + "role": "system", + "content": f"Note: The user's brand_id is {brand_id}. Use this for any endpoints that require it." + }) + + # Call Groq LLM with lower temperature for more consistent responses + response = self.client.chat.completions.create( + model="moonshotai/kimi-k2-instruct", # Updated to Kimi K2 instruct + messages=messages, + temperature=0.1, # Lower temperature for more consistent JSON output + max_tokens=1024 # Updated max tokens + ) + + # Parse the response + llm_response = response.choices[0].message.content.strip() + + # Clean the response and try to parse JSON with retry logic + parsed_response = self._parse_json_with_retry(llm_response, query) + + # Validate and enhance the response + enhanced_response = self._enhance_response(parsed_response, brand_id, query) + + logger.info(f"AI Router processed query: '{query}' -> {enhanced_response['intent']}") + return enhanced_response + + except Exception as e: + logger.error(f"Error processing query with AI Router: {e}") + raise HTTPException(status_code=500, detail="AI processing error") + + def _enhance_response(self, response: Dict[str, Any], brand_id: str, original_query: str) -> Dict[str, Any]: + """Enhance the LLM response with additional context and validation""" + + # Add brand_id to parameters if not present and route needs it + if brand_id and response.get("route"): + route_info = self.available_routes.get(response["route"]) + if route_info and "brand_id" in route_info["parameters"]: + if "parameters" not in response: + response["parameters"] = {} + if "brand_id" not in response["parameters"]: + response["parameters"]["brand_id"] = str(brand_id) # Ensure brand_id is string + + # Validate route exists + if response.get("route") and response["route"] not in self.available_routes: + response["route"] = None + response["follow_up_needed"] = True + response["follow_up_question"] = f"I don't recognize that action. Available actions include: {', '.join(self.available_routes.keys())}" + + # Ensure parameter types are correct (brand_id should be string) + if "parameters" in response: + if "brand_id" in response["parameters"]: + response["parameters"]["brand_id"] = str(response["parameters"]["brand_id"]) + + # Add metadata + response["original_query"] = original_query + response["timestamp"] = str(datetime.now()) + + return response + + def _clean_llm_response(self, response: str) -> str: + """Clean LLM response to extract valid JSON""" + # Remove markdown code blocks + if "```json" in response: + start = response.find("```json") + 7 + end = response.find("```", start) + if end != -1: + response = response[start:end].strip() + elif "```" in response: + start = response.find("```") + 3 + end = response.find("```", start) + if end != -1: + response = response[start:end].strip() + + # Remove any text before the first { + if "{" in response: + response = response[response.find("{"):] + + # Remove any text after the last } + if "}" in response: + response = response[:response.rfind("}") + 1] + + return response.strip() + + def _parse_json_with_retry(self, llm_response: str, original_query: str) -> Dict[str, Any]: + """Parse JSON with multiple fallback strategies""" + # Strategy 1: Try direct JSON parsing + try: + return json.loads(llm_response) + except json.JSONDecodeError: + pass + + # Strategy 2: Clean and try again + cleaned_response = self._clean_llm_response(llm_response) + try: + return json.loads(cleaned_response) + except json.JSONDecodeError: + pass + + # Strategy 3: Try to extract JSON from the response + try: + # Look for JSON-like structure + import re + json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' + matches = re.findall(json_pattern, llm_response) + if matches: + return json.loads(matches[0]) + except (json.JSONDecodeError, IndexError): + pass + + # Strategy 4: Create a fallback response based on simple keyword matching + fallback_response = self._create_fallback_response(original_query) + logger.warning(f"Failed to parse LLM response, using fallback: {llm_response[:100]}...") + return fallback_response + + def _create_fallback_response(self, query: str) -> Dict[str, Any]: + """Create a fallback response based on keyword matching""" + query_lower = query.lower() + + # Simple keyword matching + if any(word in query_lower for word in ["dashboard", "overview", "summary"]): + return { + "intent": "View dashboard overview", + "route": "dashboard_overview", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see dashboard overview" + } + elif any(word in query_lower for word in ["campaign", "campaigns"]): + return { + "intent": "List campaigns", + "route": "campaigns", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see their campaigns" + } + elif any(word in query_lower for word in ["creator", "creators", "influencer"]): + if any(word in query_lower for word in ["search", "find", "look"]): + return { + "intent": "Search for creators", + "route": "creator_search", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to search for creators" + } + else: + return { + "intent": "View creator matches", + "route": "creator_matches", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see creator matches" + } + elif any(word in query_lower for word in ["revenue", "money", "earnings", "income"]): + return { + "intent": "View revenue analytics", + "route": "analytics_revenue", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see revenue analytics" + } + elif any(word in query_lower for word in ["performance", "analytics", "metrics"]): + return { + "intent": "View performance analytics", + "route": "analytics_performance", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see performance analytics" + } + elif any(word in query_lower for word in ["contract", "contracts"]): + return { + "intent": "View contracts", + "route": "contracts", + "parameters": {}, + "follow_up_needed": False, + "follow_up_question": None, + "explanation": "User wants to see their contracts" + } + else: + return { + "intent": "unknown", + "route": None, + "parameters": {}, + "follow_up_needed": True, + "follow_up_question": "I didn't understand your request. Could you please rephrase it?", + "explanation": "Failed to parse LLM response, please try again with different wording" + } + + def get_route_info(self, route_name: str) -> Optional[Dict[str, Any]]: + """Get information about a specific route""" + return self.available_routes.get(route_name) + + def list_available_routes(self) -> Dict[str, Any]: + """List all available routes for debugging""" + return self.available_routes + +# Global instance +ai_router = AIRouter() \ No newline at end of file diff --git a/Backend/app/services/ai_services.py b/Backend/app/services/ai_services.py index 30482d3..b66e0af 100644 --- a/Backend/app/services/ai_services.py +++ b/Backend/app/services/ai_services.py @@ -19,7 +19,7 @@ def query_sponsorship_client(info): prompt = f"Extract key details about sponsorship and client interactions from the following:\n\n{info}\n\nRespond in JSON with 'sponsorship_details' and 'client_interaction_summary'." headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} - payload = {"model": "llama3-8b-8192", "messages": [{"role": "user", "content": prompt}], "temperature": 0} + payload = {"model": "moonshotai/kimi-k2-instruct", "messages": [{"role": "user", "content": prompt}], "temperature": 0.6, "max_completion_tokens": 1024} try: response = requests.post(CHATGROQ_API_URL_CHAT, json=payload, headers=headers) diff --git a/Backend/app/services/redis_client.py b/Backend/app/services/redis_client.py index d2fb922..8bd3541 100644 --- a/Backend/app/services/redis_client.py +++ b/Backend/app/services/redis_client.py @@ -1,6 +1,27 @@ import redis.asyncio as redis +import os +import json -redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True) +REDIS_HOST = os.getenv("REDIS_HOST", "your-redis-cloud-host") +REDIS_PORT = int(os.getenv("REDIS_PORT", 12345)) # replace with your port +REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "your-redis-cloud-password") + +redis_client = redis.Redis( + host=REDIS_HOST, + port=REDIS_PORT, + password=REDIS_PASSWORD, + decode_responses=True, + ssl=False # Redis Cloud connection works without SSL +) + +SESSION_TTL = 1800 # 30 minutes + +async def get_session_state(session_id: str): + state = await redis_client.get(f"session:{session_id}") + return json.loads(state) if state else {} + +async def save_session_state(session_id: str, state: dict): + await redis_client.set(f"session:{session_id}", json.dumps(state), ex=SESSION_TTL) async def get_redis(): diff --git a/Backend/requirements.txt b/Backend/requirements.txt index ea1ab73..8c89382 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -53,3 +53,5 @@ urllib3==2.3.0 uvicorn==0.34.0 websockets==14.2 yarl==1.18.3 +groq==0.4.2 +openai==1.12.0 diff --git a/Backend/sql.txt b/Backend/sql.txt index 3ee28b5..8f37394 100644 --- a/Backend/sql.txt +++ b/Backend/sql.txt @@ -39,3 +39,824 @@ INSERT INTO sponsorship_payments (id, creator_id, brand_id, sponsorship_id, amou (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Tech Sponsorship'), 500.00, 'completed', NOW()), (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator2'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Fashion Sponsorship'), 300.00, 'completed', NOW()), (gen_random_uuid(), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM sponsorships WHERE title = 'Gaming Sponsorship'), 400.00, 'pending', NOW()); + +-- ============================================================================ +-- NEW TABLES FOR BRAND DASHBOARD FEATURES +-- ============================================================================ + +-- Create brand_profiles table +CREATE TABLE IF NOT EXISTS brand_profiles ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + user_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + company_name TEXT, + website TEXT, + industry TEXT, + contact_person TEXT, + contact_email TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- Create campaign_metrics table +CREATE TABLE IF NOT EXISTS campaign_metrics ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + campaign_id VARCHAR REFERENCES sponsorships(id) ON DELETE CASCADE, + impressions INT, + clicks INT, + conversions INT, + revenue NUMERIC(10,2), + engagement_rate FLOAT, + recorded_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- Create contracts table +CREATE TABLE IF NOT EXISTS contracts ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + sponsorship_id VARCHAR REFERENCES sponsorships(id) ON DELETE CASCADE, + creator_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + brand_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + contract_url TEXT, + status TEXT DEFAULT 'draft', -- draft, signed, completed, cancelled + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- Create creator_matches table +CREATE TABLE IF NOT EXISTS creator_matches ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + brand_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + creator_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + match_score FLOAT, + matched_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- SAMPLE DATA FOR NEW TABLES +-- ============================================================================ + +-- Insert into brand_profiles table +INSERT INTO brand_profiles (id, user_id, company_name, website, industry, contact_person, contact_email, created_at) VALUES + (gen_random_uuid()::text, (SELECT id FROM users WHERE username = 'brand1'), 'TechCorp Inc.', 'https://techcorp.com', 'Technology', 'John Smith', 'john@techcorp.com', NOW()); + +-- Insert into campaign_metrics table +INSERT INTO campaign_metrics (id, campaign_id, impressions, clicks, conversions, revenue, engagement_rate, recorded_at) VALUES + (gen_random_uuid()::text, (SELECT id FROM sponsorships WHERE title = 'Tech Sponsorship'), 50000, 2500, 125, 2500.00, 4.5, NOW()), + (gen_random_uuid()::text, (SELECT id FROM sponsorships WHERE title = 'Fashion Sponsorship'), 30000, 1500, 75, 1500.00, 3.8, NOW()), + (gen_random_uuid()::text, (SELECT id FROM sponsorships WHERE title = 'Gaming Sponsorship'), 40000, 2000, 100, 2000.00, 4.2, NOW()); + +-- Insert into contracts table +INSERT INTO contracts (id, sponsorship_id, creator_id, brand_id, contract_url, status, created_at) VALUES + (gen_random_uuid()::text, (SELECT id FROM sponsorships WHERE title = 'Tech Sponsorship'), (SELECT id FROM users WHERE username = 'creator1'), (SELECT id FROM users WHERE username = 'brand1'), 'https://contracts.example.com/tech-contract.pdf', 'signed', NOW()), + (gen_random_uuid()::text, (SELECT id FROM sponsorships WHERE title = 'Fashion Sponsorship'), (SELECT id FROM users WHERE username = 'creator2'), (SELECT id FROM users WHERE username = 'brand1'), 'https://contracts.example.com/fashion-contract.pdf', 'draft', NOW()); + +-- Insert into creator_matches table +INSERT INTO creator_matches (id, brand_id, creator_id, match_score, matched_at) VALUES + (gen_random_uuid()::text, (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM users WHERE username = 'creator1'), 0.95, NOW()), + (gen_random_uuid()::text, (SELECT id FROM users WHERE username = 'brand1'), (SELECT id FROM users WHERE username = 'creator2'), 0.87, NOW()); + + +-- ============================================================================ +-- ENHANCE EXISTING TABLES FOR DASHBOARD FUNCTIONALITY +-- ============================================================================ + +-- Add deadline field to sponsorships table +ALTER TABLE sponsorships ADD COLUMN IF NOT EXISTS deadline TIMESTAMP WITH TIME ZONE; + +-- Add due date to sponsorship_payments table +ALTER TABLE sponsorship_payments ADD COLUMN IF NOT EXISTS due_date TIMESTAMP WITH TIME ZONE; + +-- Add content type to user_posts table +ALTER TABLE user_posts ADD COLUMN IF NOT EXISTS content_type VARCHAR(50) DEFAULT 'post'; + +-- Add top markets to audience_insights table +ALTER TABLE audience_insights ADD COLUMN IF NOT EXISTS top_markets JSONB; + +-- Add engagement tracking to campaign_metrics +ALTER TABLE campaign_metrics ADD COLUMN IF NOT EXISTS total_engagements INTEGER DEFAULT 0; + +-- ============================================================================ +-- UPDATE EXISTING DATA WITH SAMPLE VALUES +-- ============================================================================ + +-- Update sponsorships with sample deadlines +UPDATE sponsorships +SET deadline = created_at + INTERVAL '30 days' +WHERE deadline IS NULL; + +-- Update payments with sample due dates +UPDATE sponsorship_payments +SET due_date = transaction_date + INTERVAL '7 days' +WHERE due_date IS NULL; + +-- Update posts with content types +UPDATE user_posts +SET content_type = CASE + WHEN title ILIKE '%video%' OR title ILIKE '%youtube%' THEN 'video' + WHEN title ILIKE '%story%' THEN 'story' + WHEN title ILIKE '%image%' OR title ILIKE '%photo%' THEN 'image' + ELSE 'post' +END +WHERE content_type IS NULL; + +-- Update audience insights with sample top markets +UPDATE audience_insights +SET top_markets = '{"United States": 45, "United Kingdom": 25, "Canada": 15, "Australia": 15}' +WHERE top_markets IS NULL; + +-- Update campaign metrics with engagement data +UPDATE campaign_metrics +SET total_engagements = clicks + conversions +WHERE total_engagements IS NULL; + +-- ============================================================================ +-- SAMPLE DATA FOR DASHBOARD TESTING +-- ============================================================================ + +-- Insert additional sample campaigns with deadlines (only if brand exists) +INSERT INTO sponsorships (id, brand_id, title, description, required_audience, budget, engagement_minimum, status, deadline, created_at) +SELECT + gen_random_uuid(), + brand.id, + 'Summer Collection Launch', + 'Launch campaign for summer fashion collection', + '{"age": ["18-34"], "location": ["USA", "UK"]}', + 8000.00, + 4.5, + 'open', + NOW() + INTERVAL '15 days', + NOW() +FROM users brand +WHERE brand.username = 'brand1' +LIMIT 1; + +INSERT INTO sponsorships (id, brand_id, title, description, required_audience, budget, engagement_minimum, status, deadline, created_at) +SELECT + gen_random_uuid(), + brand.id, + 'Tech Review Series', + 'Series of tech product reviews', + '{"age": ["18-30"], "location": ["USA", "Canada"]}', + 6000.00, + 4.2, + 'open', + NOW() + INTERVAL '20 days', + NOW() +FROM users brand +WHERE brand.username = 'brand1' +LIMIT 1; + +INSERT INTO sponsorships (id, brand_id, title, description, required_audience, budget, engagement_minimum, status, deadline, created_at) +SELECT + gen_random_uuid(), + brand.id, + 'Fitness Challenge', + '30-day fitness challenge campaign', + '{"age": ["18-40"], "location": ["USA", "Australia"]}', + 4000.00, + 3.8, + 'pending', + NOW() + INTERVAL '30 days', + NOW() +FROM users brand +WHERE brand.username = 'brand1' +LIMIT 1; + +-- Insert additional payments with due dates (only if users exist) +INSERT INTO sponsorship_payments (id, creator_id, brand_id, sponsorship_id, amount, status, due_date, transaction_date) +SELECT + gen_random_uuid(), + creator.id, + brand.id, + sponsorship.id, + 1200.00, + 'pending', + NOW() + INTERVAL '5 days', + NOW() +FROM users creator, users brand, sponsorships sponsorship +WHERE creator.username = 'creator1' + AND brand.username = 'brand1' + AND sponsorship.title = 'Summer Collection Launch' +LIMIT 1; + +INSERT INTO sponsorship_payments (id, creator_id, brand_id, sponsorship_id, amount, status, due_date, transaction_date) +SELECT + gen_random_uuid(), + creator.id, + brand.id, + sponsorship.id, + 800.00, + 'pending', + NOW() + INTERVAL '3 days', + NOW() +FROM users creator, users brand, sponsorships sponsorship +WHERE creator.username = 'creator2' + AND brand.username = 'brand1' + AND sponsorship.title = 'Tech Review Series' +LIMIT 1; + +-- Insert additional posts with content types (only if users exist) +INSERT INTO user_posts (id, user_id, title, content, post_url, category, content_type, engagement_metrics, created_at) +SELECT + gen_random_uuid(), + creator.id, + 'Summer Fashion Haul Video', + 'Complete summer fashion haul video', + 'https://example.com/summer-haul', + 'Fashion', + 'video', + '{"likes": 800, "comments": 150, "shares": 80}', + NOW() +FROM users creator +WHERE creator.username = 'creator1' +LIMIT 1; + +INSERT INTO user_posts (id, user_id, title, content, post_url, category, content_type, engagement_metrics, created_at) +SELECT + gen_random_uuid(), + creator.id, + 'Tech Review Story', + 'Quick tech review in story format', + 'https://example.com/tech-story', + 'Tech', + 'story', + '{"likes": 400, "comments": 60, "shares": 30}', + NOW() +FROM users creator +WHERE creator.username = 'creator2' +LIMIT 1; + +INSERT INTO user_posts (id, user_id, title, content, post_url, category, content_type, engagement_metrics, created_at) +SELECT + gen_random_uuid(), + creator.id, + 'Fitness Motivation Image', + 'Motivational fitness post', + 'https://example.com/fitness-motivation', + 'Fitness', + 'image', + '{"likes": 600, "comments": 90, "shares": 45}', + NOW() +FROM users creator +WHERE creator.username = 'creator1' +LIMIT 1; + +-- Insert additional campaign metrics (only if campaigns exist) +INSERT INTO campaign_metrics (id, campaign_id, impressions, clicks, conversions, revenue, engagement_rate, total_engagements, recorded_at) +SELECT + gen_random_uuid(), + sponsorship.id, + 120000, + 6000, + 300, + 6000.00, + 5.0, + 6300, + NOW() +FROM sponsorships sponsorship +WHERE sponsorship.title = 'Summer Collection Launch' +LIMIT 1; + +INSERT INTO campaign_metrics (id, campaign_id, impressions, clicks, conversions, revenue, engagement_rate, total_engagements, recorded_at) +SELECT + gen_random_uuid(), + sponsorship.id, + 80000, + 4000, + 200, + 4000.00, + 5.25, + 4200, + NOW() +FROM sponsorships sponsorship +WHERE sponsorship.title = 'Tech Review Series' +LIMIT 1; + +INSERT INTO campaign_metrics (id, campaign_id, impressions, clicks, conversions, revenue, engagement_rate, total_engagements, recorded_at) +SELECT + gen_random_uuid(), + sponsorship.id, + 60000, + 3000, + 150, + 3000.00, + 5.25, + 3150, + NOW() +FROM sponsorships sponsorship +WHERE sponsorship.title = 'Fitness Challenge' +LIMIT 1; + +-- ============================================================================ +-- VERIFICATION QUERIES +-- ============================================================================ + +-- Check sponsorships with deadlines +SELECT title, deadline, status FROM sponsorships WHERE deadline IS NOT NULL; + +-- Check payments with due dates +SELECT amount, due_date, status FROM sponsorship_payments WHERE due_date IS NOT NULL; + +-- Check posts with content types +SELECT title, content_type, category FROM user_posts WHERE content_type IS NOT NULL; + +-- Check audience insights with top markets +SELECT user_id, top_markets FROM audience_insights WHERE top_markets IS NOT NULL; + +-- Check campaign metrics with engagement data +SELECT campaign_id, impressions, total_engagements, engagement_rate FROM campaign_metrics WHERE total_engagements > 0; + +-- ============================================================================ +-- ADD CREATOR MATCHES FOR ANALYTICS TESTING +-- ============================================================================ + +-- Get the brand ID +DO $$ +DECLARE + brand_id_val VARCHAR; + creator1_id_val VARCHAR; + creator2_id_val VARCHAR; +BEGIN + -- Get brand1 ID + SELECT id INTO brand_id_val FROM users WHERE username = 'brand1' LIMIT 1; + + -- Get creator IDs + SELECT id INTO creator1_id_val FROM users WHERE username = 'creator1' LIMIT 1; + SELECT id INTO creator2_id_val FROM users WHERE username = 'creator2' LIMIT 1; + + -- Insert creator matches if they don't exist + IF brand_id_val IS NOT NULL AND creator1_id_val IS NOT NULL THEN + INSERT INTO creator_matches (id, brand_id, creator_id, match_score, matched_at) + SELECT + gen_random_uuid()::text, + brand_id_val, + creator1_id_val, + 0.95, + NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM creator_matches + WHERE brand_id = brand_id_val AND creator_id = creator1_id_val + ); + END IF; + + IF brand_id_val IS NOT NULL AND creator2_id_val IS NOT NULL THEN + INSERT INTO creator_matches (id, brand_id, creator_id, match_score, matched_at) + SELECT + gen_random_uuid()::text, + brand_id_val, + creator2_id_val, + 0.87, + NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM creator_matches + WHERE brand_id = brand_id_val AND creator_id = creator2_id_val + ); + END IF; + + RAISE NOTICE 'Creator matches added for brand: %', brand_id_val; +END $$; + +-- Verify the creator matches +SELECT + cm.id, + b.username as brand_username, + c.username as creator_username, + cm.match_score, + cm.matched_at +FROM creator_matches cm +JOIN users b ON cm.brand_id = b.id +JOIN users c ON cm.creator_id = c.id +WHERE b.username = 'brand1'; + +-- ============================================================================ +-- UPDATE AUDIENCE INSIGHTS WITH BETTER GEOGRAPHIC DATA +-- ============================================================================ + +-- Update existing audience insights with top_markets data +UPDATE audience_insights +SET top_markets = '{"United States": 45, "United Kingdom": 25, "Canada": 15, "Australia": 15}' +WHERE user_id IN (SELECT id FROM users WHERE username = 'creator1'); + +UPDATE audience_insights +SET top_markets = '{"India": 40, "United States": 30, "Canada": 20, "United Kingdom": 10}' +WHERE user_id IN (SELECT id FROM users WHERE username = 'creator2'); + +-- Add more diverse audience insights for better analytics +INSERT INTO audience_insights (id, user_id, audience_age_group, audience_location, engagement_rate, average_views, time_of_attention, price_expectation, top_markets, created_at) +SELECT + gen_random_uuid(), + u.id, + '{"18-24": 60, "25-34": 40}', + '{"USA": 50, "UK": 30, "Canada": 20}', + 4.2, + 8500, + 110, + 480.00, + '{"United States": 50, "United Kingdom": 30, "Canada": 20}', + NOW() +FROM users u +WHERE u.username = 'brand1' +AND NOT EXISTS ( + SELECT 1 FROM audience_insights WHERE user_id = u.id +); + +-- Verify the updates +SELECT + ai.user_id, + u.username, + ai.top_markets, + ai.engagement_rate +FROM audience_insights ai +JOIN users u ON ai.user_id = u.id +WHERE u.username IN ('creator1', 'creator2', 'brand1'); + +-- ============================================================================ +-- ENHANCED CONTRACTS DATABASE SCHEMA +-- ============================================================================ + +-- ============================================================================ +-- 1. ENHANCE EXISTING CONTRACTS TABLE +-- ============================================================================ + +-- Add missing columns to existing contracts table +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS contract_title TEXT; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS contract_type TEXT DEFAULT 'one-time'; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS terms_and_conditions JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS payment_terms JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS deliverables JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS start_date DATE; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS end_date DATE; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS total_budget NUMERIC(10,2); +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS payment_schedule JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS legal_compliance JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS signature_data JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS version_history JSONB; +ALTER TABLE contracts ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(); + +-- ============================================================================ +-- 2. CONTRACT TEMPLATES TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_templates ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + template_name TEXT NOT NULL, + template_type TEXT NOT NULL, -- 'one-time', 'ongoing', 'performance-based' + industry TEXT, + terms_template JSONB, + payment_terms_template JSONB, + deliverables_template JSONB, + created_by VARCHAR REFERENCES users(id) ON DELETE SET NULL, + is_public BOOLEAN DEFAULT false, + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 3. CONTRACT MILESTONES TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_milestones ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + milestone_name TEXT NOT NULL, + description TEXT, + due_date DATE NOT NULL, + payment_amount NUMERIC(10,2) NOT NULL, + status TEXT DEFAULT 'pending', -- pending, completed, overdue, cancelled + completion_criteria JSONB, + completed_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 4. CONTRACT DELIVERABLES TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_deliverables ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + deliverable_type TEXT NOT NULL, -- 'post', 'video', 'story', 'review', 'live' + description TEXT, + platform TEXT NOT NULL, -- 'instagram', 'youtube', 'tiktok', 'twitter', 'facebook' + requirements JSONB, + due_date DATE NOT NULL, + status TEXT DEFAULT 'pending', -- pending, in_progress, submitted, approved, rejected + content_url TEXT, + approval_status TEXT DEFAULT 'pending', -- pending, approved, rejected, needs_revision + approval_notes TEXT, + submitted_at TIMESTAMP WITH TIME ZONE, + approved_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 5. CONTRACT PAYMENTS TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_payments ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + milestone_id VARCHAR REFERENCES contract_milestones(id) ON DELETE SET NULL, + amount NUMERIC(10,2) NOT NULL, + payment_type TEXT NOT NULL, -- 'advance', 'milestone', 'final', 'bonus' + status TEXT DEFAULT 'pending', -- pending, paid, overdue, cancelled, failed + due_date DATE NOT NULL, + paid_date TIMESTAMP WITH TIME ZONE, + payment_method TEXT, -- 'bank_transfer', 'paypal', 'stripe', 'escrow' + transaction_id TEXT, + payment_notes TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 6. CONTRACT COMMENTS/NEGOTIATIONS TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_comments ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + user_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + comment TEXT NOT NULL, + comment_type TEXT DEFAULT 'general', -- 'negotiation', 'approval', 'general', 'revision' + is_internal BOOLEAN DEFAULT false, + parent_comment_id VARCHAR REFERENCES contract_comments(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 7. CONTRACT ANALYTICS TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_analytics ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + performance_metrics JSONB, -- engagement_rate, reach, impressions, clicks + engagement_data JSONB, -- likes, comments, shares, saves + revenue_generated NUMERIC(10,2) DEFAULT 0, + roi_percentage FLOAT DEFAULT 0, + cost_per_engagement NUMERIC(10,2) DEFAULT 0, + cost_per_click NUMERIC(10,2) DEFAULT 0, + recorded_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- 8. CONTRACT NOTIFICATIONS TABLE +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contract_notifications ( + id VARCHAR PRIMARY KEY DEFAULT gen_random_uuid()::text, + contract_id VARCHAR REFERENCES contracts(id) ON DELETE CASCADE, + user_id VARCHAR REFERENCES users(id) ON DELETE CASCADE, + notification_type TEXT NOT NULL, -- 'milestone_due', 'payment_received', 'deliverable_submitted', 'contract_expiring' + title TEXT NOT NULL, + message TEXT NOT NULL, + is_read BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() +); + +-- ============================================================================ +-- SAMPLE DATA FOR CONTRACTS TABLES +-- ============================================================================ + +-- Get existing user IDs for sample data +DO $$ +DECLARE + brand1_id_val VARCHAR; + creator1_id_val VARCHAR; + creator2_id_val VARCHAR; + sponsorship1_id_val VARCHAR; + contract1_id_val VARCHAR; + contract2_id_val VARCHAR; + contract3_id_val VARCHAR; +BEGIN + -- Get user IDs + SELECT id INTO brand1_id_val FROM users WHERE username = 'brand1' LIMIT 1; + SELECT id INTO creator1_id_val FROM users WHERE username = 'creator1' LIMIT 1; + SELECT id INTO creator2_id_val FROM users WHERE username = 'creator2' LIMIT 1; + SELECT id INTO sponsorship1_id_val FROM sponsorships WHERE title = 'Tech Sponsorship' LIMIT 1; + + -- Insert sample contracts + INSERT INTO contracts (id, sponsorship_id, creator_id, brand_id, contract_title, contract_type, terms_and_conditions, payment_terms, deliverables, start_date, end_date, total_budget, payment_schedule, legal_compliance, status, created_at) + VALUES + (gen_random_uuid()::text, sponsorship1_id_val, creator1_id_val, brand1_id_val, 'Tech Watch Campaign Contract', 'one-time', + '{"content_guidelines": "Must mention product features", "disclosure_requirements": "Clear FTC compliance"}', + '{"payment_schedule": "50% upfront, 50% on completion", "late_fees": "5% per week"}', + '{"deliverables": ["2 Instagram posts", "1 YouTube video", "3 TikTok videos"]}', + '2024-01-15', '2024-02-15', 2500.00, + '{"advance": 1250.00, "final": 1250.00}', + '{"ftc_compliance": true, "disclosure_required": true}', + 'signed', NOW()), + + (gen_random_uuid()::text, NULL, creator2_id_val, brand1_id_val, 'Fashion Collaboration Contract', 'ongoing', + '{"content_guidelines": "Fashion-forward styling", "brand_guidelines": "Must use brand hashtags"}', + '{"payment_schedule": "Monthly payments", "performance_bonus": "10% for high engagement"}', + '{"deliverables": ["4 Instagram posts per month", "2 Stories per week", "1 Reel per month"]}', + '2024-01-01', '2024-06-30', 6000.00, + '{"monthly": 1000.00}', + '{"ftc_compliance": true, "disclosure_required": true}', + 'active', NOW()), + + (gen_random_uuid()::text, NULL, creator1_id_val, brand1_id_val, 'Gaming Setup Review Contract', 'one-time', + '{"content_guidelines": "Honest review required", "disclosure_requirements": "Sponsored content disclosure"}', + '{"payment_schedule": "100% on completion", "bonus": "200 for high engagement"}', + '{"deliverables": ["1 detailed review video", "2 social media posts", "1 blog post"]}', + '2024-02-01', '2024-03-01', 1500.00, + '{"final": 1500.00}', + '{"ftc_compliance": true, "disclosure_required": true}', + 'draft', NOW()) + RETURNING id INTO contract1_id_val; + + -- Get contract IDs for further data insertion + SELECT id INTO contract1_id_val FROM contracts WHERE contract_title = 'Tech Watch Campaign Contract' LIMIT 1; + SELECT id INTO contract2_id_val FROM contracts WHERE contract_title = 'Fashion Collaboration Contract' LIMIT 1; + SELECT id INTO contract3_id_val FROM contracts WHERE contract_title = 'Gaming Setup Review Contract' LIMIT 1; + + -- Insert contract templates + INSERT INTO contract_templates (id, template_name, template_type, industry, terms_template, payment_terms_template, deliverables_template, created_by, is_public, is_active, created_at) + VALUES + (gen_random_uuid()::text, 'Standard Influencer Contract', 'one-time', 'General', + '{"content_guidelines": "Brand guidelines must be followed", "disclosure_requirements": "FTC compliance required"}', + '{"payment_schedule": "50% upfront, 50% on completion", "late_fees": "5% per week"}', + '{"deliverables": ["2-3 social media posts", "1 video content", "Stories coverage"]}', + brand1_id_val, true, true, NOW()), + + (gen_random_uuid()::text, 'Ongoing Collaboration Contract', 'ongoing', 'Fashion', + '{"content_guidelines": "Fashion-forward content", "brand_guidelines": "Use brand hashtags"}', + '{"payment_schedule": "Monthly payments", "performance_bonus": "10% for high engagement"}', + '{"deliverables": ["4 posts per month", "2 stories per week", "1 reel per month"]}', + brand1_id_val, true, true, NOW()), + + (gen_random_uuid()::text, 'Tech Review Contract', 'one-time', 'Technology', + '{"content_guidelines": "Honest review required", "disclosure_requirements": "Sponsored content disclosure"}', + '{"payment_schedule": "100% on completion", "bonus": "200 for high engagement"}', + '{"deliverables": ["1 review video", "2 social posts", "1 blog post"]}', + brand1_id_val, true, true, NOW()); + + -- Insert contract milestones + INSERT INTO contract_milestones (id, contract_id, milestone_name, description, due_date, payment_amount, status, completion_criteria, created_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, 'Content Creation', 'Create initial content drafts', '2024-01-25', 1250.00, 'completed', + '{"drafts_submitted": true, "brand_approval": true}', NOW()), + + (gen_random_uuid()::text, contract1_id_val, 'Content Publication', 'Publish all content across platforms', '2024-02-10', 1250.00, 'pending', + '{"all_posts_published": true, "engagement_metrics": "minimum 5% engagement"}', NOW()), + + (gen_random_uuid()::text, contract2_id_val, 'January Content', 'Complete January content deliverables', '2024-01-31', 1000.00, 'completed', + '{"4_posts_published": true, "stories_completed": true}', NOW()), + + (gen_random_uuid()::text, contract2_id_val, 'February Content', 'Complete February content deliverables', '2024-02-29', 1000.00, 'in_progress', + '{"4_posts_published": true, "stories_completed": true}', NOW()); + + -- Insert contract deliverables + INSERT INTO contract_deliverables (id, contract_id, deliverable_type, description, platform, requirements, due_date, status, content_url, approval_status, created_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, 'post', 'Instagram post featuring the tech watch', 'instagram', + '{"image_requirements": "High quality", "caption_requirements": "Include product features", "hashtags": ["#tech", "#watch"]}', + '2024-01-30', 'approved', 'https://instagram.com/p/example1', 'approved', NOW()), + + (gen_random_uuid()::text, contract1_id_val, 'video', 'YouTube review video of the tech watch', 'youtube', + '{"video_length": "5-10 minutes", "content_requirements": "Honest review with pros and cons", "thumbnail_requirements": "Eye-catching design"}', + '2024-02-05', 'submitted', 'https://youtube.com/watch?v=example1', 'pending', NOW()), + + (gen_random_uuid()::text, contract1_id_val, 'story', 'Instagram stories showcasing the watch', 'instagram', + '{"story_count": "3-5 stories", "content_requirements": "Behind the scenes and product features"}', + '2024-02-08', 'in_progress', NULL, 'pending', NOW()), + + (gen_random_uuid()::text, contract2_id_val, 'post', 'Fashion collaboration post', 'instagram', + '{"image_requirements": "Fashion-forward styling", "caption_requirements": "Include brand hashtags"}', + '2024-01-15', 'approved', 'https://instagram.com/p/example2', 'approved', NOW()), + + (gen_random_uuid()::text, contract2_id_val, 'story', 'Fashion stories', 'instagram', + '{"story_count": "2 stories", "content_requirements": "Styling tips and product features"}', + '2024-01-20', 'approved', 'https://instagram.com/stories/example2', 'approved', NOW()); + + -- Insert contract payments + INSERT INTO contract_payments (id, contract_id, milestone_id, amount, payment_type, status, due_date, paid_date, payment_method, transaction_id, payment_notes, created_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, (SELECT id FROM contract_milestones WHERE milestone_name = 'Content Creation' LIMIT 1), + 1250.00, 'advance', 'paid', '2024-01-15', '2024-01-15', 'bank_transfer', 'TXN001', 'Advance payment for content creation', NOW()), + + (gen_random_uuid()::text, contract1_id_val, (SELECT id FROM contract_milestones WHERE milestone_name = 'Content Publication' LIMIT 1), + 1250.00, 'final', 'pending', '2024-02-10', NULL, NULL, NULL, 'Final payment upon completion', NOW()), + + (gen_random_uuid()::text, contract2_id_val, (SELECT id FROM contract_milestones WHERE milestone_name = 'January Content' LIMIT 1), + 1000.00, 'milestone', 'paid', '2024-01-31', '2024-01-31', 'paypal', 'TXN002', 'January content payment', NOW()), + + (gen_random_uuid()::text, contract2_id_val, (SELECT id FROM contract_milestones WHERE milestone_name = 'February Content' LIMIT 1), + 1000.00, 'milestone', 'pending', '2024-02-29', NULL, NULL, NULL, 'February content payment', NOW()); + + -- Insert contract comments + INSERT INTO contract_comments (id, contract_id, user_id, comment, comment_type, is_internal, created_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, brand1_id_val, 'Great initial content! Please ensure FTC disclosure is clearly visible.', 'approval', false, NOW()), + + (gen_random_uuid()::text, contract1_id_val, creator1_id_val, 'Thank you! I\'ll make sure the disclosure is prominent in all content.', 'general', false, NOW()), + + (gen_random_uuid()::text, contract2_id_val, brand1_id_val, 'Love the fashion content! The engagement is exceeding expectations.', 'general', false, NOW()), + + (gen_random_uuid()::text, contract2_id_val, creator2_id_val, 'Thank you! I\'m excited to continue this collaboration.', 'general', false, NOW()), + + (gen_random_uuid()::text, contract3_id_val, brand1_id_val, 'Please review the gaming setup thoroughly and provide honest feedback.', 'negotiation', false, NOW()); + + -- Insert contract analytics + INSERT INTO contract_analytics (id, contract_id, performance_metrics, engagement_data, revenue_generated, roi_percentage, cost_per_engagement, cost_per_click, recorded_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, + '{"engagement_rate": 6.2, "reach": 15000, "impressions": 25000, "clicks": 1200}', + '{"likes": 1800, "comments": 450, "shares": 200, "saves": 300}', + 3500.00, 140.0, 0.83, 2.08, NOW()), + + (gen_random_uuid()::text, contract2_id_val, + '{"engagement_rate": 8.5, "reach": 22000, "impressions": 35000, "clicks": 1800}', + '{"likes": 2800, "comments": 600, "shares": 350, "saves": 450}', + 4800.00, 180.0, 0.71, 1.78, NOW()), + + (gen_random_uuid()::text, contract3_id_val, + '{"engagement_rate": 4.8, "reach": 8000, "impressions": 12000, "clicks": 600}', + '{"likes": 900, "comments": 200, "shares": 100, "saves": 150}', + 1200.00, 80.0, 1.25, 2.50, NOW()); + + -- Insert contract notifications + INSERT INTO contract_notifications (id, contract_id, user_id, notification_type, title, message, is_read, created_at) + VALUES + (gen_random_uuid()::text, contract1_id_val, creator1_id_val, 'milestone_due', 'Milestone Due', 'Content Publication milestone is due in 3 days', false, NOW()), + + (gen_random_uuid()::text, contract1_id_val, brand1_id_val, 'deliverable_submitted', 'Content Submitted', 'New content has been submitted for review', false, NOW()), + + (gen_random_uuid()::text, contract2_id_val, creator2_id_val, 'payment_received', 'Payment Received', 'January content payment has been processed', true, NOW()), + + (gen_random_uuid()::text, contract2_id_val, brand1_id_val, 'milestone_due', 'Milestone Due', 'February Content milestone is due in 5 days', false, NOW()), + + (gen_random_uuid()::text, contract3_id_val, creator1_id_val, 'contract_expiring', 'Contract Expiring', 'Gaming Setup Review contract expires in 10 days', false, NOW()); + + RAISE NOTICE 'Sample contract data inserted successfully'; +END $$; + +-- Verify the sample data +SELECT + c.contract_title, + c.status, + c.total_budget, + u1.username as creator, + u2.username as brand +FROM contracts c +JOIN users u1 ON c.creator_id = u1.id +JOIN users u2 ON c.brand_id = u2.id; + +SELECT + ct.template_name, + ct.template_type, + ct.industry, + u.username as created_by +FROM contract_templates ct +LEFT JOIN users u ON ct.created_by = u.id; + +SELECT + cm.milestone_name, + cm.status, + cm.payment_amount, + c.contract_title +FROM contract_milestones cm +JOIN contracts c ON cm.contract_id = c.id; + +SELECT + cd.deliverable_type, + cd.platform, + cd.status, + cd.approval_status, + c.contract_title +FROM contract_deliverables cd +JOIN contracts c ON cd.contract_id = c.id; + +SELECT + cp.amount, + cp.payment_type, + cp.status, + c.contract_title +FROM contract_payments cp +JOIN contracts c ON cp.contract_id = c.id; + +SELECT + cc.comment, + cc.comment_type, + u.username as user, + c.contract_title +FROM contract_comments cc +JOIN users u ON cc.user_id = u.id +JOIN contracts c ON cc.contract_id = c.id; + +SELECT + ca.revenue_generated, + ca.roi_percentage, + c.contract_title +FROM contract_analytics ca +JOIN contracts c ON ca.contract_id = c.id; + +SELECT + cn.notification_type, + cn.title, + cn.is_read, + c.contract_title +FROM contract_notifications cn +JOIN contracts c ON cn.contract_id = c.id; \ No newline at end of file diff --git a/Frontend/README-INTEGRATION.md b/Frontend/README-INTEGRATION.md new file mode 100644 index 0000000..251b6bd --- /dev/null +++ b/Frontend/README-INTEGRATION.md @@ -0,0 +1,60 @@ +# Frontend-Backend Integration + +## 🚀 Connected Successfully! + +Your brand dashboard frontend is now fully connected to the backend API. + +## 📋 What's Integrated: + +### **API Service (`brandApi.ts`)** +- Complete API client for all brand dashboard endpoints +- Type-safe TypeScript interfaces +- Error handling and response parsing +- All CRUD operations for campaigns, profiles, applications, payments + +### **Custom Hook (`useBrandDashboard.ts`)** +- State management for all dashboard data +- Loading states and error handling +- Real-time data synchronization +- AI query integration + +### **Enhanced Dashboard Component** +- Real-time data display +- AI-powered search functionality +- Loading and error states +- Interactive metrics dashboard + +## 🔗 API Endpoints Connected: + +- ✅ Dashboard Overview +- ✅ Brand Profile Management +- ✅ Campaign CRUD Operations +- ✅ Creator Matching & Search +- ✅ Application Management +- ✅ Payment Tracking +- ✅ Analytics & Performance +- ✅ AI-Powered Natural Language Search + +## 🎯 Features Working: + +1. **Real-time Dashboard Metrics** +2. **AI Search Bar** - Ask questions in natural language +3. **Campaign Management** +4. **Creator Discovery** +5. **Application Tracking** +6. **Payment Analytics** + +## 🚀 How to Test: + +1. **Start Backend:** `cd Backend && python -m uvicorn app.main:app --reload` +2. **Start Frontend:** `cd Frontend && npm run dev` +3. **Navigate to:** `http://localhost:5173/brand/dashboard` +4. **Try AI Search:** Type questions like "Show me my campaigns" or "Find creators for tech industry" + +## 🔧 Configuration: + +- Backend runs on: `http://localhost:8000` +- Frontend runs on: `http://localhost:5173` +- API proxy configured in `vite.config.ts` + +Your brand dashboard is now fully functional! 🎉 \ No newline at end of file diff --git a/Frontend/public/aossielogo.png b/Frontend/public/aossielogo.png new file mode 100644 index 0000000..b2421da Binary files /dev/null and b/Frontend/public/aossielogo.png differ diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx index 60f7ecd..8eef46c 100644 --- a/Frontend/src/App.tsx +++ b/Frontend/src/App.tsx @@ -18,6 +18,7 @@ import { AuthProvider } from "./context/AuthContext"; import ProtectedRoute from "./components/ProtectedRoute"; import PublicRoute from "./components/PublicRoute"; import Dashboard from "./pages/Brand/Dashboard"; +import DashboardOverview from "./pages/Brand/DashboardOverview"; import BasicDetails from "./pages/BasicDetails"; import Onboarding from "./components/Onboarding"; @@ -68,6 +69,16 @@ function App() { } /> + + + + } /> + + + + } /> } /> } /> void; + sessionId: string | null; + setSessionId: (sessionId: string | null) => void; +} + +const BrandChatAssistant: React.FC = ({ + initialQuery, + onClose, + sessionId, + setSessionId +}) => { + const [messages, setMessages] = useState([ + { sender: "user", text: initialQuery }, + ]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const chatEndRef = useRef(null); + + // Scroll to bottom on new message + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Send message to backend API + const sendMessageToBackend = async (message: string, currentSessionId?: string) => { + try { + const response = await fetch('/api/ai/query', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(currentSessionId && { 'X-Session-ID': currentSessionId }), + }, + body: JSON.stringify({ + query: message, + brand_id: "550e8400-e29b-41d4-a716-446655440000", // Test brand ID - TODO: Get from auth context + context: currentSessionId ? { session_id: currentSessionId } : undefined, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Update session ID if provided + if (data.session_id && !currentSessionId) { + setSessionId(data.session_id); + } + + return data; + } catch (error) { + console.error('Error calling AI API:', error); + throw error; + } + }; + + // Handle initial AI response + useEffect(() => { + if (messages.length === 1) { + setLoading(true); + sendMessageToBackend(initialQuery) + .then((response) => { + const aiMessage: ChatMessage = { + sender: "ai", + text: response.explanation || "I understand your request. Let me help you with that.", + result: response.result, + }; + setMessages((msgs) => [...msgs, aiMessage]); + }) + .catch((error) => { + const errorMessage: ChatMessage = { + sender: "ai", + text: "Sorry, I encountered an error processing your request. Please try again.", + error: error.message, + }; + setMessages((msgs) => [...msgs, errorMessage]); + }) + .finally(() => { + setLoading(false); + }); + } + }, []); + + const sendMessage = async () => { + if (!input.trim()) return; + + const userMsg: ChatMessage = { sender: "user", text: input }; + setMessages((msgs) => [...msgs, userMsg]); + setInput(""); + setLoading(true); + + try { + const response = await sendMessageToBackend(input, sessionId || undefined); + + const aiMessage: ChatMessage = { + sender: "ai", + text: response.explanation || "I've processed your request.", + result: response.result, + }; + + setMessages((msgs) => [...msgs, aiMessage]); + } catch (error) { + const errorMessage: ChatMessage = { + sender: "ai", + text: "Sorry, I encountered an error. Please try again.", + error: error instanceof Error ? error.message : "Unknown error", + }; + setMessages((msgs) => [...msgs, errorMessage]); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Header */} +
+ + 🤖 Brand AI Assistant + + +
+ + {/* Chat history */} +
+ {messages.map((msg, idx) => ( +
+
+ {msg.text} + {msg.result && ( +
+ Result: {JSON.stringify(msg.result, null, 2)} +
+ )} +
+
+ ))} + {loading && ( +
+
+ AI is typing… +
+ )} +
+
+ + {/* Input */} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + placeholder="Type your message…" + style={{ + flex: 1, + padding: 12, + borderRadius: 10, + border: "1px solid #333", + background: "#222", + color: "#fff", + fontSize: 15, + outline: "none", + }} + disabled={loading} + /> + +
+ + {/* CSS for loading animation */} + +
+ ); +}; + +export default BrandChatAssistant; \ No newline at end of file diff --git a/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx b/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx index 57c1f01..db435e7 100644 --- a/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx +++ b/Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx @@ -18,8 +18,8 @@ const CreatorMatchGrid: React.FC = ({ creators }) => { return (
- {currentCreators.map((creator) => ( - + {currentCreators.map((creator, index) => ( + ))}
diff --git a/Frontend/src/components/contracts/ContractDetailsModal.tsx b/Frontend/src/components/contracts/ContractDetailsModal.tsx new file mode 100644 index 0000000..4c28bd8 --- /dev/null +++ b/Frontend/src/components/contracts/ContractDetailsModal.tsx @@ -0,0 +1,668 @@ +import React, { useState } from 'react'; +import { + X, + Calendar, + DollarSign, + Users, + FileText, + CheckCircle, + Clock, + AlertCircle, + TrendingUp, + Eye, + Edit, + Download, + MessageSquare, + BarChart3, + CreditCard, + Target, + Award +} from 'lucide-react'; + +interface Contract { + id: string; + title: string; + creator: string; + brand: string; + status: string; + type: string; + budget: number; + startDate: string; + endDate: string; + progress: number; + milestones: number; + completedMilestones: number; + deliverables: number; + completedDeliverables: number; + payments: Array<{ + amount: number; + status: string; + date: string; + }>; +} + +interface ContractDetailsModalProps { + contract: Contract | null; + onClose: () => void; +} + +const ContractDetailsModal: React.FC = ({ contract, onClose }) => { + const [activeTab, setActiveTab] = useState('overview'); + + if (!contract) return null; + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return '#10b981'; + case 'pending': return '#f59e0b'; + case 'draft': return '#6b7280'; + case 'completed': return '#3b82f6'; + default: return '#6b7280'; + } + }; + + const getStatusBgColor = (status: string) => { + switch (status) { + case 'active': return 'rgba(16, 185, 129, 0.2)'; + case 'pending': return 'rgba(245, 158, 11, 0.2)'; + case 'draft': return 'rgba(107, 114, 128, 0.2)'; + case 'completed': return 'rgba(59, 130, 246, 0.2)'; + default: return 'rgba(107, 114, 128, 0.2)'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': return 'Active'; + case 'pending': return 'Pending'; + case 'draft': return 'Draft'; + case 'completed': return 'Completed'; + default: return status; + } + }; + + const tabs = [ + { id: 'overview', label: 'Overview', icon: Eye }, + { id: 'milestones', label: 'Milestones', icon: Target }, + { id: 'deliverables', label: 'Deliverables', icon: FileText }, + { id: 'payments', label: 'Payments', icon: CreditCard }, + { id: 'analytics', label: 'Analytics', icon: BarChart3 }, + { id: 'comments', label: 'Comments', icon: MessageSquare } + ]; + + const renderOverview = () => ( +
+ {/* Contract Info */} +
+

Contract Information

+
+
+
Contract Title
+
{contract.title}
+
+
+
Status
+
+ {getStatusText(contract.status)} +
+
+
+
Creator
+
{contract.creator}
+
+
+
Brand
+
{contract.brand}
+
+
+
Contract Type
+
{contract.type}
+
+
+
Total Budget
+
${contract.budget.toLocaleString()}
+
+
+
+ + {/* Progress Overview */} +
+

Progress Overview

+
+
+ Overall Progress + {contract.progress}% +
+
+
+
+
+
+
+
Milestones
+
+ {contract.completedMilestones}/{contract.milestones} +
+
+
+
Deliverables
+
+ {contract.completedDeliverables}/{contract.deliverables} +
+
+
+
+ + {/* Timeline */} +
+

Timeline

+
+
+
+
+
Contract Start
+
{contract.startDate}
+
+
+
+
+
+
Contract End
+
{contract.endDate}
+
+
+
+
+
+ ); + + const renderMilestones = () => ( +
+ {contract.milestones > 0 && ( +
+

Milestones

+
+ {['Advance Payment', 'Content Creation', 'Final Payment'].map((milestone, index) => ( +
+
+
+ {index < contract.completedMilestones ? : } +
+
+
{milestone}
+
+ {index === 0 ? 'Immediate' : index === 1 ? 'Due in 15 days' : 'Due on completion'} +
+
+
+
+
+ ${index === 0 ? 2500 : index === 1 ? 0 : 2500} +
+
+ {index < contract.completedMilestones ? 'Completed' : 'Pending'} +
+
+
+ ))} +
+
+ )} +
+ ); + + const renderDeliverables = () => ( +
+
+

Deliverables

+
+ {['Video Review', 'Instagram Post 1', 'Instagram Post 2'].map((deliverable, index) => ( +
+
+
+ {index < contract.completedDeliverables ? : } +
+
+
{deliverable}
+
+ {index === 0 ? 'YouTube' : 'Instagram'} • Due in {index === 0 ? '15' : index === 1 ? '20' : '25'} days +
+
+
+
+
+ {index < contract.completedDeliverables ? 'Approved' : 'Pending'} +
+
+
+ ))} +
+
+
+ ); + + const renderPayments = () => ( +
+
+

Payment History

+
+ {contract.payments.map((payment, index) => ( +
+
+
+ {payment.status === 'paid' ? : } +
+
+
+ {index === 0 ? 'Advance Payment' : index === 1 ? 'Final Payment' : `Payment ${index + 1}`} +
+
{payment.date}
+
+
+
+
${payment.amount.toLocaleString()}
+
+ {payment.status === 'paid' ? 'Paid' : 'Pending'} +
+
+
+ ))} +
+
+
+ ); + + const renderAnalytics = () => ( +
+
+

Performance Analytics

+
+
+
+ +
Engagement Rate
+
+
4.5%
+
+0.3% from last month
+
+
+
+ +
Total Reach
+
+
50K
+
+15% from last month
+
+
+
+ +
ROI
+
+
125%
+
+25% from last month
+
+
+
+ +
Revenue Generated
+
+
$2.5K
+
From this contract
+
+
+
+
+ ); + + const renderComments = () => ( +
+
+

Comments & Negotiations

+
+
+
+
+ B +
+
+
Brand Manager
+
2 hours ago
+
+
+
+ Looking forward to working with you on this tech review! The product specs have been updated. +
+
+
+
+
+ C +
+
+
Creator
+
1 hour ago
+
+
+
+ Thanks! I'll make sure to create high-quality content that showcases the product well. +
+
+
+
+
+ ); + + const renderTabContent = () => { + switch (activeTab) { + case 'overview': return renderOverview(); + case 'milestones': return renderMilestones(); + case 'deliverables': return renderDeliverables(); + case 'payments': return renderPayments(); + case 'analytics': return renderAnalytics(); + case 'comments': return renderComments(); + default: return renderOverview(); + } + }; + + return ( +
+
+ {/* Header */} +
+
+

{contract.title}

+

{contract.creator} • {contract.brand}

+
+
+ + + +
+
+ + {/* Tabs */} +
+ {tabs.map((tab) => { + const Icon = tab.icon; + return ( + + ); + })} +
+ + {/* Content */} +
+ {renderTabContent()} +
+
+
+ ); +}; + +export default ContractDetailsModal; \ No newline at end of file diff --git a/Frontend/src/components/user-nav.tsx b/Frontend/src/components/user-nav.tsx index 9c4939f..e771848 100644 --- a/Frontend/src/components/user-nav.tsx +++ b/Frontend/src/components/user-nav.tsx @@ -14,7 +14,11 @@ import { import { useAuth } from "../context/AuthContext"; import { Link } from "react-router-dom"; -export function UserNav() { +interface UserNavProps { + showDashboard?: boolean; +} + +export function UserNav({ showDashboard = true }: UserNavProps) { const { user, isAuthenticated, logout } = useAuth(); const [avatarError, setAvatarError] = useState(false); @@ -60,9 +64,11 @@ export function UserNav() { - - Dashboard - + {showDashboard && ( + + Dashboard + + )} Profile Settings diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx index 8588c41..ac10747 100644 --- a/Frontend/src/context/AuthContext.tsx +++ b/Frontend/src/context/AuthContext.tsx @@ -94,7 +94,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { .eq("user_id", userToUse.id) .limit(1); - const hasOnboarding = (socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0); + const hasOnboarding = Boolean((socialProfiles && socialProfiles.length > 0) || (brandData && brandData.length > 0)); // Get user role const { data: userData } = await supabase diff --git a/Frontend/src/hooks/useBrandDashboard.ts b/Frontend/src/hooks/useBrandDashboard.ts new file mode 100644 index 0000000..b6e6626 --- /dev/null +++ b/Frontend/src/hooks/useBrandDashboard.ts @@ -0,0 +1,288 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useAuth } from '../context/AuthContext'; +import { brandApi, DashboardOverview, BrandProfile, Campaign, CreatorMatch, Application, Payment } from '../services/brandApi'; +import { aiApi } from '../services/aiApi'; + +export const useBrandDashboard = () => { + const { user } = useAuth(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Dashboard Overview + const [dashboardOverview, setDashboardOverview] = useState(null); + const [brandProfile, setBrandProfile] = useState(null); + const [campaigns, setCampaigns] = useState([]); + const [creatorMatches, setCreatorMatches] = useState([]); + const [applications, setApplications] = useState([]); + const [payments, setPayments] = useState([]); + + // AI Query + const [aiResponse, setAiResponse] = useState(null); + const [aiLoading, setAiLoading] = useState(false); + + const brandId = user?.id; + + // Load dashboard overview + const loadDashboardOverview = useCallback(async () => { + if (!brandId) return; + + try { + setLoading(true); + const overview = await brandApi.getDashboardOverview(brandId); + setDashboardOverview(overview); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load dashboard overview'); + } finally { + setLoading(false); + } + }, [brandId]); + + // Load brand profile + const loadBrandProfile = useCallback(async () => { + if (!brandId) return; + + try { + const profile = await brandApi.getBrandProfile(brandId); + setBrandProfile(profile); + } catch (err) { + console.error('Failed to load brand profile:', err); + } + }, [brandId]); + + // Load campaigns + const loadCampaigns = useCallback(async () => { + if (!brandId) return; + + try { + const campaignsData = await brandApi.getBrandCampaigns(brandId); + setCampaigns(campaignsData); + } catch (err) { + console.error('Failed to load campaigns:', err); + } + }, [brandId]); + + // Load creator matches + const loadCreatorMatches = useCallback(async () => { + if (!brandId) return; + + try { + const matches = await brandApi.getCreatorMatches(brandId); + setCreatorMatches(matches); + } catch (err) { + console.error('Failed to load creator matches:', err); + } + }, [brandId]); + + // Load applications + const loadApplications = useCallback(async () => { + if (!brandId) return; + + try { + const applicationsData = await brandApi.getBrandApplications(brandId); + setApplications(applicationsData); + } catch (err) { + console.error('Failed to load applications:', err); + } + }, [brandId]); + + // Load payments + const loadPayments = useCallback(async () => { + if (!brandId) return; + + try { + const paymentsData = await brandApi.getBrandPayments(brandId); + setPayments(paymentsData); + } catch (err) { + console.error('Failed to load payments:', err); + } + }, [brandId]); + + // Create campaign + const createCampaign = useCallback(async (campaignData: { + title: string; + description: string; + required_audience: Record; + budget: number; + engagement_minimum: number; + }) => { + if (!brandId) throw new Error('User not authenticated'); + + try { + const newCampaign = await brandApi.createCampaign({ + ...campaignData, + brand_id: brandId, + }); + setCampaigns(prev => [...prev, newCampaign]); + return newCampaign; + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to create campaign'); + } + }, [brandId]); + + // Update campaign + const updateCampaign = useCallback(async (campaignId: string, updates: Partial) => { + if (!brandId) throw new Error('User not authenticated'); + + try { + const updatedCampaign = await brandApi.updateCampaign(campaignId, updates, brandId); + setCampaigns(prev => prev.map(campaign => + campaign.id === campaignId ? updatedCampaign : campaign + )); + return updatedCampaign; + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to update campaign'); + } + }, [brandId]); + + // Delete campaign + const deleteCampaign = useCallback(async (campaignId: string) => { + if (!brandId) throw new Error('User not authenticated'); + + try { + await brandApi.deleteCampaign(campaignId, brandId); + setCampaigns(prev => prev.filter(campaign => campaign.id !== campaignId)); + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to delete campaign'); + } + }, [brandId]); + + // Update application status + const updateApplicationStatus = useCallback(async ( + applicationId: string, + status: string, + notes?: string + ) => { + if (!brandId) throw new Error('User not authenticated'); + + try { + const updatedApplication = await brandApi.updateApplicationStatus( + applicationId, + status, + notes, + brandId + ); + setApplications(prev => prev.map(app => + app.id === applicationId ? updatedApplication : app + )); + return updatedApplication; + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to update application status'); + } + }, [brandId]); + + // AI Query + const queryAI = useCallback(async (query: string) => { + try { + setAiLoading(true); + const response = await aiApi.queryAI(query, brandId); + setAiResponse(response); + return response; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to process AI query'); + throw err; + } finally { + setAiLoading(false); + } + }, [brandId]); + + // Search creators + const searchCreators = useCallback(async (filters?: { + industry?: string; + min_engagement?: number; + location?: string; + }) => { + if (!brandId) throw new Error('User not authenticated'); + + try { + return await brandApi.searchCreators(brandId, filters); + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to search creators'); + } + }, [brandId]); + + // Get analytics + const getCampaignPerformance = useCallback(async () => { + if (!brandId) throw new Error('User not authenticated'); + + try { + return await brandApi.getCampaignPerformance(brandId); + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to get campaign performance'); + } + }, [brandId]); + + const getRevenueAnalytics = useCallback(async () => { + if (!brandId) throw new Error('User not authenticated'); + + try { + return await brandApi.getRevenueAnalytics(brandId); + } catch (err) { + throw err instanceof Error ? err : new Error('Failed to get revenue analytics'); + } + }, [brandId]); + + // Load all data on mount + useEffect(() => { + if (brandId) { + Promise.all([ + loadDashboardOverview(), + loadBrandProfile(), + loadCampaigns(), + loadCreatorMatches(), + loadApplications(), + loadPayments(), + ]).catch(err => { + console.error('Error loading dashboard data:', err); + }); + } + }, [brandId, loadDashboardOverview, loadBrandProfile, loadCampaigns, loadCreatorMatches, loadApplications, loadPayments]); + + // Refresh all data + const refreshData = useCallback(() => { + if (brandId) { + Promise.all([ + loadDashboardOverview(), + loadBrandProfile(), + loadCampaigns(), + loadCreatorMatches(), + loadApplications(), + loadPayments(), + ]).catch(err => { + console.error('Error refreshing dashboard data:', err); + }); + } + }, [brandId, loadDashboardOverview, loadBrandProfile, loadCampaigns, loadCreatorMatches, loadApplications, loadPayments]); + + return { + // State + loading, + error, + dashboardOverview, + brandProfile, + campaigns, + creatorMatches, + applications, + payments, + aiResponse, + aiLoading, + + // Actions + createCampaign, + updateCampaign, + deleteCampaign, + updateApplicationStatus, + queryAI, + searchCreators, + getCampaignPerformance, + getRevenueAnalytics, + refreshData, + + // Individual loaders + loadDashboardOverview, + loadBrandProfile, + loadCampaigns, + loadCreatorMatches, + loadApplications, + loadPayments, + }; +}; \ No newline at end of file diff --git a/Frontend/src/index.css b/Frontend/src/index.css index f2a93bb..b4c00c7 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -1,3 +1,4 @@ +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap'); @import "tailwindcss"; @import "tw-animate-css"; diff --git a/Frontend/src/pages/Brand/Dashboard.tsx b/Frontend/src/pages/Brand/Dashboard.tsx index 023c77b..c08f622 100644 --- a/Frontend/src/pages/Brand/Dashboard.tsx +++ b/Frontend/src/pages/Brand/Dashboard.tsx @@ -1,380 +1,651 @@ -import Chat from "@/components/chat/chat"; -import { Button } from "../../components/ui/button"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "../../components/ui/card"; -import { Input } from "../../components/ui/input"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "../../components/ui/tabs"; -import { - BarChart3, - Users, - MessageSquareMore, - TrendingUp, - Search, - Bell, - UserCircle, - FileText, - Send, - Clock, - CheckCircle2, - XCircle, - BarChart, - ChevronRight, - FileSignature, - LineChart, - Activity, - Rocket, -} from "lucide-react"; -import { CreatorMatches } from "../../components/dashboard/creator-matches"; -import { useState } from "react"; +import React, { useState } from "react"; +import { Menu, Settings, Search, Plus, Home, BarChart3, MessageSquare, FileText, ChevronLeft, ChevronRight, User, Loader2, LayoutDashboard } from "lucide-react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { UserNav } from "../../components/user-nav"; +import { useBrandDashboard } from "../../hooks/useBrandDashboard"; +import BrandChatAssistant from "../../components/chat/BrandChatAssistant"; -const Dashboard = () => { - // Mock sponsorships for selection (replace with real API call if needed) - const sponsorships = [ - { id: "1", title: "Summer Collection" }, - { id: "2", title: "Tech Launch" }, - { id: "3", title: "Fitness Drive" }, +const PRIMARY = "#0B00CF"; +const SECONDARY = "#300A6E"; +const ACCENT = "#FF2D2B"; + +const TABS = [ + { label: "Dashboard", route: "/brand/dashboard/overview", icon: LayoutDashboard }, + { label: "Discover", route: "/brand/dashboard", icon: Home }, + { label: "Contracts", route: "/brand/contracts", icon: FileText }, + { label: "Messages", route: "/brand/messages", icon: MessageSquare }, + { label: "Tracking", route: "/brand/tracking", icon: BarChart3 }, ]; - const [selectedSponsorship, setSelectedSponsorship] = useState(""); + +export default function BrandDashboard() { + const navigate = useNavigate(); + const location = useLocation(); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResults, setSearchResults] = useState(null); + + // Chat state management + const [chatActive, setChatActive] = useState(false); + const [sessionId, setSessionId] = useState(null); + + // Brand Dashboard Hook + const { + loading, + error, + dashboardOverview, + brandProfile, + campaigns, + creatorMatches, + applications, + payments, + aiResponse, + aiLoading, + queryAI, + refreshData, + } = useBrandDashboard(); + + // Handle AI Search - now triggers chat + const handleAISearch = async () => { + if (!searchQuery.trim()) return; + + // Activate chat and set initial query + setChatActive(true); + setSessionId(null); // Reset session for new conversation + }; + + // Handle chat close + const handleChatClose = () => { + setChatActive(false); + setSessionId(null); + setSearchQuery(""); // Clear search query + }; return ( - <> -
- {/* Navigation */} - - -
- {/* Header */} -
-

- Brand Dashboard -

-

- Discover and collaborate with creators that match your brand -

- {/* Search */} -
- - + {/* New Button */} +
+
- {/* Main Content */} - - - Discover - Contracts - Messages - Tracking - - - {/* Discover Tab */} - - {/* Stats */} -
- - - - Active Creators - - - - -
12,234
-

- +180 from last month -

-
-
- - - - Avg. Engagement - - - - -
4.5%
-

- +0.3% from last month -

-
-
- - - - Active Campaigns - - - - -
24
-

- 8 pending approval -

-
-
- - - - Messages - - - - -
12
-

- 3 unread messages -

-
-
+ {/* Navigation */} +
+ {TABS.map((tab) => { + const isActive = location.pathname === tab.route; + const Icon = tab.icon; + return ( + + ); + })}
- {/* Creator Recommendations */} -
-
-

- Matched Creators for Your Campaign -

-
-
- - + {/* Bottom Section - Profile and Settings */} +
+ {/* Profile */} + - {/* Contracts Tab */} - -
-

- Active Contracts -

- + {/* Settings */} +
-
- {[1, 2, 3].map((i) => ( -
-
-
- Creator -
-

- Summer Collection Campaign -

-

- with Alex Rivera -

-
- - - Due in 12 days - + {/* Collapse Toggle */} + +
+ + {/* Main Content */} +
+ {/* Top Bar */} +
+
+ INPACT Brands
+
+ {/* Settings button removed from top bar since it's now in sidebar */}
-
- - Active - -

- $2,400 -

-
-
-
-
- - -
- -
-
- ))} -
- - {/* Messages Tab */} - - - + {/* Content Area */} +
+ {/* Show Chat Assistant when active */} + {chatActive ? ( + + ) : ( + <> + {/* INPACT AI Title with animated gradient */} +

+ INPACT + + AI + +

- {/* Tracking Tab */} - -
- - - - Total Reach - - - - -
2.4M
-

- Across all campaigns -

-
-
- - - - Engagement Rate - - - - -
5.2%
-

- Average across creators -

-
-
- - - ROI - - - -
3.8x
-

- Last 30 days -

-
-
- - - - Active Posts - - - - -
156
-

- Across platforms -

-
-
+ {/* Main Search */} +
+
{ + e.currentTarget.style.borderColor = "#87CEEB"; + e.currentTarget.style.background = "rgba(26, 26, 26, 0.8)"; + e.currentTarget.style.backdropFilter = "blur(10px)"; + e.currentTarget.style.padding = "12px 16px"; + e.currentTarget.style.gap = "8px"; + e.currentTarget.style.width = "110%"; + e.currentTarget.style.transform = "translateX(-5%)"; + // Remove glass texture + const overlay = e.currentTarget.querySelector('[data-glass-overlay]') as HTMLElement; + if (overlay) overlay.style.opacity = "0"; + }} + onBlur={(e) => { + e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.1)"; + e.currentTarget.style.background = "rgba(26, 26, 26, 0.6)"; + e.currentTarget.style.backdropFilter = "blur(20px)"; + e.currentTarget.style.padding = "16px 20px"; + e.currentTarget.style.gap = "12px"; + e.currentTarget.style.width = "100%"; + e.currentTarget.style.transform = "translateX(0)"; + // Restore glass texture + const overlay = e.currentTarget.querySelector('[data-glass-overlay]') as HTMLElement; + if (overlay) overlay.style.opacity = "1"; + }} + > + {/* Glass texture overlay */} +
+ + setSearchQuery(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && searchQuery.trim()) { + handleAISearch(); + } + }} + style={{ + flex: 1, + background: "transparent", + border: "none", + color: "#fff", + fontSize: "16px", + outline: "none", + position: "relative", + zIndex: 1, + }} + /> + +
-
-

- Campaign Performance -

-
- {[1, 2, 3].map((i) => ( -
-
-
- Creator -
-

Summer Collection

-

- with Sarah Parker -

-
-
-
-

458K Reach

-

- 6.2% Engagement -

-
-
-
-
- - 12 Posts Live -
-
- - 2 Pending -
-
-
- ))} + {/* Loading State */} + {loading && ( +
+ +
Loading your dashboard...
+
+ )} + + {/* Error State */} + {error && ( +
+
Error
+
{error}
+
+ )} + + {/* Quick Actions */} +
+ {[ + { label: "Find Creators", icon: "👥", color: "#3b82f6" }, + { label: "Campaign Stats", icon: "📊", color: "#10b981" }, + { label: "Draft Contract", icon: "📄", color: "#f59e0b" }, + { label: "Analytics", icon: "📈", color: "#8b5cf6" }, + { label: "Messages", icon: "💬", color: "#ef4444" }, + ].map((action, index) => ( + + ))}
- - -
+ + )} +
- - ); -}; -export default Dashboard; + {/* CSS for gradient animation */} + +
+ ); +} diff --git a/Frontend/src/pages/Brand/DashboardOverview.tsx b/Frontend/src/pages/Brand/DashboardOverview.tsx new file mode 100644 index 0000000..1dbec2f --- /dev/null +++ b/Frontend/src/pages/Brand/DashboardOverview.tsx @@ -0,0 +1,1570 @@ +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Badge } from "@/components/ui/badge"; +import { + TrendingUp, + Users, + DollarSign, + Calendar, + BarChart3, + MessageSquare, + FileText, + Eye, + Target, + Clock, + CheckCircle, + AlertCircle, + Plus, + Search, + ArrowUpRight, + ArrowDownRight, + Activity, + MapPin, + Star, + Zap, + ArrowLeft, + Loader2 +} from "lucide-react"; + +const DashboardOverview = () => { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [dashboardData, setDashboardData] = useState(null); + + // Modal states + const [campaignsModalOpen, setCampaignsModalOpen] = useState(false); + const [creatorsModalOpen, setCreatorsModalOpen] = useState(false); + const [paymentsModalOpen, setPaymentsModalOpen] = useState(false); + const [analyticsModalOpen, setAnalyticsModalOpen] = useState(false); + const [notificationsModalOpen, setNotificationsModalOpen] = useState(false); + + // Brand ID for testing (in production, this would come from auth context) + const brandId = "6dbfcdd5-795f-49c1-8f7a-a5538b8c6f6f"; // Test brand ID + + // Theme colors matching brand homepage + const PRIMARY = "#0B00CF"; + const SECONDARY = "#300A6E"; + const ACCENT = "#FF2D2B"; + + // Mock data for demonstration (fallback) + const mockData = { + // Key Performance Metrics + kpis: { + activeCampaigns: 12, + totalReach: "2.4M", + engagementRate: 4.8, + roi: 320, + budgetSpent: 45000, + budgetAllocated: 75000 + }, + + // Campaign Overview + campaigns: [ + { id: 1, name: "Summer Collection Launch", status: "active", performance: "excellent", reach: "850K", engagement: 5.2, deadline: "2024-08-15" }, + { id: 2, name: "Tech Review Series", status: "active", performance: "good", reach: "620K", engagement: 4.1, deadline: "2024-08-20" }, + { id: 3, name: "Fitness Challenge", status: "pending", performance: "pending", reach: "0", engagement: 0, deadline: "2024-09-01" } + ], + + // Creator Management + creators: { + totalConnected: 28, + pendingApplications: 5, + topPerformers: 8, + newRecommendations: 12 + }, + + // Financial Overview + financial: { + monthlySpend: 18500, + pendingPayments: 3200, + costPerEngagement: 0.85, + budgetUtilization: 62 + }, + + // Analytics & Insights + analytics: { + audienceGrowth: 12.5, + bestContentType: "Video", + topGeographicMarket: "United States", + trendingTopics: ["Sustainability", "Tech Reviews", "Fitness"] + }, + + // Notifications + notifications: [ + { id: 1, type: "urgent", message: "3 applications need review", time: "2 hours ago" }, + { id: 2, type: "alert", message: "Campaign 'Tech Review' underperforming", time: "4 hours ago" }, + { id: 3, type: "info", message: "New creator recommendations available", time: "1 day ago" } + ] + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "active": return "text-green-500 bg-green-100"; + case "pending": return "text-yellow-500 bg-yellow-100"; + case "completed": return "text-blue-500 bg-blue-100"; + default: return "text-gray-500 bg-gray-100"; + } + }; + + const getPerformanceColor = (performance: string) => { + switch (performance) { + case "excellent": return "text-green-600"; + case "good": return "text-blue-600"; + case "average": return "text-yellow-600"; + case "poor": return "text-red-600"; + default: return "text-gray-600"; + } + }; + + // Fetch dashboard data from API + useEffect(() => { + const fetchDashboardData = async () => { + try { + setLoading(true); + setError(null); + + // Fetch KPIs data + const kpisResponse = await fetch(`http://localhost:8000/api/brand/dashboard/kpis?brand_id=${brandId}`); + if (!kpisResponse.ok) throw new Error('Failed to fetch KPIs data'); + const kpisData = await kpisResponse.json(); + + // Fetch campaigns data + const campaignsResponse = await fetch(`http://localhost:8000/api/brand/dashboard/campaigns/overview?brand_id=${brandId}`); + if (!campaignsResponse.ok) throw new Error('Failed to fetch campaigns data'); + const campaignsData = await campaignsResponse.json(); + + // Fetch analytics data + const analyticsResponse = await fetch(`http://localhost:8000/api/brand/dashboard/analytics?brand_id=${brandId}`); + if (!analyticsResponse.ok) throw new Error('Failed to fetch analytics data'); + const analyticsData = await analyticsResponse.json(); + + // Fetch notifications data + const notificationsResponse = await fetch(`http://localhost:8000/api/brand/dashboard/notifications?brand_id=${brandId}`); + if (!notificationsResponse.ok) throw new Error('Failed to fetch notifications data'); + const notificationsData = await notificationsResponse.json(); + + // Combine all data + setDashboardData({ + kpis: kpisData.kpis, + creators: kpisData.creators, + financial: kpisData.financial, + analytics: analyticsData.analytics, + campaigns: campaignsData.campaigns, + notifications: notificationsData.notifications + }); + + } catch (err) { + console.error('Error fetching dashboard data:', err); + setError(err instanceof Error ? err.message : 'Failed to load dashboard data'); + // Use mock data as fallback + setDashboardData(mockData); + } finally { + setLoading(false); + } + }; + + fetchDashboardData(); + }, [brandId]); + + // Use API data if available, otherwise fall back to mock data + const data = dashboardData || mockData; + + // Loading state + if (loading) { + return ( +
+
+
+ + Loading dashboard data... +
+
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+
+ +
+

Dashboard Overview

+

At a glance view of your brand performance and campaigns

+
+
+
+ + Error loading dashboard data +
+

{error}

+

Using fallback data for demonstration.

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

+ Dashboard Overview +

+

+ At a glance view of your brand performance and campaigns +

+
+ + {/* Key Performance Metrics */} +
+

+ + Key Performance Metrics +

+
+
+
+
+ +
+ Active Campaigns +
+
{data.kpis.activeCampaigns}
+
+ + +2 from last month +
+
+ +
+
+
+ +
+ Total Reach +
+
{data.kpis.totalReach}
+
+ + +15% from last month +
+
+ +
+
+
+ +
+ Engagement Rate +
+
{data.kpis.engagementRate}%
+
+ + +0.3% from last month +
+
+ +
+
+
+ +
+ ROI +
+
{data.kpis.roi}%
+
+ + +25% from last month +
+
+ +
+
+
+ +
+ Budget Spent +
+
${data.kpis.budgetSpent.toLocaleString()}
+
+ {data.kpis.budgetUtilization}% of allocated budget +
+
+ +
+
+
+ +
+ Cost per Engagement +
+
${data.financial.costPerEngagement}
+
+ + -12% from last month +
+
+
+
+ + {/* Campaign Overview & Creator Management */} +
+ {/* Campaign Overview */} +
+
+

+ + Recent Campaigns +

+ +
+
+ {data.campaigns.map((campaign) => ( +
+
+

{campaign.name}

+ + {campaign.status} + +
+
+
+ Reach: +
{campaign.reach}
+
+
+ Engagement: +
+ {campaign.engagement}% +
+
+
+ Deadline: +
+ {new Date(campaign.deadline).toLocaleDateString()} +
+
+
+
+ ))} +
+
+ + {/* Creator Management */} +
+
+

+ + Creator Management +

+ +
+
+
+
{data.creators.totalConnected}
+
Connected Creators
+
+
+
{data.creators.pendingApplications}
+
Pending Applications
+
+
+
{data.creators.topPerformers}
+
Top Performers
+
+
+
{data.creators.newRecommendations}
+
New Recommendations
+
+
+
+
+ + {/* Financial Overview & Analytics */} +
+ {/* Financial Overview */} +
+
+

+ + Financial Overview +

+ +
+
+
+
+
+
Monthly Spend
+
+ ${data.financial.monthlySpend.toLocaleString()} +
+
+
+
vs Last Month
+
+8%
+
+
+
+
+
+
+
Pending Payments
+
+ ${data.financial.pendingPayments.toLocaleString()} +
+
+
+
Due This Week
+
3 payments
+
+
+
+
+
+
+
Budget Utilization
+
+ {data.financial.budgetUtilization}% +
+
+
+
+
+
+
+
+
+ + {/* Analytics & Insights */} +
+
+

+ + Analytics & Insights +

+ +
+
+
+
+
+
Audience Growth
+
+ +{data.analytics.audienceGrowth}% +
+
+ +
+
+
+
+
+
Best Content Type
+
+ {data.analytics.bestContentType} +
+
+ +
+
+
+
+
+
Top Market
+
+ {data.analytics.topGeographicMarket} +
+
+ +
+
+
+
+
+ + {/* Notifications & Quick Actions */} +
+ {/* Notifications */} +
+
+

+ + Notifications +

+
+ + Still mock data + + +
+
+
+ {data.notifications.map((notification) => ( +
+
+
+
+
+ {notification.message} +
+
+ {notification.time} +
+
+
+
+ ))} +
+
+ + {/* Quick Actions */} +
+

+ + Quick Actions +

+
+ + + + +
+
+ + {/* Timeline View */} +
+

+ + This Week +

+
+
+
+
+
Campaign Deadline
+
Summer Collection Launch - Aug 15
+
+
+
+
+
+
Payment Due
+
Creator Payment - Aug 12
+
+
+
+
+
+
Content Review
+
Tech Review Video - Aug 14
+
+
+
+
+
+ + {/* Modal Components */} + + {/* Campaigns Modal */} + {campaignsModalOpen && ( +
+
+
+

All Campaigns

+ +
+
+ {data.campaigns.map((campaign) => ( +
+
+

{campaign.name}

+ + {campaign.status} + +
+
+
+ Reach: +
{campaign.reach}
+
+
+ Engagement: +
+ {campaign.engagement}% +
+
+
+ Deadline: +
+ {new Date(campaign.deadline).toLocaleDateString()} +
+
+
+
+ ))} +
+
+
+ )} + + {/* Creators Modal */} + {creatorsModalOpen && ( +
+
+
+

Creator Management

+ +
+
+
+
{data.creators.totalConnected}
+
Connected Creators
+
+
+
{data.creators.pendingApplications}
+
Pending Applications
+
+
+
{data.creators.topPerformers}
+
Top Performers
+
+
+
{data.creators.newRecommendations}
+
New Recommendations
+
+
+
+
+ )} + + {/* Payments Modal */} + {paymentsModalOpen && ( +
+
+
+

Financial Overview

+ +
+
+
+
+
+
Monthly Spend
+
+ ${data.financial.monthlySpend.toLocaleString()} +
+
+
+
vs Last Month
+
+8%
+
+
+
+
+
+
+
Pending Payments
+
+ ${data.financial.pendingPayments.toLocaleString()} +
+
+
+
Due This Week
+
3 payments
+
+
+
+
+
+
+
Budget Utilization
+
+ {data.financial.budgetUtilization}% +
+
+
+
+
+
+
+
+
+
+ )} + + {/* Analytics Modal */} + {analyticsModalOpen && ( +
+
+
+

Analytics & Insights

+ +
+
+
+
+
+
Audience Growth
+
+ +{data.analytics.audienceGrowth}% +
+
+ +
+
+
+
+
+
Best Content Type
+
+ {data.analytics.bestContentType} +
+
+ +
+
+
+
+
+
Top Market
+
+ {data.analytics.topGeographicMarket} +
+
+ +
+
+
+
+
Trending Topics
+
+ {data.analytics.trendingTopics.map((topic, index) => ( + + {topic} + + ))} +
+
+
+
+
+
+ )} + + {/* Notifications Modal */} + {notificationsModalOpen && ( +
+
+
+

All Notifications

+ +
+
+ {data.notifications.map((notification) => ( +
+
+
+
+
+ {notification.message} +
+
+ {notification.time} +
+
+
+
+ ))} +
+
+
+ )} +
+ ); +}; + +export default DashboardOverview; \ No newline at end of file diff --git a/Frontend/src/pages/Contracts.tsx b/Frontend/src/pages/Contracts.tsx index 792b38a..58a6ef0 100644 --- a/Frontend/src/pages/Contracts.tsx +++ b/Frontend/src/pages/Contracts.tsx @@ -1,9 +1,526 @@ -import React from 'react' +import React, { useState, useEffect } from 'react'; +import { + Plus, + Search, + Filter, + Calendar, + DollarSign, + Users, + FileText, + CheckCircle, + Clock, + AlertCircle, + TrendingUp, + Eye, + Edit, + MoreVertical, + Download, + Upload +} from 'lucide-react'; +import ContractDetailsModal from '../components/contracts/ContractDetailsModal'; + +// Mock data for contracts +const mockContracts = [ + { + id: '1', + title: 'Tech Product Review Campaign', + creator: 'TechCreator', + brand: 'TechCorp Inc.', + status: 'active', + type: 'one-time', + budget: 5000, + startDate: '2024-01-15', + endDate: '2024-02-15', + progress: 75, + milestones: 3, + completedMilestones: 2, + deliverables: 4, + completedDeliverables: 3, + payments: [ + { amount: 2500, status: 'paid', date: '2024-01-15' }, + { amount: 2500, status: 'pending', date: '2024-02-15' } + ] + }, + { + id: '2', + title: 'Fashion Collection Promotion', + creator: 'FashionInfluencer', + brand: 'StyleBrand', + status: 'pending', + type: 'ongoing', + budget: 9000, + startDate: '2024-02-01', + endDate: '2024-05-01', + progress: 25, + milestones: 3, + completedMilestones: 1, + deliverables: 12, + completedDeliverables: 3, + payments: [ + { amount: 3000, status: 'paid', date: '2024-02-01' }, + { amount: 3000, status: 'pending', date: '2024-03-01' }, + { amount: 3000, status: 'pending', date: '2024-04-01' } + ] + }, + { + id: '3', + title: 'Gaming Content Series', + creator: 'GameMaster', + brand: 'GameStudio', + status: 'draft', + type: 'performance-based', + budget: 7500, + startDate: '2024-03-01', + endDate: '2024-06-01', + progress: 0, + milestones: 4, + completedMilestones: 0, + deliverables: 9, + completedDeliverables: 0, + payments: [ + { amount: 2000, status: 'pending', date: '2024-03-01' }, + { amount: 2000, status: 'pending', date: '2024-04-01' }, + { amount: 2000, status: 'pending', date: '2024-05-01' }, + { amount: 1500, status: 'pending', date: '2024-06-01' } + ] + } +]; + +const Contracts = () => { + const [contracts, setContracts] = useState(mockContracts); + const [selectedStatus, setSelectedStatus] = useState('all'); + const [searchTerm, setSearchTerm] = useState(''); + const [showCreateModal, setShowCreateModal] = useState(false); + const [selectedContract, setSelectedContract] = useState(null); + + // Calculate stats + const stats = { + active: contracts.filter(c => c.status === 'active').length, + pending: contracts.filter(c => c.status === 'pending').length, + draft: contracts.filter(c => c.status === 'draft').length, + completed: contracts.filter(c => c.status === 'completed').length, + totalBudget: contracts.reduce((sum, c) => sum + c.budget, 0), + totalRevenue: contracts.reduce((sum, c) => { + const paidPayments = c.payments.filter(p => p.status === 'paid'); + return sum + paidPayments.reduce((pSum, p) => pSum + p.amount, 0); + }, 0) + }; + + // Filter contracts + const filteredContracts = contracts.filter(contract => { + const matchesStatus = selectedStatus === 'all' || contract.status === selectedStatus; + const matchesSearch = contract.title.toLowerCase().includes(searchTerm.toLowerCase()) || + contract.creator.toLowerCase().includes(searchTerm.toLowerCase()) || + contract.brand.toLowerCase().includes(searchTerm.toLowerCase()); + return matchesStatus && matchesSearch; + }); + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return '#10b981'; + case 'pending': return '#f59e0b'; + case 'draft': return '#6b7280'; + case 'completed': return '#3b82f6'; + default: return '#6b7280'; + } + }; + + const getStatusBgColor = (status: string) => { + switch (status) { + case 'active': return 'rgba(16, 185, 129, 0.2)'; + case 'pending': return 'rgba(245, 158, 11, 0.2)'; + case 'draft': return 'rgba(107, 114, 128, 0.2)'; + case 'completed': return 'rgba(59, 130, 246, 0.2)'; + default: return 'rgba(107, 114, 128, 0.2)'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'active': return 'Active'; + case 'pending': return 'Pending'; + case 'draft': return 'Draft'; + case 'completed': return 'Completed'; + default: return status; + } + }; -function Contracts() { return ( -
Contracts
- ) -} +
+ {/* Header */} +
+
+
+

Contracts

+

Manage your brand partnerships and creator agreements

+
+ +
+
+ + {/* Stats Cards */} +
+
+
+
Active Contracts
+ +
+
{stats.active}
+
+2 from last month
+
+ +
+
+
Pending Contracts
+ +
+
{stats.pending}
+
Awaiting signatures
+
+ +
+
+
Total Budget
+ +
+
${stats.totalBudget.toLocaleString()}
+
Across all contracts
+
+ +
+
+
Revenue Generated
+ +
+
${stats.totalRevenue.toLocaleString()}
+
From completed contracts
+
+
+ + {/* Filters and Search */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + style={{ + width: '100%', + padding: '12px 12px 12px 44px', + background: 'rgba(42, 42, 42, 0.6)', + border: '1px solid rgba(42, 42, 42, 0.8)', + borderRadius: '8px', + color: '#fff', + fontSize: '14px' + }} + /> +
+ + {/* Status Filter */} + + + {/* Quick Actions */} + + + +
+
+ + {/* Contracts Grid */} +
+ {filteredContracts.map((contract) => ( +
e.currentTarget.style.transform = 'translateY(-4px)'} + onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'} + onClick={() => setSelectedContract(contract)} + > + {/* Header */} +
+
+
+ {contract.creator.charAt(0)} +
+
+

{contract.title}

+

{contract.creator} • {contract.brand}

+
+
+
+ {getStatusText(contract.status)} +
+
+ + {/* Progress */} +
+
+ Progress + {contract.progress}% +
+
+
+
+
+ + {/* Stats */} +
+
+
Budget
+
${contract.budget.toLocaleString()}
+
+
+
Type
+
{contract.type}
+
+
+
Milestones
+
+ {contract.completedMilestones}/{contract.milestones} +
+
+
+
Deliverables
+
+ {contract.completedDeliverables}/{contract.deliverables} +
+
+
+ + {/* Actions */} +
+ + + +
+
+ ))} +
+ + {/* Empty State */} + {filteredContracts.length === 0 && ( +
+ +

No contracts found

+

Try adjusting your search or filters

+
+ )} + + {/* Contract Details Modal */} + setSelectedContract(null)} + /> +
+ ); +}; -export default Contracts \ No newline at end of file +export default Contracts; \ No newline at end of file diff --git a/Frontend/src/services/aiApi.ts b/Frontend/src/services/aiApi.ts new file mode 100644 index 0000000..cd9cdc2 --- /dev/null +++ b/Frontend/src/services/aiApi.ts @@ -0,0 +1,102 @@ +// AI API Service +// Handles AI-related API calls to the backend + +const AI_API_BASE_URL = 'http://localhost:8000/api/ai'; + +// Types for AI API responses +export interface AIQueryRequest { + query: string; + brand_id?: string; + context?: Record; +} + +export interface AIQueryResponse { + intent: string; + route?: string; + parameters: Record; + follow_up_needed: boolean; + follow_up_question?: string; + explanation: string; + original_query: string; + timestamp: string; + session_id?: string; + result?: any; + error?: string; +} + +// AI API Service Class +class AIApiService { + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${AI_API_BASE_URL}${endpoint}`; + + try { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`AI API Error (${endpoint}):`, error); + throw error; + } + } + + // Process AI Query with session management + async queryAI( + query: string, + brandId?: string, + sessionId?: string + ): Promise { + const requestBody: AIQueryRequest = { query }; + if (brandId) { + requestBody.brand_id = brandId; + } + if (sessionId) { + requestBody.context = { session_id: sessionId }; + } + + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (sessionId) { + headers['X-Session-ID'] = sessionId; + } + + return this.makeRequest('/query', { + method: 'POST', + headers, + body: JSON.stringify(requestBody), + }); + } + + // Get available routes + async getAvailableRoutes(): Promise<{ available_routes: string[]; total_routes: number }> { + return this.makeRequest<{ available_routes: string[]; total_routes: number }>('/routes'); + } + + // Get route info + async getRouteInfo(routeName: string): Promise<{ route_name: string; info: any }> { + return this.makeRequest<{ route_name: string; info: any }>(`/route/${routeName}`); + } + + // Test AI query (for development) + async testQuery(query: string): Promise { + return this.makeRequest(`/test?query=${encodeURIComponent(query)}`); + } +} + +// Export singleton instance +export const aiApi = new AIApiService(); \ No newline at end of file diff --git a/Frontend/src/services/brandApi.ts b/Frontend/src/services/brandApi.ts new file mode 100644 index 0000000..394cdea --- /dev/null +++ b/Frontend/src/services/brandApi.ts @@ -0,0 +1,246 @@ +// Brand Dashboard API Service +// Handles all API calls to the backend for brand dashboard functionality + +const API_BASE_URL = 'http://localhost:8000/api/brand'; + +// Types for API responses +export interface DashboardOverview { + total_campaigns: number; + active_campaigns: number; + total_revenue: number; + total_creators_matched: number; + recent_activity: any[]; +} + +export interface BrandProfile { + id: string; + user_id: string; + company_name?: string; + website?: string; + industry?: string; + contact_person?: string; + contact_email?: string; + created_at: string; +} + +export interface Campaign { + id: string; + brand_id: string; + title: string; + description: string; + required_audience: Record; + budget: number; + engagement_minimum: number; + status: string; + created_at: string; +} + +export interface CreatorMatch { + id: string; + brand_id: string; + creator_id: string; + match_score?: number; + matched_at: string; +} + +export interface Application { + id: string; + creator_id: string; + sponsorship_id: string; + post_id?: string; + proposal: string; + status: string; + applied_at: string; + creator?: any; + campaign?: any; +} + +export interface Payment { + id: string; + creator_id: string; + brand_id: string; + sponsorship_id: string; + amount: number; + status: string; + transaction_date: string; + creator?: any; + campaign?: any; +} + +// API Service Class +class BrandApiService { + private async makeRequest( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${API_BASE_URL}${endpoint}`; + + try { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error(`API Error (${endpoint}):`, error); + throw error; + } + } + + // Dashboard Overview + async getDashboardOverview(brandId: string): Promise { + return this.makeRequest(`/dashboard/overview?brand_id=${brandId}`); + } + + // Brand Profile + async getBrandProfile(userId: string): Promise { + return this.makeRequest(`/profile/${userId}`); + } + + async createBrandProfile(profile: Omit): Promise { + return this.makeRequest('/profile', { + method: 'POST', + body: JSON.stringify(profile), + }); + } + + async updateBrandProfile(userId: string, updates: Partial): Promise { + return this.makeRequest(`/profile/${userId}`, { + method: 'PUT', + body: JSON.stringify(updates), + }); + } + + // Campaigns + async getBrandCampaigns(brandId: string): Promise { + return this.makeRequest(`/campaigns?brand_id=${brandId}`); + } + + async getCampaignDetails(campaignId: string, brandId: string): Promise { + return this.makeRequest(`/campaigns/${campaignId}?brand_id=${brandId}`); + } + + async createCampaign(campaign: { + brand_id: string; + title: string; + description: string; + required_audience: Record; + budget: number; + engagement_minimum: number; + }): Promise { + return this.makeRequest('/campaigns', { + method: 'POST', + body: JSON.stringify(campaign), + }); + } + + async updateCampaign(campaignId: string, updates: Partial, brandId: string): Promise { + return this.makeRequest(`/campaigns/${campaignId}?brand_id=${brandId}`, { + method: 'PUT', + body: JSON.stringify(updates), + }); + } + + async deleteCampaign(campaignId: string, brandId: string): Promise { + return this.makeRequest(`/campaigns/${campaignId}?brand_id=${brandId}`, { + method: 'DELETE', + }); + } + + // Creator Matches + async getCreatorMatches(brandId: string): Promise { + return this.makeRequest(`/creators/matches?brand_id=${brandId}`); + } + + async searchCreators( + brandId: string, + filters?: { + industry?: string; + min_engagement?: number; + location?: string; + } + ): Promise { + const params = new URLSearchParams({ brand_id: brandId }); + if (filters?.industry) params.append('industry', filters.industry); + if (filters?.min_engagement) params.append('min_engagement', filters.min_engagement.toString()); + if (filters?.location) params.append('location', filters.location); + + return this.makeRequest(`/creators/search?${params.toString()}`); + } + + async getCreatorProfile(creatorId: string, brandId: string): Promise { + return this.makeRequest(`/creators/${creatorId}/profile?brand_id=${brandId}`); + } + + // Analytics + async getCampaignPerformance(brandId: string): Promise { + return this.makeRequest(`/analytics/performance?brand_id=${brandId}`); + } + + async getRevenueAnalytics(brandId: string): Promise { + return this.makeRequest(`/analytics/revenue?brand_id=${brandId}`); + } + + // Applications + async getBrandApplications(brandId: string): Promise { + return this.makeRequest(`/applications?brand_id=${brandId}`); + } + + async getApplicationDetails(applicationId: string, brandId: string): Promise { + return this.makeRequest(`/applications/${applicationId}?brand_id=${brandId}`); + } + + async updateApplicationStatus( + applicationId: string, + status: string, + notes?: string, + brandId?: string + ): Promise { + return this.makeRequest(`/applications/${applicationId}?brand_id=${brandId}`, { + method: 'PUT', + body: JSON.stringify({ status, notes }), + }); + } + + async getApplicationsSummary(brandId: string): Promise { + return this.makeRequest(`/applications/summary?brand_id=${brandId}`); + } + + // Payments + async getBrandPayments(brandId: string): Promise { + return this.makeRequest(`/payments?brand_id=${brandId}`); + } + + async getPaymentDetails(paymentId: string, brandId: string): Promise { + return this.makeRequest(`/payments/${paymentId}?brand_id=${brandId}`); + } + + async updatePaymentStatus( + paymentId: string, + status: string, + brandId: string + ): Promise { + return this.makeRequest(`/payments/${paymentId}/status?brand_id=${brandId}`, { + method: 'PUT', + body: JSON.stringify({ status }), + }); + } + + async getPaymentAnalytics(brandId: string): Promise { + return this.makeRequest(`/payments/analytics?brand_id=${brandId}`); + } + + +} + +// Export singleton instance +export const brandApi = new BrandApiService(); \ No newline at end of file