Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,28 @@

router = APIRouter()


@router.get("/dashboard/summary")
async def get_dashboard_summary(
property_id: str,
current_user: dict = Depends(get_current_user)
current_user: Any = 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'])

tenant_id = getattr(current_user, "tenant_id", None)
if not tenant_id:
raise HTTPException(status_code=403, detail="No tenant assigned")

try:
revenue_data = await get_revenue_summary(property_id, tenant_id)
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=503, detail="Revenue data temporarily unavailable")

return {
"property_id": revenue_data['property_id'],
"total_revenue": total_revenue_float,
"total_revenue": float(revenue_data['total']),
"currency": revenue_data['currency'],
"tenant_id": tenant_id,
"reservations_count": revenue_data['count']
}

14 changes: 7 additions & 7 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import QueuePool
from sqlalchemy.engine import make_url
import logging
from ..config import settings

Expand All @@ -13,13 +13,13 @@ def __init__(self):

async def initialize(self):
"""Initialize database connection pool"""
if self.engine and 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}"

db_url = make_url(settings.database_url).set(drivername="postgresql+asyncpg")

self.engine = create_async_engine(
database_url,
poolclass=QueuePool,
db_url,
pool_size=20, # Number of connections to maintain
max_overflow=30, # Additional connections when needed
pool_pre_ping=True, # Validate connections
Expand All @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion backend/app/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 10 additions & 29 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from datetime import datetime
from decimal import Decimal
from decimal import Decimal, ROUND_HALF_UP
from typing import Dict, Any, List

logger = logging.getLogger(__name__)

async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal:
"""
Calculates revenue for a specific month.
Expand Down Expand Up @@ -31,24 +34,21 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_

return Decimal('0') # Placeholder for now until DB connection is finalized


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

# Initialize pool if needed
db_pool = DatabasePool()
from app.core.database_pool import db_pool
await db_pool.initialize()

if db_pool.session_factory:
async with db_pool.get_session() as session:
# Use SQLAlchemy text for raw SQL
from sqlalchemy import text

query = text("""

SELECT
property_id,
SUM(total_amount) as total_revenue,
Expand All @@ -65,7 +65,7 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str,
row = result.fetchone()

if row:
total_revenue = Decimal(str(row.total_revenue))
total_revenue = Decimal(str(row.total_revenue or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
return {
"property_id": property_id,
"tenant_id": tenant_id,
Expand All @@ -86,24 +86,5 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str,
raise Exception("Database pool not available")

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']
}
logger.error(f"Database error calculating revenue for {property_id} (tenant: {tenant_id}): {e}")
raise
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ redis>=5.0.0
requests
sendgrid
sqlalchemy>=2.0.0
greenlet>=3.0.0
supabase
tenacity>=8.0.0
uvicorn[standard]
Expand Down
16 changes: 5 additions & 11 deletions frontend/src/components/RevenueSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,20 @@ interface RevenueData {

interface RevenueSummaryProps {
propertyId?: string;
debugTenant?: string;
showRaw?: boolean;
}

export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ propertyId = 'prop-001', debugTenant, showRaw }) => {
export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ propertyId = 'prop-001', showRaw }) => {
const [data, setData] = useState<RevenueData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');

const activeTenant = debugTenant || 'candidate';

useEffect(() => {
const fetchRevenue = async () => {
setLoading(true);
setError('');
try {
// Use SecureAPI to handle authentication automatically
// We pass the simulatedTenant option which SecureAPI will attach as a header
const response = await SecureAPI.getDashboardSummary(propertyId, {
simulatedTenant: activeTenant,
timestamp: Date.now()
});
const response = await SecureAPI.getDashboardSummary(propertyId);
setData(response);
} catch (err) {
setError('Failed to load revenue data');
Expand All @@ -41,7 +34,8 @@ export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ propertyId = 'pr
};

fetchRevenue();
}, [propertyId, activeTenant]);
}, [propertyId]);


if (loading) {
return (
Expand Down
18 changes: 4 additions & 14 deletions frontend/src/lib/secureApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,24 +1450,14 @@ export class SecureAPIClient {

// ============= DASHBOARD API =============
/**
* Get dashboard summary with optional simulation header
* Get dashboard summary
*/
async getDashboardSummary(propertyId: string, options?: { simulatedTenant?: string, timestamp?: number }) {
async getDashboardSummary(propertyId: string) {
const queryParams = new URLSearchParams({ property_id: propertyId });
if (options?.timestamp) {
queryParams.append('_t', options.timestamp.toString());
}

const requestOptions: RequestInit = {};
if (options?.simulatedTenant) {
requestOptions.headers = {
'X-Simulated-Tenant': options.simulatedTenant
};
}

return this.request<any>(`/api/v1/dashboard/summary?${queryParams}`, requestOptions);
return this.request<any>(`/api/v1/dashboard/summary?${queryParams}`);
}


async uploadCompanyLogo(logo_url: string) {
return this.request<any>('/api/v1/company-settings/logo', {
method: 'POST',
Expand Down