diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e3..8581480e31 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,21 +1,43 @@ from fastapi import APIRouter, Depends, HTTPException -from typing import Dict, Any +from typing import Dict, Any, List +from sqlalchemy import text +from app.core.database_pool import db_pool +from decimal import Decimal, ROUND_HALF_UP from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user router = APIRouter() +@router.get("/dashboard/properties") +async def get_dashboard_properties( + current_user: dict = Depends(get_current_user) +) -> List[Dict[str, Any]]: + + tenant_id = getattr(current_user, "tenant_id", None) + if not tenant_id: + raise HTTPException(status_code=403, detail="No tenant for user") + + await db_pool.initialize() + async with db_pool.get_session() as session: + result = await session.execute( + text("SELECT id, name FROM properties WHERE tenant_id = :tenant_id ORDER BY id"), + {"tenant_id": tenant_id} + ) + return [{"id": row.id, "name": row.name} for row in result] + @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, current_user: dict = Depends(get_current_user) ) -> Dict[str, Any]: - tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" + tenant_id = getattr(current_user, "tenant_id", None) + if not tenant_id: + raise HTTPException(status_code=403, detail="No tenant for user") revenue_data = await get_revenue_summary(property_id, tenant_id) - total_revenue_float = float(revenue_data['total']) + total_revenue_float = float(Decimal(revenue_data['total']).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)) return { "property_id": revenue_data['property_id'], diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe3..b230ed3946 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,5 @@ import asyncio from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.pool import QueuePool import logging from ..config import settings @@ -13,13 +12,14 @@ def __init__(self): async def initialize(self): """Initialize database connection pool""" + if self.session_factory: + return try: # Create async engine with connection pooling - database_url = f"postgresql+asyncpg://{settings.supabase_db_user}:{settings.supabase_db_password}@{settings.supabase_db_host}:{settings.supabase_db_port}/{settings.supabase_db_name}" + database_url = settings.database_url.replace("postgresql://", "postgresql+asyncpg://", 1) self.engine = create_async_engine( database_url, - poolclass=QueuePool, pool_size=20, # Number of connections to maintain max_overflow=30, # Additional connections when needed pool_pre_ping=True, # Validate connections @@ -45,7 +45,7 @@ async def close(self): if self.engine: await self.engine.dispose() - async def get_session(self) -> AsyncSession: + def get_session(self) -> AsyncSession: """Get database session from pool""" if not self.session_factory: raise Exception("Database pool not initialized") diff --git a/backend/app/core/tenant_resolver.py b/backend/app/core/tenant_resolver.py index db09a46290..be547ed02f 100644 --- a/backend/app/core/tenant_resolver.py +++ b/backend/app/core/tenant_resolver.py @@ -88,8 +88,8 @@ async def resolve_tenant_id(user_id: str, user_email: str, token: Optional[str] if user_email == "candidate@propertyflow.com": return "tenant-a" - # Default fallback - return "tenant-a" + # Unknown users get no tenant rather than another tenant's data + return None @staticmethod async def update_user_tenant_metadata(user_id: str, tenant_id: str) -> None: diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index b81474957f..672e3f9d69 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -10,7 +10,7 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any """ Fetches revenue summary, utilizing caching to improve performance. """ - cache_key = f"revenue:{property_id}" + cache_key = f"revenue:{tenant_id}:{property_id}" # Try to get from cache cached = await redis_client.get(cache_key) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00abe..3eef72152f 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -2,7 +2,7 @@ from decimal import Decimal from typing import Dict, Any, List -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: +async def calculate_monthly_revenue(property_id: str, tenant_id: str, month: int, year: int, db_session=None) -> Decimal: """ Calculates revenue for a specific month. """ @@ -21,8 +21,8 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_ FROM reservations WHERE property_id = $1 AND tenant_id = $2 - AND check_in_date >= $3 - AND check_in_date < $4 + AND check_in_date >= ($3::timestamp AT TIME ZONE (SELECT timezone FROM properties WHERE id = $1 AND tenant_id = $2)) + AND check_in_date < ($4::timestamp AT TIME ZONE (SELECT timezone FROM properties WHERE id = $1 AND tenant_id = $2)) """ # In production this query executes against a database session. @@ -37,10 +37,9 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, """ try: # Import database pool - from app.core.database_pool import DatabasePool + from app.core.database_pool import db_pool # Initialize pool if needed - db_pool = DatabasePool() await db_pool.initialize() if db_pool.session_factory: @@ -87,23 +86,4 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, except Exception as e: print(f"Database error for {property_id} (tenant: {tenant_id}): {e}") - - # Create property-specific mock data for testing when DB is unavailable - # This ensures each property shows different figures - mock_data = { - 'prop-001': {'total': '1000.00', 'count': 3}, - 'prop-002': {'total': '4975.50', 'count': 4}, - 'prop-003': {'total': '6100.50', 'count': 2}, - 'prop-004': {'total': '1776.50', 'count': 4}, - 'prop-005': {'total': '3256.00', 'count': 3} - } - - mock_property_data = mock_data.get(property_id, {'total': '0.00', 'count': 0}) - - return { - "property_id": property_id, - "tenant_id": tenant_id, - "total": mock_property_data['total'], - "currency": "USD", - "count": mock_property_data['count'] - } + raise diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ebaa4bc92d..6968736a46 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "reportlab>=4.4.3", "requests>=2.32.4", "sendgrid>=6.12.4", - "sqlalchemy>=2.0.0", + "sqlalchemy[asyncio]>=2.0.0", "stripe>=7.0.0", "supabase>=2.18.0", "tenacity>=9.1.2", diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b777d2aaa..06cb8bd4a6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,7 +18,7 @@ pytz redis>=5.0.0 requests sendgrid -sqlalchemy>=2.0.0 +sqlalchemy[asyncio]>=2.0.0 supabase tenacity>=8.0.0 uvicorn[standard] diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404e..0fdc94590f 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,16 +1,19 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { RevenueSummary } from "./RevenueSummary"; - -const PROPERTIES = [ - { id: 'prop-001', name: 'Beach House Alpha' }, - { id: 'prop-002', name: 'City Apartment Downtown' }, - { id: 'prop-003', name: 'Country Villa Estate' }, - { id: 'prop-004', name: 'Lakeside Cottage' }, - { id: 'prop-005', name: 'Urban Loft Modern' } -]; +import { SecureAPI } from "../lib/secureApi"; const Dashboard: React.FC = () => { - const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [properties, setProperties] = useState<{ id: string; name: string }[]>([]); + const [selectedProperty, setSelectedProperty] = useState(''); + + useEffect(() => { + SecureAPI.getDashboardProperties() + .then((list) => { + setProperties(list); + if (list.length) setSelectedProperty(list[0].id); + }) + .catch(console.error); + }, []); return (