diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 6881747..2d159e3 100755 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -20,6 +20,8 @@ from app.api.schemas import ( ResolvedItemResponse, ErrorItemResponse, AnalyticsResponse, + AgencyAnalyticsItem, + AgencyAnalyticsResponse, DropdownOptionsResponse, AgencyResponse, AgencyCreate, @@ -587,6 +589,21 @@ async def get_analytics( return AnalyticsResponse(**analytics) +@router.get("/analytics/by-agency", response_model=AgencyAnalyticsResponse) +async def get_analytics_by_agency( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_db_user), +): + """Get per-agency analytics breakdown (admin only).""" + if current_user.role not in ("super_admin", "oversight_admin"): + return AgencyAnalyticsResponse(agencies=[]) + repo = CampaignRepository(db) + rows = await repo.get_analytics_by_agency() + return AgencyAnalyticsResponse( + agencies=[AgencyAnalyticsItem(**row) for row in rows] + ) + + # --------------------------------------------------------------------------- # User Management endpoints # --------------------------------------------------------------------------- diff --git a/backend/app/api/schemas.py b/backend/app/api/schemas.py index 5d52793..a10ce0f 100755 --- a/backend/app/api/schemas.py +++ b/backend/app/api/schemas.py @@ -148,6 +148,20 @@ class AnalyticsResponse(BaseModel): legal_review: int +class AgencyAnalyticsItem(BaseModel): + agency_id: uuid.UUID + agency_name: str + total_reviews: int + passed: int + failed: int + errors: int + legal_review: int + + +class AgencyAnalyticsResponse(BaseModel): + agencies: list[AgencyAnalyticsItem] + + # Dropdown options schemas class DropdownOptionsResponse(BaseModel): campaigns: list[str] diff --git a/backend/app/repositories/campaign_repository.py b/backend/app/repositories/campaign_repository.py index d1654ab..68a67ff 100755 --- a/backend/app/repositories/campaign_repository.py +++ b/backend/app/repositories/campaign_repository.py @@ -5,7 +5,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.models.models import Campaign, Proof, ProofVersion +from app.models.models import Agency, Campaign, Proof, ProofVersion class CampaignRepository: @@ -164,3 +164,39 @@ class CampaignRepository: "errors": row.errors, "legal_review": row.legal_review, } + + async def get_analytics_by_agency(self) -> list[dict]: + """Get analytics data grouped by agency.""" + query = ( + select( + Agency.id.label("agency_id"), + Agency.name.label("agency_name"), + func.count(ProofVersion.id).label("total"), + func.count(ProofVersion.id).filter(ProofVersion.overall_status == "Passed").label("passed"), + func.count(ProofVersion.id).filter(ProofVersion.overall_status == "Failed").label("failed"), + func.count(ProofVersion.id).filter(ProofVersion.overall_status == "Analysis Error").label("errors"), + func.count(ProofVersion.id).filter(ProofVersion.overall_status == "Requires Manual Legal Review").label("legal_review"), + ) + .select_from(ProofVersion) + .join(Proof) + .join(Campaign) + .join(Agency, Campaign.agency_id == Agency.id) + .group_by(Agency.id, Agency.name) + .order_by(func.count(ProofVersion.id).desc()) + ) + + result = await self.session.execute(query) + rows = result.all() + + return [ + { + "agency_id": row.agency_id, + "agency_name": row.agency_name, + "total_reviews": row.total, + "passed": row.passed, + "failed": row.failed, + "errors": row.errors, + "legal_review": row.legal_review, + } + for row in rows + ] diff --git a/frontend/App.tsx b/frontend/App.tsx index 0e20f38..976ac9f 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -852,7 +852,7 @@ const AppContent: React.FC<{ msalInstance: any }> = ({ msalInstance }) => { const renderContent = () => { switch (currentView) { case 'Analytics': - return ; + return ; case 'Profile': return ; case 'CopyGenAI': diff --git a/frontend/components/Analytics.tsx b/frontend/components/Analytics.tsx index 4dd9b7a..b1b1267 100755 --- a/frontend/components/Analytics.tsx +++ b/frontend/components/Analytics.tsx @@ -5,7 +5,7 @@ import { BugIcon } from './icons/BugIcon'; import { ClockIcon } from './icons/ClockIcon'; import { LightbulbIcon } from './icons/LightbulbIcon'; import { ExclamationTriangleIcon } from './icons/StatusIcons'; -import apiService, { AnalyticsResponse } from '../services/apiService'; +import apiService, { AnalyticsResponse, AgencyAnalyticsItem } from '../services/apiService'; // Agent performance is still static for now - would need separate API const agentPerformance = [ @@ -29,8 +29,9 @@ const TrendIndicator: React.FC<{ trend: 'up' | 'down' | 'stable' }> = ({ trend } return
Stable
; }; -export const Analytics: React.FC<{ agencyId?: string }> = ({ agencyId }) => { +export const Analytics: React.FC<{ agencyId?: string; isAdmin?: boolean }> = ({ agencyId, isAdmin }) => { const [analytics, setAnalytics] = useState(null); + const [agencyAnalytics, setAgencyAnalytics] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -48,6 +49,19 @@ export const Analytics: React.FC<{ agencyId?: string }> = ({ agencyId }) => { loadAnalytics(); }, [agencyId]); + useEffect(() => { + if (!isAdmin) return; + const loadAgencyAnalytics = async () => { + try { + const data = await apiService.getAnalyticsByAgency(); + setAgencyAnalytics(data.agencies); + } catch (error) { + console.error('Failed to load agency analytics:', error); + } + }; + loadAgencyAnalytics(); + }, [isAdmin]); + // Calculate stats from API data // Exclude errors from denominator since they weren't successfully reviewed const reviewedCount = analytics @@ -92,6 +106,63 @@ export const Analytics: React.FC<{ agencyId?: string }> = ({ agencyId }) => { + {/* Per-Agency Breakdown (admin only) */} + {isAdmin && agencyAnalytics.length > 0 && ( +
+

Agency performance

+
+
+ + + + + + + + + + + + + {agencyAnalytics.map((agency, index) => { + const reviewed = agency.passed + agency.failed + agency.legal_review; + const rate = reviewed > 0 ? Math.round((agency.passed / reviewed) * 100) : 0; + const isSelected = agencyId === agency.agency_id; + return ( + + + + + + + + + ); + })} + +
AgencyProofs ReviewedPass RateFailedErrorsLegal Review
+ {agency.agency_name} + {isSelected && (selected)} + {agency.total_reviews} +
+ = 80 ? 'bg-success' : rate < 70 ? 'bg-error' : 'bg-warning'}`}> + {rate}% +
+
{agency.failed}{agency.errors}{agency.legal_review}
+
+
+
+ )} + {/* AI Performance Summary */}

AI performance summary

diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 66b1a77..df4f042 100755 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -55,6 +55,20 @@ export interface AnalyticsResponse { legal_review: number; } +export interface AgencyAnalyticsItem { + agency_id: string; + agency_name: string; + total_reviews: number; + passed: number; + failed: number; + errors: number; + legal_review: number; +} + +export interface AgencyAnalyticsResponse { + agencies: AgencyAnalyticsItem[]; +} + export interface FlaggedItemResponse { id: string; proof_version_id: string; @@ -263,12 +277,16 @@ class ApiService { return this.fetch(`/audit/errors${params}`); } - // Analytics endpoint + // Analytics endpoints async getAnalytics(agencyId?: string): Promise { const params = agencyId ? `?agency_id=${agencyId}` : ''; return this.fetch(`/analytics${params}`); } + async getAnalyticsByAgency(): Promise { + return this.fetch('/analytics/by-agency'); + } + // Helper to convert API response to frontend format convertCampaignToFrontend(campaign: CampaignResponse) { return {