diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..33c79d806 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, Query from typing import Dict, Any from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user @@ -8,18 +8,25 @@ @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, + month: str = Query(..., pattern=r"^\d{4}-(0[1-9]|1[0-2])$"), + currency: str = Query("USD", pattern=r"^[A-Za-z]{3}$"), current_user: dict = Depends(get_current_user) ) -> Dict[str, Any]: tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" - revenue_data = await get_revenue_summary(property_id, tenant_id) - - total_revenue_float = float(revenue_data['total']) + normalized_currency = currency.upper() + revenue_data = await get_revenue_summary( + property_id, + tenant_id, + month, + normalized_currency, + ) return { "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, + "total_revenue": revenue_data['total'], "currency": revenue_data['currency'], + "month": revenue_data['month'], "reservations_count": revenue_data['count'] } diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..cbf9f4993 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,4 @@ -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,18 +11,19 @@ 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 - pool_recycle=3600, # Recycle connections every hour - echo=False # Set to True for SQL debugging + pool_pre_ping=True, ) self.session_factory = async_sessionmaker( @@ -45,7 +44,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/services/cache.py b/backend/app/services/cache.py index b81474957..0d3f5cc35 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -6,11 +6,17 @@ # Initialize Redis client (typically configured centrally). redis_client = redis.Redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0")) -async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any]: +async def get_revenue_summary( + property_id: str, + tenant_id: str, + month: str, + currency: str = "USD", +) -> Dict[str, Any]: """ Fetches revenue summary, utilizing caching to improve performance. """ - cache_key = f"revenue:{property_id}" + currency = currency.upper() + cache_key = f"revenue:{tenant_id}:{property_id}:{month}:{currency}" # Try to get from cache cached = await redis_client.get(cache_key) @@ -18,10 +24,15 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any return json.loads(cached) # Revenue calculation is delegated to the reservation service. - from app.services.reservations import calculate_total_revenue + from app.services.reservations import calculate_monthly_revenue # Calculate revenue - result = await calculate_total_revenue(property_id, tenant_id) + result = await calculate_monthly_revenue( + property_id, + tenant_id, + month, + currency, + ) # Cache the result for 5 minutes await redis_client.setex(cache_key, 300, json.dumps(result)) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00ab..f5216963b 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,46 +1,27 @@ from datetime import datetime -from decimal import Decimal -from typing import Dict, Any, List +from decimal import Decimal, ROUND_HALF_UP +from typing import Dict, Any -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: str, + currency: str = "USD", +) -> Dict[str, Any]: """ - Calculates revenue for a specific month. + Aggregates revenue for a property's local calendar month and currency. """ - - start_date = datetime(year, month, 1) - if month < 12: - end_date = datetime(year, month + 1, 1) + start_date = datetime.strptime(month, "%Y-%m") + if start_date.month < 12: + end_year = start_date.year + end_month = start_date.month + 1 else: - end_date = datetime(year + 1, 1, 1) - - print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") - - # SQL Simulation (This would be executed against the actual DB) - query = """ - SELECT SUM(total_amount) as total - FROM reservations - WHERE property_id = $1 - AND tenant_id = $2 - AND check_in_date >= $3 - AND check_in_date < $4 - """ - - # In production this query executes against a database session. - # result = await db.fetch_val(query, property_id, tenant_id, start_date, end_date) - # return result or Decimal('0') - - return Decimal('0') # Placeholder for now until DB connection is finalized + end_year = start_date.year + 1 + end_month = 1 -async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: - """ - Aggregates revenue from database. - """ 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: @@ -50,27 +31,47 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, query = text(""" SELECT - property_id, - SUM(total_amount) as total_revenue, + r.property_id, + SUM(r.total_amount) as total_revenue, COUNT(*) as reservation_count - FROM reservations - WHERE property_id = :property_id AND tenant_id = :tenant_id - GROUP BY property_id + FROM reservations r + JOIN properties p + ON p.id = r.property_id + AND p.tenant_id = r.tenant_id + WHERE r.property_id = :property_id + AND r.tenant_id = :tenant_id + AND r.currency = :currency + AND r.check_in_date >= make_timestamptz( + :start_year, :start_month, 1, 0, 0, 0, p.timezone + ) + AND r.check_in_date < make_timestamptz( + :end_year, :end_month, 1, 0, 0, 0, p.timezone + ) + GROUP BY r.property_id """) result = await session.execute(query, { "property_id": property_id, - "tenant_id": tenant_id + "tenant_id": tenant_id, + "currency": currency, + "start_year": start_date.year, + "start_month": start_date.month, + "end_year": end_year, + "end_month": end_month, }) row = result.fetchone() if row: total_revenue = Decimal(str(row.total_revenue)) + formatted_total = str( + total_revenue.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + ) return { "property_id": property_id, "tenant_id": tenant_id, - "total": str(total_revenue), - "currency": "USD", + "total": formatted_total, + "currency": currency, + "month": month, "count": row.reservation_count } else: @@ -79,31 +80,40 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, "property_id": property_id, "tenant_id": tenant_id, "total": "0.00", - "currency": "USD", + "currency": currency, + "month": month, "count": 0 } else: raise Exception("Database pool not available") except Exception as e: - print(f"Database error for {property_id} (tenant: {tenant_id}): {e}") + print( + f"Database error for {property_id} " + f"(tenant: {tenant_id}, month: {month}, currency: {currency}): {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-001': {'total': '2250.00', 'count': 4}, '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}) + mock_property_data = ( + mock_data.get(property_id, {'total': '0.00', 'count': 0}) + if month == "2024-03" and currency == "USD" + else {'total': '0.00', 'count': 0} + ) return { "property_id": property_id, "tenant_id": tenant_id, "total": mock_property_data['total'], - "currency": "USD", + "currency": currency, + "month": month, "count": mock_property_data['count'] } diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404..6f4db5772 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -9,8 +9,32 @@ const PROPERTIES = [ { id: 'prop-005', name: 'Urban Loft Modern' } ]; +const MONTH_OPTIONS = (() => { + const options: Array<{ value: string; label: string }> = []; + const today = new Date(); + + for (let year = 2024; year <= today.getFullYear(); year += 1) { + const lastMonth = year === today.getFullYear() ? today.getMonth() : 11; + + for (let month = 0; month <= lastMonth; month += 1) { + const value = `${year}-${String(month + 1).padStart(2, '0')}`; + const label = new Intl.DateTimeFormat(undefined, { + month: 'long', + year: 'numeric', + timeZone: 'UTC' + }).format(new Date(Date.UTC(year, month, 1))); + + options.push({ value, label }); + } + } + + return options.reverse(); +})(); + const Dashboard: React.FC = () => { const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [selectedMonth, setSelectedMonth] = useState('2024-03'); + const [selectedCurrency, setSelectedCurrency] = useState('USD'); return (
{data.reservations_count} bookings