Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 7 additions & 7 deletions backend/src/handlers/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ func (srv *Server) registerDashboardRoutes() []routeDef {
newAdminRoute("GET /api/department-metrics/login-trend", srv.handleDepartmentLoginTrend),
newAdminRoute("GET /api/department-metrics/facility-comparison", srv.handleFacilityEngagementComparison),
newAdminRoute("GET /api/department-metrics/knowledge-center", srv.handleKnowledgeCenterMetrics),
newAdminRoute("GET /api/department-metrics/programs/engagement-overview", srv.handleProgramEngagementOverview),
newAdminRoute("GET /api/department-metrics/programs/second-enrollment", srv.handleSecondProgramEnrollment),
newAdminRoute("GET /api/department-metrics/programs/completion-matrix", srv.handleProgramCompletionMatrix),
newAdminRoute("GET /api/department-metrics/programs/load-distribution", srv.handleProgramLoadDistribution),
newAdminRoute("GET /api/department-metrics/programs/enrollment-by-type", srv.handleEnrollmentByProgramType),
newAdminRoute("GET /api/dashboard/class-metrics", srv.handleClassDashboardMetrics),
newAdminRoute("GET /api/dashboard/facility-health", srv.handleFacilityHealthSummary),
newAdminFeatureRoute("GET /api/department-metrics/programs/engagement-overview", srv.handleProgramEngagementOverview, models.ProgramAccess),
newAdminFeatureRoute("GET /api/department-metrics/programs/second-enrollment", srv.handleSecondProgramEnrollment, models.ProgramAccess),
newAdminFeatureRoute("GET /api/department-metrics/programs/completion-matrix", srv.handleProgramCompletionMatrix, models.ProgramAccess),
newAdminFeatureRoute("GET /api/department-metrics/programs/load-distribution", srv.handleProgramLoadDistribution, models.ProgramAccess),
newAdminFeatureRoute("GET /api/department-metrics/programs/enrollment-by-type", srv.handleEnrollmentByProgramType, models.ProgramAccess),
newAdminFeatureRoute("GET /api/dashboard/class-metrics", srv.handleClassDashboardMetrics, models.ProgramAccess),
newAdminFeatureRoute("GET /api/dashboard/facility-health", srv.handleFacilityHealthSummary, models.ProgramAccess),
newAdminRoute("GET /api/users/{id}/admin-layer2", srv.handleAdminLayer2),
validatedRoute("GET /api/users/{id}/profile", srv.handleResidentProfile, resolver),
}
Expand Down
9 changes: 9 additions & 0 deletions backend/src/handlers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ func newAdminRoute(method string, handler HttpFunc) routeDef {
}
}

func newAdminFeatureRoute(method string, handler HttpFunc, features ...models.FeatureAccess) routeDef {
return routeDef{
routeMethod: method,
handler: handler,
admin: true,
features: features,
}
}

