diff --git a/apps/app/src/app/(app)/[orgId]/components/DynamicMinHeight.tsx b/apps/app/src/app/(app)/[orgId]/components/DynamicMinHeight.tsx new file mode 100644 index 000000000..d45fa4553 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/components/DynamicMinHeight.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { cn } from '@comp/ui/cn'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +interface DynamicMinHeightProps { + children: React.ReactNode; + className?: string; +} + +export function DynamicMinHeight({ children, className }: DynamicMinHeightProps) { + const containerRef = useRef(null); + const [offsetPx, setOffsetPx] = useState(0); + + useEffect(() => { + const headerEl = document.querySelector('header.sticky') as HTMLElement | null; + const bannerEl = document.getElementById('onboarding-banner') as HTMLElement | null; + + const compute = () => { + const header = headerEl?.offsetHeight ?? 0; + const banner = bannerEl?.offsetHeight ?? 0; + // Add 1px border for each element like the server calculation did + const extra = 0; // borders already included in offsetHeight + setOffsetPx(header + banner + extra); + }; + + compute(); + + const resizeObserver = new ResizeObserver(() => compute()); + if (headerEl) resizeObserver.observe(headerEl); + if (bannerEl) resizeObserver.observe(bannerEl); + + const onResize = () => compute(); + window.addEventListener('resize', onResize); + + return () => { + resizeObserver.disconnect(); + window.removeEventListener('resize', onResize); + }; + }, []); + + const style = useMemo(() => ({ minHeight: `calc(100vh - ${offsetPx}px)` }), [offsetPx]); + + return ( +
+ {children} +
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 987b15f8d..28fdcc4f3 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -197,7 +197,10 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => } return ( - +
{renderStatusContent()}
diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx index 86de5fd77..02bd9d40e 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkCard.tsx @@ -8,6 +8,7 @@ import type { Control, Task } from '@db'; import { BarChart3, Clock } from 'lucide-react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; +import { computeFrameworkStats } from '../lib/compute'; import type { FrameworkInstanceWithControls } from '../types'; interface FrameworkCardProps { @@ -51,44 +52,13 @@ export function FrameworkCard({ return 'text-red-600 dark:text-red-400'; }; - const controlsCount = frameworkInstance.controls?.length || 0; - const compliantControlsCount = Math.round((complianceScore / 100) * controlsCount); - - // Calculate not started controls: controls where all policies are draft or non-existent AND all tasks are todo or non-existent - const notStartedControlsCount = - frameworkInstance.controls?.filter((control) => { - // If a control has no policies and no tasks, it's not started. - const controlTasks = tasks.filter((task) => task.controls.some((c) => c.id === control.id)); - - if ((!control.policies || control.policies.length === 0) && controlTasks.length === 0) { - return true; - } - - // Check if ALL policies are in draft state or non-existent - const policiesNotStarted = - !control.policies || - control.policies.length === 0 || - control.policies.every((policy) => policy.status === 'draft'); - - // Check if ALL tasks are in todo state or there are no tasks - const tasksNotStarted = - controlTasks.length === 0 || controlTasks.every((task) => task.status === 'todo'); - - return policiesNotStarted && tasksNotStarted; - // If either any policy is not draft or any task is not todo, it's in progress - }).length || 0; - - // Calculate in progress controls: Total - Compliant - Not Started - const inProgressCount = Math.max( - 0, // Ensure count doesn't go below zero - controlsCount - compliantControlsCount - notStartedControlsCount, + const { totalPolicies, publishedPolicies, totalTasks, doneTasks } = computeFrameworkStats( + frameworkInstance, + tasks, ); - // Use direct framework data: const frameworkDetails = frameworkInstance.framework; const statusBadge = getStatusBadge(complianceScore); - - // Calculate last activity date - use current date as fallback const lastActivityDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short', @@ -129,11 +99,21 @@ export function FrameworkCard({ - {/* Stats */} -
- {compliantControlsCount} complete - {inProgressCount} active - {controlsCount} total + {/* Breakdown */} +
+
+ Policies + + {publishedPolicies}/{totalPolicies} published + +
+
+ Tasks + + {doneTasks}/{totalTasks} done + +
+ {/* Intentionally omit controls in card to reduce noise */}
{/* Footer */} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkList.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkList.tsx index 5ad5b796f..36b5281ac 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkList.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworkList.tsx @@ -1,25 +1,25 @@ 'use client'; import { Control, Task } from '@db'; -import type { FrameworkInstanceWithControls } from '../types'; import { FrameworkCard } from './FrameworkCard'; +import type { FrameworkInstanceWithComplianceScore } from './types'; export function FrameworkList({ - frameworksWithControls, + frameworksWithCompliance, tasks, }: { - frameworksWithControls: FrameworkInstanceWithControls[]; + frameworksWithCompliance: FrameworkInstanceWithComplianceScore[]; tasks: (Task & { controls: Control[] })[]; }) { - if (!frameworksWithControls.length) return null; + if (!frameworksWithCompliance.length) return null; return (
- {frameworksWithControls.map((frameworkInstance) => ( + {frameworksWithCompliance.map(({ frameworkInstance, complianceScore }) => ( ))} diff --git a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx index 74b3822b1..8ff594329 100644 --- a/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/frameworks/components/FrameworksOverview.tsx @@ -10,17 +10,20 @@ import { useState } from 'react'; import type { FrameworkInstanceWithControls } from '../types'; import { AddFrameworkModal } from './AddFrameworkModal'; import { FrameworkList } from './FrameworkList'; +import type { FrameworkInstanceWithComplianceScore } from './types'; export interface FrameworksOverviewProps { frameworksWithControls: FrameworkInstanceWithControls[]; tasks: (Task & { controls: Control[] })[]; allFrameworks: FrameworkEditorFramework[]; + frameworksWithCompliance?: FrameworkInstanceWithComplianceScore[]; } export function FrameworksOverview({ frameworksWithControls, tasks, allFrameworks, + frameworksWithCompliance, }: FrameworksOverviewProps) { const params = useParams<{ orgId: string }>(); const organizationId = params.orgId; @@ -34,7 +37,13 @@ export function FrameworksOverview({ return (
- + ({ frameworkInstance: fw, complianceScore: 0 })) + } + tasks={tasks} + />