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
28 changes: 25 additions & 3 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
@@ -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'],
Expand Down
8 changes: 4 additions & 4 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
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
4 changes: 2 additions & 2 deletions backend/app/core/tenant_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
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
30 changes: 5 additions & 25 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
27 changes: 15 additions & 12 deletions frontend/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-4 lg:p-6 min-h-full">
Expand All @@ -35,7 +38,7 @@ const Dashboard: React.FC = () => {
onChange={(e) => setSelectedProperty(e.target.value)}
className="block w-full sm:w-auto min-w-[200px] px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm"
>
{PROPERTIES.map((property) => (
{properties.map((property) => (
<option key={property.id} value={property.id}>
{property.name}
</option>
Expand All @@ -46,7 +49,7 @@ const Dashboard: React.FC = () => {
</div>

<div className="space-y-6">
<RevenueSummary propertyId={selectedProperty} />
{selectedProperty && <RevenueSummary propertyId={selectedProperty} />}
</div>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/secureApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,10 @@ export class SecureAPIClient {
/**
* Get dashboard summary with optional simulation header
*/
async getDashboardProperties(): Promise<{ id: string; name: string }[]> {
return this.request<any>('/api/v1/dashboard/properties');
}

async getDashboardSummary(propertyId: string, options?: { simulatedTenant?: string, timestamp?: number }) {
const queryParams = new URLSearchParams({ property_id: propertyId });
if (options?.timestamp) {
Expand Down