func newDeptAdminRoute(method string, handler HttpFunc) routeDef {
return routeDef{
routeMethod: method,
Expand Down
13 changes: 7 additions & 6 deletions backend/tests/integration/programs_core_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestProgramsCoreRoutes(t *testing.T) {
}

// TestProgramsCoreRouteAccess pins who may call core's Programs dashboard and
// metrics routes: any admin, with or without program_management.
// metrics routes: an admin with program_management.
func TestProgramsCoreRouteAccess(t *testing.T) {
env := SetupTestEnv(t)
defer env.CleanupTestEnv()
Expand All @@ -195,11 +195,12 @@ func TestProgramsCoreRouteAccess(t *testing.T) {
NewRequest[any](client, t, http.MethodGet, path, nil).
WithTestClaims(resident).Do().
ExpectStatus(http.StatusUnauthorized).ExpectMessage("Unauthorized - not admin")
for _, c := range []*handlers.Claims{facAdmin, withFeatures(facAdmin, models.ProgramAccess)} {
resp := NewRequest[any](client, t, http.MethodGet, path, nil).
WithTestClaims(c).AsRaw().Do()
require.NotEqual(t, http.StatusUnauthorized, resp.resp.StatusCode, resp.rawBody)
}
NewRequest[any](client, t, http.MethodGet, path, nil).
WithTestClaims(withFeatures(facAdmin, models.ProgramAccess)).Do().
ExpectStatus(http.StatusUnauthorized).ExpectMessage("Feature not enabled")
resp := NewRequest[any](client, t, http.MethodGet, path, nil).
WithTestClaims(facAdmin).AsRaw().Do()
require.NotEqual(t, http.StatusUnauthorized, resp.resp.StatusCode, resp.rawBody)
})
}
}
Expand Down
49 changes: 34 additions & 15 deletions frontend/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useMemo, ComponentProps } from 'react';
import { useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import { useAuth, canSwitchFacility } from '@/auth/useAuth';
import { useAuth, canSwitchFacility, hasFeature } from '@/auth/useAuth';
import {
ClassMetrics,
FeatureAccess,
FacilityHealthSummary,
MissingAttendanceItem,
TodaysScheduleItem,
Expand Down Expand Up @@ -51,10 +52,14 @@ function SurfaceCard({ className, ...props }: ComponentProps<'div'>) {
export default function Dashboard() {
const { user } = useAuth();
const deptAdmin = user ? canSwitchFacility(user) : false;
const programsEnabled =
!!user && hasFeature(user, FeatureAccess.ProgramAccess);
const { data: classMetricsResp } = useSWR<ServerResponseOne<ClassMetrics>>(
deptAdmin
? '/api/dashboard/class-metrics?facility=all'
: '/api/dashboard/class-metrics'
!programsEnabled
? null
: deptAdmin
? '/api/dashboard/class-metrics?facility=all'
: '/api/dashboard/class-metrics'
);
const { data: missingAttendanceResp, isLoading: missingAttendanceLoading } =
useSWR<ServerResponseMany<MissingAttendanceItem>>(
Expand All @@ -65,9 +70,11 @@ export default function Dashboard() {
const { data: facilityHealthResp } = useSWR<
ServerResponseOne<FacilityHealthSummary[]>
>(
deptAdmin
? '/api/dashboard/facility-health?facility=all&days=3'
: '/api/dashboard/facility-health?days=3'
!programsEnabled
? null
: deptAdmin
? '/api/dashboard/facility-health?facility=all&days=3'
: '/api/dashboard/facility-health?days=3'
);
const { data: todaysScheduleResp, isLoading: todaysScheduleLoading } =
useSWR<ServerResponseMany<TodaysScheduleItem>>(
Expand All @@ -85,13 +92,15 @@ export default function Dashboard() {
<div className="max-w-7xl mx-auto px-6 py-8">
{deptAdmin ? (
<DeptAdminView
programsEnabled={programsEnabled}
metrics={classMetricsResp?.data}
missingAttendance={missingAttendanceResp?.data ?? []}
missingAttendanceLoading={missingAttendanceLoading}
facilityHealthSummaries={facilityHealthResp?.data ?? []}
/>
) : (
<FacilityAdminView
programsEnabled={programsEnabled}
facilityName={user?.facility.name ?? 'My Facility'}
metrics={classMetricsResp?.data}
missingAttendance={missingAttendanceResp?.data ?? []}
Expand All @@ -106,13 +115,15 @@ export default function Dashboard() {
}

function FacilityAdminView({
programsEnabled,
facilityName,
metrics,
missingAttendance,
missingAttendanceLoading,
todaysScheduleItems,
todaysScheduleLoading
}: {
programsEnabled: boolean;
facilityName: string;
metrics?: ClassMetrics;
missingAttendance: MissingAttendanceItem[];
Expand Down Expand Up @@ -142,7 +153,9 @@ function FacilityAdminView({
className="mb-[15px]"
/>

<MetricCards stats={stats} onNavigate={navigate} />
{programsEnabled && (
<MetricCards stats={stats} onNavigate={navigate} />
)}

<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
Expand All @@ -166,11 +179,13 @@ function FacilityAdminView({
}

function DeptAdminView({
programsEnabled,
metrics,
missingAttendance,
missingAttendanceLoading,
facilityHealthSummaries
}: {
programsEnabled: boolean;
metrics?: ClassMetrics;
missingAttendance: MissingAttendanceItem[];
missingAttendanceLoading: boolean;
Expand All @@ -190,15 +205,19 @@ function DeptAdminView({
<div>
<PageHeader title="Department Overview" className="mb-[15px]" />

<MetricCards stats={stats} onNavigate={navigate} />
{programsEnabled && (
<MetricCards stats={stats} onNavigate={navigate} />
)}

<div className="space-y-6">
<div>
<h2 className="text-brand-dark dark:text-white mb-4">
Facility Health Overview
</h2>
<FacilityHealthTable rows={facilityRows} />
</div>
{programsEnabled && (
<div>
<h2 className="text-brand-dark dark:text-white mb-4">
Facility Health Overview
</h2>
<FacilityHealthTable rows={facilityRows} />
</div>
)}
<div className="flex gap-6 flex-wrap lg:flex-nowrap">
<div className="flex-1 lg:flex-[2] min-w-75">
<MissingAttendanceWidget
Expand Down
43 changes: 27 additions & 16 deletions frontend/src/pages/insights/OperationalInsights.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import {
SelectValue
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useAuth, canSwitchFacility } from '@/auth/useAuth';
import { Facility, InsightsRangeKey, ServerResponseMany } from '@/types';
import { useAuth, canSwitchFacility, hasFeature } from '@/auth/useAuth';
import {
Facility,
FeatureAccess,
InsightsRangeKey,
ServerResponseMany
} from '@/types';
import OverviewTab from './OverviewTab';
import KnowledgeCenterTab from './KnowledgeCenterTab';
import ProgramsTab from './ProgramsTab';
Expand All @@ -23,6 +28,8 @@ const TAB_TRIGGER_CLASS =
export default function OperationalInsightsPage() {
const { user } = useAuth();
const canSwitch = user ? canSwitchFacility(user) : false;
const programsEnabled =
!!user && hasFeature(user, FeatureAccess.ProgramAccess);

const [activeRange, setActiveRange] = useState<InsightsRangeKey>('30D');
const [customFrom, setCustomFrom] = useState('');
Expand Down Expand Up @@ -156,12 +163,14 @@ export default function OperationalInsightsPage() {
>
Knowledge Center
</TabsTrigger>
<TabsTrigger
value="programs"
className={TAB_TRIGGER_CLASS}
>
Programs
</TabsTrigger>
{programsEnabled && (
<TabsTrigger
value="programs"
className={TAB_TRIGGER_CLASS}
>
Programs
</TabsTrigger>
)}
</TabsList>
<TabsContent value="overview" className="mt-0">
<OverviewTab
Expand All @@ -178,14 +187,16 @@ export default function OperationalInsightsPage() {
rangeLabel={rangeLabel}
/>
</TabsContent>
<TabsContent value="programs" className="mt-0">
<ProgramsTab
dateParams={dateParams}
selectedFacility={selectedFacility}
rangeLabel={rangeLabel}
scopeLabel={scopeLabel}
/>
</TabsContent>
{programsEnabled && (
<TabsContent value="programs" className="mt-0">
<ProgramsTab
dateParams={dateParams}
selectedFacility={selectedFacility}
rangeLabel={rangeLabel}
scopeLabel={scopeLabel}
/>
</TabsContent>
)}
</Tabs>
</SWRConfig>
</div>
Expand Down
Loading