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
7 changes: 5 additions & 2 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Dict, Any
from app.services.cache import get_revenue_summary
from app.core.auth import authenticate_request as get_current_user

from decimal import Decimal, ROUND_HALF_UP
router = APIRouter()

@router.get("/dashboard/summary")
Expand All @@ -17,9 +17,12 @@ async def get_dashboard_summary(

total_revenue_float = float(revenue_data['total'])

total = Decimal(str(revenue_data["total"])).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
return {
"property_id": revenue_data['property_id'],
"total_revenue": total_revenue_float,
"total_revenue": str(total),
"currency": revenue_data['currency'],
"reservations_count": revenue_data['count']
}
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
13 changes: 8 additions & 5 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
from zoneinfo import ZoneInfo
from datetime import datetime
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.
"""

start_date = datetime(year, month, 1)
# 1) load property timezone for (property_id, tenant_id) e.g. Europe/Paris
tz = ZoneInfo(property_timezone) # from DB
start_date = datetime(year, month, 1, tzinfo=tz)
if month < 12:
end_date = datetime(year, month + 1, 1)
end_date = datetime(year, month + 1, 1, tzinfo=tz)
else:
end_date = datetime(year + 1, 1, 1)
end_date = datetime(year + 1, 1, 1, tzinfo=tz)


print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}")

Expand Down
28 changes: 15 additions & 13 deletions frontend/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React, { 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 { useAuth } from "../contexts/AuthContext.new";
const ALL_PROPERTIES = [
{ id: "prop-001", name: "Beach House Alpha", tenantId: "tenant-a" },
{ id: "prop-002", name: "City Apartment Downtown", tenantId: "tenant-a" },
{ id: "prop-003", name: "Country Villa Estate", tenantId: "tenant-a" },
{ id: "prop-004", name: "Lakeside Cottage", tenantId: "tenant-b" },
{ id: "prop-005", name: "Urban Loft Modern", tenantId: "tenant-b" },
// note: tenant-b also has prop-001 "Mountain Lodge Beta" in seed —
// for video you can add that row if you want Ocean's list complete
];

const Dashboard: React.FC = () => {
const [selectedProperty, setSelectedProperty] = useState('prop-001');

const { user } = useAuth();
const tenantId = user?.tenant_id || user?.app_metadata?.tenant_id || "tenant-a";
const PROPERTIES = ALL_PROPERTIES.filter((p) => p.tenantId === tenantId);
const [selectedProperty, setSelectedProperty] = useState(PROPERTIES[0]?.id ?? "prop-001");
return (
<div className="p-4 lg:p-6 min-h-full">
<div className="max-w-7xl mx-auto">
Expand All @@ -22,11 +26,9 @@ const Dashboard: React.FC = () => {
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
<div>
<h2 className="text-lg lg:text-xl font-medium text-gray-900 mb-2">Revenue Overview</h2>
<p className="text-sm lg:text-base text-gray-600">
Monthly performance insights for your properties
</p>
<p className="text-sm lg:text-base text-gray-600">Monthly performance insights for your properties</p>
</div>

{/* Property Selector */}
<div className="flex flex-col sm:items-end">
<label className="text-xs font-medium text-gray-700 mb-1">Select Property</label>
Expand Down
216 changes: 116 additions & 100 deletions frontend/src/components/RevenueSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,135 @@
import React, { useEffect, useState } from 'react';
import { SecureAPI } from '../lib/secureApi';
import React, { useEffect, useState } from "react";
import { SecureAPI } from "../lib/secureApi";

interface RevenueData {
property_id: string;
total_revenue: number;
currency: string;
reservations_count: number;
property_id: string;
total_revenue: number;
currency: string;
reservations_count: number;
}

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

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

const activeTenant = debugTenant || 'candidate';
const activeTenant = debugTenant || "candidate";

useEffect(() => {
const fetchRevenue = async () => {
setLoading(true);
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()
});
setData(response);
} catch (err) {
setError('Failed to load revenue data');
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
const fetchRevenue = async () => {
setLoading(true);
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(),
});
setData(response);
} catch (err) {
setError("Failed to load revenue data");
console.error(err);
} finally {
setLoading(false);
}
};

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

if (loading) {
return (
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-100 rounded w-1/4"></div>
<div className="h-8 bg-gray-100 rounded w-1/2"></div>
<div className="flex gap-4 pt-4">
<div className="h-12 bg-gray-100 rounded flex-1"></div>
<div className="h-12 bg-gray-100 rounded flex-1"></div>
</div>
</div>
</div>
);
}

if (error) return <div className="p-4 text-red-500 bg-red-50 rounded-lg">{error}</div>;
if (!data) return null;
if (loading) {
return (
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-100 rounded w-1/4"></div>
<div className="h-8 bg-gray-100 rounded w-1/2"></div>
<div className="flex gap-4 pt-4">
<div className="h-12 bg-gray-100 rounded flex-1"></div>
<div className="h-12 bg-gray-100 rounded flex-1"></div>
</div>
</div>
</div>
);
}

const displayTotal = Math.round(data.total_revenue * 100) / 100;
if (error) return <div className="p-4 text-red-500 bg-red-50 rounded-lg">{error}</div>;
if (!data) return null;

return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-300">
{showRaw && (
<div className="p-3 bg-gray-50 text-xs font-mono border-b border-gray-100 overflow-auto max-h-32">
<strong className="block mb-1 text-gray-500 uppercase tracking-wider text-[10px]">Raw API Response</strong>
<pre className="text-gray-700">{JSON.stringify(data, null, 2)}</pre>
</div>
)}
const displayTotal = Number(data.total_revenue);
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-300">
{showRaw && (
<div className="p-3 bg-gray-50 text-xs font-mono border-b border-gray-100 overflow-auto max-h-32">
<strong className="block mb-1 text-gray-500 uppercase tracking-wider text-[10px]">Raw API Response</strong>
<pre className="text-gray-700">{JSON.stringify(data, null, 2)}</pre>
</div>
)}

<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Total Revenue</h2>
<div className="flex items-baseline gap-2 mt-1">
<span className="text-3xl font-bold text-gray-900 tracking-tight">
{data.currency} {displayTotal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
{/* Fake trend indicator for premium feel */}
<span className="inline-flex items-baseline px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 md:mt-2 lg:mt-0">
<svg className="-ml-1 mr-0.5 h-3 w-3 flex-shrink-0 self-center text-green-500" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path fillRule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
12%
</span>
</div>
</div>
</div>
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide">Total Revenue</h2>
<div className="flex items-baseline gap-2 mt-1">
<span className="text-3xl font-bold text-gray-900 tracking-tight">
{data.currency}{" "}
{displayTotal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
{/* Fake trend indicator for premium feel */}
<span className="inline-flex items-baseline px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 md:mt-2 lg:mt-0">
<svg
className="-ml-1 mr-0.5 h-3 w-3 flex-shrink-0 self-center text-green-500"
fill="currentColor"
viewBox="0 0 20 20"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
12%
</span>
</div>
</div>
</div>

<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-100">
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wider">Property ID</p>
<p className="text-sm font-semibold text-gray-700 font-mono mt-1">{data.property_id}</p>
</div>
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wider">Reservations</p>
<p className="text-sm font-semibold text-gray-700 mt-1">{data.reservations_count} <span className="font-normal text-gray-400">bookings</span></p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-gray-100">
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wider">Property ID</p>
<p className="text-sm font-semibold text-gray-700 font-mono mt-1">{data.property_id}</p>
</div>
<div>
<p className="text-xs text-gray-500 font-medium uppercase tracking-wider">Reservations</p>
<p className="text-sm font-semibold text-gray-700 mt-1">
{data.reservations_count} <span className="font-normal text-gray-400">bookings</span>
</p>
</div>
</div>

{/* Precision Warning Area */}
<div className="mt-4 h-6">
{Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && (
<div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
<svg className="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Precision Mismatch Detected
</div>
)}
</div>
{/* Precision Warning Area */}
<div className="mt-4 h-6">
{Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && (
<div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
<svg className="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
Precision Mismatch Detected
</div>
)}
</div>
);
</div>
</div>
);
};