feat: add multi-tenant system with properties, organizations, and public booking
Implement complete multi-property architecture: - Properties (groups of spaces) with public/private visibility - Property managers (many-to-many) with role-based permissions - Organizations with member management - Anonymous/guest booking support via public API (/api/public/*) - Property-scoped spaces, bookings, and settings - Frontend: property selector, organization management, public booking views - Migration script and updated seed data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@ from sqlalchemy import and_, case, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.deps import get_current_admin, get_db
|
||||
from app.core.deps import get_current_manager_or_superadmin, get_db
|
||||
from app.core.permissions import get_manager_property_ids
|
||||
from app.models.booking import Booking
|
||||
from app.models.space import Space
|
||||
from app.models.user import User
|
||||
@@ -41,8 +42,9 @@ def get_usage_report(
|
||||
start_date: date | None = Query(None),
|
||||
end_date: date | None = Query(None),
|
||||
space_id: int | None = Query(None),
|
||||
property_id: int | None = Query(None),
|
||||
db: Annotated[Session, Depends(get_db)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_admin)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_manager_or_superadmin)] = None,
|
||||
) -> SpaceUsageReport:
|
||||
"""Get booking usage report by space."""
|
||||
query = (
|
||||
@@ -81,6 +83,13 @@ def get_usage_report(
|
||||
)
|
||||
if space_id:
|
||||
filters.append(Booking.space_id == space_id)
|
||||
if property_id:
|
||||
filters.append(Space.property_id == property_id)
|
||||
|
||||
# Property scoping for managers
|
||||
if current_admin.role == "manager":
|
||||
managed_ids = get_manager_property_ids(db, current_admin.id)
|
||||
filters.append(Space.property_id.in_(managed_ids))
|
||||
|
||||
if filters:
|
||||
query = query.filter(and_(*filters))
|
||||
@@ -114,7 +123,7 @@ def get_top_users_report(
|
||||
end_date: date | None = Query(None),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: Annotated[Session, Depends(get_db)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_admin)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_manager_or_superadmin)] = None,
|
||||
) -> TopUsersReport:
|
||||
"""Get top users by booking count."""
|
||||
query = (
|
||||
@@ -129,6 +138,7 @@ def get_top_users_report(
|
||||
func.sum(calculate_hours_expr()).label("total_hours"),
|
||||
)
|
||||
.join(User, Booking.user_id == User.id)
|
||||
.join(Space, Booking.space_id == Space.id)
|
||||
.group_by(Booking.user_id, User.full_name, User.email)
|
||||
)
|
||||
|
||||
@@ -143,6 +153,11 @@ def get_top_users_report(
|
||||
Booking.start_datetime <= datetime.combine(end_date, datetime.max.time())
|
||||
)
|
||||
|
||||
# Property scoping for managers
|
||||
if current_admin.role == "manager":
|
||||
managed_ids = get_manager_property_ids(db, current_admin.id)
|
||||
query = query.filter(Space.property_id.in_(managed_ids))
|
||||
|
||||
# Order by total bookings desc
|
||||
query = query.order_by(func.count(Booking.id).desc()).limit(limit)
|
||||
|
||||
@@ -171,7 +186,7 @@ def get_approval_rate_report(
|
||||
start_date: date | None = Query(None),
|
||||
end_date: date | None = Query(None),
|
||||
db: Annotated[Session, Depends(get_db)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_admin)] = None,
|
||||
current_admin: Annotated[User, Depends(get_current_manager_or_superadmin)] = None,
|
||||
) -> ApprovalRateReport:
|
||||
"""Get approval/rejection rate report."""
|
||||
query = db.query(
|
||||
@@ -180,7 +195,7 @@ def get_approval_rate_report(
|
||||
func.sum(case((Booking.status == "rejected", 1), else_=0)).label("rejected"),
|
||||
func.sum(case((Booking.status == "pending", 1), else_=0)).label("pending"),
|
||||
func.sum(case((Booking.status == "canceled", 1), else_=0)).label("canceled"),
|
||||
)
|
||||
).join(Space, Booking.space_id == Space.id)
|
||||
|
||||
# Apply date filters
|
||||
if start_date:
|
||||
@@ -193,6 +208,11 @@ def get_approval_rate_report(
|
||||
Booking.start_datetime <= datetime.combine(end_date, datetime.max.time())
|
||||
)
|
||||
|
||||
# Property scoping for managers
|
||||
if current_admin.role == "manager":
|
||||
managed_ids = get_manager_property_ids(db, current_admin.id)
|
||||
query = query.filter(Space.property_id.in_(managed_ids))
|
||||
|
||||
result = query.first()
|
||||
|
||||
total = result.total or 0
|
||||
|
||||
Reference in New Issue
Block a user