diff --git a/backend/app/api/routes/admin.py b/backend/app/api/routes/admin.py
index 467e03c..7b39207 100755
--- a/backend/app/api/routes/admin.py
+++ b/backend/app/api/routes/admin.py
@@ -7,12 +7,14 @@ for managing the application.
"""
import logging
+from datetime import datetime, timedelta
from enum import Enum
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from pydantic import BaseModel, Field
+from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.permissions import require_superuser
@@ -26,8 +28,9 @@ from app.core.exceptions import (
from app.crud.organization import organization as organization_crud
from app.crud.session import session as session_crud
from app.crud.user import user as user_crud
+from app.models.organization import Organization
from app.models.user import User
-from app.models.user_organization import OrganizationRole
+from app.models.user_organization import OrganizationRole, UserOrganization
from app.schemas.common import (
MessageResponse,
PaginatedResponse,
@@ -79,19 +82,23 @@ class BulkActionResult(BaseModel):
# ===== User Management Endpoints =====
+
class UserGrowthData(BaseModel):
date: str
- totalUsers: int
- activeUsers: int
+ total_users: int
+ active_users: int
+
class OrgDistributionData(BaseModel):
name: str
value: int
+
class UserStatusData(BaseModel):
name: str
value: int
+
class AdminStatsResponse(BaseModel):
user_growth: list[UserGrowthData]
organization_distribution: list[OrgDistributionData]
@@ -110,27 +117,28 @@ async def admin_get_stats(
db: AsyncSession = Depends(get_db),
) -> Any:
"""Get admin dashboard statistics."""
- from sqlalchemy import func, select
- from datetime import datetime, timedelta
-
# 1. User Growth (Last 30 days)
# Note: This is a simplified implementation. For production, consider a dedicated stats table or materialized view.
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
-
+
# Get all users created in last 30 days
- query = select(User).where(User.created_at >= thirty_days_ago).order_by(User.created_at)
+ query = (
+ select(User).where(User.created_at >= thirty_days_ago).order_by(User.created_at)
+ )
result = await db.execute(query)
recent_users = result.scalars().all()
-
+
# Get total count before 30 days
- count_query = select(func.count()).select_from(User).where(User.created_at < thirty_days_ago)
- result = await db.execute(count_query)
- base_count = result.scalar() or 0
-
+ count_query = (
+ select(func.count()).select_from(User).where(User.created_at < thirty_days_ago)
+ )
+ count_result = await db.execute(count_query)
+ base_count = count_result.scalar() or 0
+
# Aggregate by day
user_growth = []
current_total = base_count
-
+
# Create a map of date -> count
daily_counts = {}
for user in recent_users:
@@ -140,31 +148,30 @@ async def admin_get_stats(
daily_counts[date_str]["total"] += 1
if user.is_active:
daily_counts[date_str]["active"] += 1
-
+
# Fill in the last 30 days
for i in range(29, -1, -1):
date = datetime.utcnow() - timedelta(days=i)
date_str = date.strftime("%b %d")
-
+
day_data = daily_counts.get(date_str, {"total": 0, "active": 0})
current_total += day_data["total"]
-
+
# For active users, we'd ideally track history, but for now let's approximate
# by just counting current active users created up to this point
# This is a simplification
- active_count = current_total # Simplified
-
- user_growth.append(UserGrowthData(
- date=date_str,
- totalUsers=current_total,
- activeUsers=int(current_total * 0.8) # Mocking active ratio for demo visual appeal if real data lacks history
- ))
+ user_growth.append(
+ UserGrowthData(
+ date=date_str,
+ total_users=current_total,
+ active_users=int(
+ current_total * 0.8
+ ), # Mocking active ratio for demo visual appeal if real data lacks history
+ )
+ )
# 2. Organization Distribution
# Get top 5 organizations by member count
- from app.models.user_organization import UserOrganization
- from app.models.organization import Organization
-
org_query = (
select(Organization.name, func.count(UserOrganization.user_id).label("count"))
.join(UserOrganization, Organization.id == UserOrganization.organization_id)
@@ -173,24 +180,28 @@ async def admin_get_stats(
.limit(5)
)
result = await db.execute(org_query)
- org_dist = [OrgDistributionData(name=row.name, value=row.count) for row in result.all()]
-
+ org_dist = [
+ OrgDistributionData(name=row.name, value=row.count) for row in result.all()
+ ]
+
# 3. User Status
- active_query = select(func.count()).select_from(User).where(User.is_active == True)
- inactive_query = select(func.count()).select_from(User).where(User.is_active == False)
-
+ active_query = select(func.count()).select_from(User).where(User.is_active)
+ inactive_query = (
+ select(func.count()).select_from(User).where(User.is_active.is_(False))
+ )
+
active_count = (await db.execute(active_query)).scalar() or 0
inactive_count = (await db.execute(inactive_query)).scalar() or 0
-
+
user_status = [
UserStatusData(name="Active", value=active_count),
- UserStatusData(name="Inactive", value=inactive_count)
+ UserStatusData(name="Inactive", value=inactive_count),
]
-
+
return AdminStatsResponse(
user_growth=user_growth,
organization_distribution=org_dist,
- user_status=user_status
+ user_status=user_status,
)
diff --git a/backend/app/core/demo_data.json b/backend/app/core/demo_data.json
index d542738..c955533 100644
--- a/backend/app/core/demo_data.json
+++ b/backend/app/core/demo_data.json
@@ -34,7 +34,8 @@
"last_name": "Smith",
"is_superuser": false,
"organization_slug": "acme-corp",
- "role": "admin"
+ "role": "admin",
+ "is_active": true
},
{
"email": "bob@acme.com",
@@ -43,7 +44,18 @@
"last_name": "Jones",
"is_superuser": false,
"organization_slug": "acme-corp",
- "role": "member"
+ "role": "member",
+ "is_active": true
+ },
+ {
+ "email": "charlie@acme.com",
+ "password": "Demo123!",
+ "first_name": "Charlie",
+ "last_name": "Brown",
+ "is_superuser": false,
+ "organization_slug": "acme-corp",
+ "role": "member",
+ "is_active": false
},
{
"email": "carol@globex.com",
@@ -52,7 +64,18 @@
"last_name": "Williams",
"is_superuser": false,
"organization_slug": "globex",
- "role": "owner"
+ "role": "owner",
+ "is_active": true
+ },
+ {
+ "email": "dan@globex.com",
+ "password": "Demo123!",
+ "first_name": "Dan",
+ "last_name": "Miller",
+ "is_superuser": false,
+ "organization_slug": "globex",
+ "role": "member",
+ "is_active": true
},
{
"email": "dave@soylent.com",
@@ -61,7 +84,8 @@
"last_name": "Brown",
"is_superuser": false,
"organization_slug": "soylent",
- "role": "member"
+ "role": "member",
+ "is_active": true
},
{
"email": "eve@initech.com",
@@ -70,7 +94,8 @@
"last_name": "Davis",
"is_superuser": false,
"organization_slug": "initech",
- "role": "admin"
+ "role": "admin",
+ "is_active": true
},
{
"email": "frank@umbrella.com",
@@ -79,7 +104,18 @@
"last_name": "Miller",
"is_superuser": false,
"organization_slug": "umbrella",
- "role": "member"
+ "role": "member",
+ "is_active": true
+ },
+ {
+ "email": "george@umbrella.com",
+ "password": "Demo123!",
+ "first_name": "George",
+ "last_name": "Costanza",
+ "is_superuser": false,
+ "organization_slug": "umbrella",
+ "role": "member",
+ "is_active": false
},
{
"email": "grace@example.com",
@@ -88,7 +124,8 @@
"last_name": "Hopper",
"is_superuser": false,
"organization_slug": null,
- "role": null
+ "role": null,
+ "is_active": true
},
{
"email": "heidi@example.com",
@@ -97,7 +134,18 @@
"last_name": "Klum",
"is_superuser": false,
"organization_slug": null,
- "role": null
+ "role": null,
+ "is_active": true
+ },
+ {
+ "email": "ivan@example.com",
+ "password": "Demo123!",
+ "first_name": "Ivan",
+ "last_name": "Drago",
+ "is_superuser": false,
+ "organization_slug": null,
+ "role": null,
+ "is_active": false
}
]
}
\ No newline at end of file
diff --git a/backend/app/init_db.py b/backend/app/init_db.py
index d29f66d..934cf11 100644
--- a/backend/app/init_db.py
+++ b/backend/app/init_db.py
@@ -8,6 +8,8 @@ Creates the first superuser if configured and doesn't already exist.
import asyncio
import json
import logging
+import random
+from datetime import datetime, timedelta
from pathlib import Path
from sqlalchemy import select, text
@@ -137,15 +139,38 @@ async def load_demo_data(session):
session, email=user_data["email"]
)
if not existing_user:
+ # Create user
user_in = UserCreate(
email=user_data["email"],
password=user_data["password"],
first_name=user_data["first_name"],
last_name=user_data["last_name"],
is_superuser=user_data["is_superuser"],
+ is_active=user_data.get("is_active", True),
)
user = await user_crud.create(session, obj_in=user_in)
- logger.info(f"Created demo user: {user.email}")
+
+ # Randomize created_at for demo data (last 30 days)
+ # This makes the charts look more realistic
+ days_ago = random.randint(0, 30) # noqa: S311
+ random_time = datetime.utcnow() - timedelta(days=days_ago)
+ # Add some random hours/minutes variation
+ random_time = random_time.replace(
+ hour=random.randint(0, 23), # noqa: S311
+ minute=random.randint(0, 59), # noqa: S311
+ )
+
+ # Update the timestamp directly in the database
+ await session.execute(
+ text(
+ "UPDATE users SET created_at = :created_at WHERE id = :user_id"
+ ),
+ {"created_at": random_time, "user_id": user.id},
+ )
+
+ logger.info(
+ f"Created demo user: {user.email} (created {days_ago} days ago)"
+ )
# Add to organization if specified
org_slug = user_data.get("organization_slug")
diff --git a/backend/app/schemas/users.py b/backend/app/schemas/users.py
index 1229954..c0e08ef 100755
--- a/backend/app/schemas/users.py
+++ b/backend/app/schemas/users.py
@@ -23,6 +23,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase):
password: str
is_superuser: bool = False
+ is_active: bool = True
@field_validator("password")
@classmethod
diff --git a/frontend/src/components/charts/UserGrowthChart.tsx b/frontend/src/components/charts/UserGrowthChart.tsx
index 508d159..1c0da5a 100644
--- a/frontend/src/components/charts/UserGrowthChart.tsx
+++ b/frontend/src/components/charts/UserGrowthChart.tsx
@@ -21,8 +21,8 @@ import { CHART_PALETTES } from '@/lib/chart-colors';
export interface UserGrowthData {
date: string;
- totalUsers: number;
- activeUsers: number;
+ total_users: number;
+ active_users: number;
}
interface UserGrowthChartProps {
@@ -41,8 +41,8 @@ function generateMockData(): UserGrowthData[] {
const baseUsers = 100 + i * 3;
data.push({
date: format(date, 'MMM d'),
- totalUsers: baseUsers + Math.floor(Math.random() * 10),
- activeUsers: Math.floor(baseUsers * 0.8) + Math.floor(Math.random() * 5),
+ total_users: baseUsers + Math.floor(Math.random() * 10),
+ active_users: Math.floor(baseUsers * 0.8) + Math.floor(Math.random() * 5),
});
}
@@ -89,7 +89,7 @@ export function UserGrowthChart({ data, loading, error }: UserGrowthChartProps)
/>