Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Version: 1.0.0
Last Updated: 2025-12-31
Owning Module: PF (Platform Foundation)
Overview
This guide documents the standardized pattern for module dashboard/overview pages. All overview pages should use PageHeader and StatCard components for consistency.
Architecture
Overview Page
├── PageHeader (title, description, icon, actions)
├── Stat Card Grid (key metrics)
│ └── StatCard (individual metric)
├── Section Headers (optional)
├── Navigation Cards (AreaNavigationCard)
└── Additional Content (charts, tables, etc.)
Quick Start
import { PageHeader, StatCard } from '@/shared/components';
import { Users, Clock, AlertCircle } from 'lucide-react';
function HROverview() {
const { data: stats, isLoading } = useHRDashboardStats();
const statCards = [
{
title: 'Total Employees',
value: stats?.totalEmployees ?? 0,
description: 'Active staff members',
icon: Users,
onClick: () => navigate('/hr/employees'),
},
{
title: 'Pending Requests',
value: stats?.pendingRequests ?? 0,
description: 'Awaiting approval',
icon: Clock,
onClick: () => navigate('/hr/requests'),
variant: stats?.pendingRequests > 10 ? 'warning' : 'default',
},
];
return (
<OverviewPageWrapper refreshQueryKeys={['hr']} spacing="md">
<PageHeader
title="Workforce"
description="Manage employees, scheduling, and HR operations"
icon={Users}
/>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{statCards.map((card) => (
<StatCard
key={card.title}
{...card}
loading={isLoading}
/>
))}
</div>
</OverviewPageWrapper>
);
}
Dashboards vs Overviews
| Aspect | Dashboard | Overview |
|---|
| Route | /{core}/dashboard | /{core} |
| Purpose | Widget-based KPIs | Navigation hub |
| Component | ModuleDashboard | OverviewPageWrapper |
| Widgets | Customizable, draggable | Static layout |
| PTR | Built-in | Via wrapper |
When to Use Each
- Dashboard: When users need customizable widgets and real-time KPIs
- Overview: When users need a landing page with navigation to sub-areas
interface PageHeaderProps {
title: string;
description?: string;
icon?: LucideIcon;
actions?: React.ReactNode;
backLink?: string;
backLabel?: string;
}
Examples:
// Basic
<PageHeader
title="Workforce"
description="Manage employees and HR operations"
icon={Users}
/>
// With actions
<PageHeader
title="Employees"
description="View and manage staff"
icon={Users}
actions={
<Button onClick={() => navigate('/hr/employees/new')}>
<Plus className="h-4 w-4 mr-2" />
Add Employee
</Button>
}
/>
// With back link
<PageHeader
title="Employee Details"
backLink="/hr/employees"
backLabel="Back to Employees"
/>
StatCard
Metric display card with optional click action and variants.
interface StatCardProps {
title: string;
value: string | number;
description?: string;
icon?: LucideIcon;
onClick?: () => void;
variant?: 'default' | 'success' | 'warning' | 'destructive' | 'info';
loading?: boolean;
trend?: {
value: number;
direction: 'up' | 'down';
};
}
Examples:
// Basic
<StatCard
title="Total Residents"
value={42}
description="Current census"
icon={Users}
/>
// With click action
<StatCard
title="Pending Approvals"
value={5}
description="Awaiting review"
icon={Clock}
onClick={() => navigate('/approvals')}
/>
// With variant (for alerts)
<StatCard
title="Overdue Tasks"
value={12}
description="Need attention"
icon={AlertCircle}
variant="destructive"
/>
// With loading state
<StatCard
title="Revenue"
value={stats?.revenue ?? 0}
loading={isLoading}
/>
// With trend
<StatCard
title="Monthly Revenue"
value="$45,231"
trend={{ value: 12, direction: 'up' }}
/>
Stat Card Patterns
Defining Stat Cards
Use arrays for maintainability:
const statCards = [
{
title: 'Active Policies',
value: stats?.activePolicies ?? 0,
icon: FileText,
description: 'Published policies',
onClick: () => navigate('/gr/policies'),
},
{
title: 'Overdue Reviews',
value: stats?.overdueReviews ?? 0,
icon: AlertCircle,
description: 'Need attention',
variant: getVariant(stats?.overdueReviews),
onClick: () => navigate('/gr/reviews'),
},
];
Conditional Variants
Use a helper function for type safety:
function getVariant(value: number | undefined): 'default' | 'destructive' {
return value && value > 0 ? 'destructive' : 'default';
}
// Or for more complex logic
function getStatusVariant(
value: number | undefined,
warningThreshold: number,
errorThreshold: number
): StatCardVariant {
if (!value) return 'default';
if (value >= errorThreshold) return 'destructive';
if (value >= warningThreshold) return 'warning';
return 'default';
}
Rendering Stat Cards
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{statCards.map((card) => (
<StatCard
key={card.title}
title={card.title}
value={card.value}
description={card.description}
icon={card.icon}
onClick={card.onClick}
variant={card.variant}
loading={isLoading}
/>
))}
</div>
Grid Layouts
Standard Gaps
Use consistent gap sizing:
// Stat cards
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
// Navigation cards
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
// Content sections
<div className="space-y-6">
Responsive Columns
| Breakpoint | Stat Cards | Nav Cards |
|---|
| Mobile | 1 col | 1 col |
| md (768px) | 2 cols | 2 cols |
| lg (1024px) | 4 cols | 3 cols |
Section Organization
<div className="space-y-6">
<PageHeader title="Governance" icon={Shield} />
{/* Stat section */}
<div>
<h2 className="text-lg font-semibold mb-3">Policy Management</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{policyStats.map(card => <StatCard key={card.title} {...card} />)}
</div>
</div>
{/* Another section */}
<div>
<h2 className="text-lg font-semibold mb-3">Compliance</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{complianceStats.map(card => <StatCard key={card.title} {...card} />)}
</div>
</div>
</div>
For simpler pages:
<div className="space-y-6">
<PageHeader title="Dashboard" icon={LayoutDashboard} />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{allStats.map(card => <StatCard key={card.title} {...card} />)}
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{navigationCards.map(card => <AreaNavigationCard key={card.title} {...card} />)}
</div>
</div>
Conditional Rendering
AI Features
{aiEnabled && (
<div>
<h2 className="text-lg font-semibold mb-3">AI Assistant</h2>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{aiCards.map(card => <StatCard key={card.title} {...card} />)}
</div>
</div>
)}
Permission-Based
{hasPermission('hr.reports.view') && (
<StatCard
title="Reports"
value="View"
icon={BarChart}
onClick={() => navigate('/hr/reports')}
/>
)}
Existing Implementations
Reference these standardized overview pages:
| Module | Page | Location |
|---|
| HR | HROverview | /hr/dashboard |
| RH | RHOverview | /rh/dashboard |
| FA | FAOverview | /fa/dashboard |
| GR | GROverview | /gr/dashboard |
| FW | FWOverview | /fw/dashboard |
| FM | FMOverview | /fm/dashboard |
| LO | LOOverview | /lo/dashboard |
Best Practices
- Always use PageHeader - Never custom headers
- Always use StatCard - Never inline Card components
- Define cards as arrays - Better maintainability
- Handle loading states - Pass
loading prop to StatCard
- Use semantic variants -
destructive for errors, warning for alerts
- Keep grids consistent - Use standard gap and column patterns
- Group related metrics - Use section headers when needed
- Make cards clickable - Navigate to relevant detail pages
Anti-Patterns to Avoid
// ❌ DON'T: Inline Card components
<Card className="cursor-pointer" onClick={...}>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? <Skeleton /> : <div>{value}</div>}
</CardContent>
</Card>
// ✅ DO: Use StatCard
<StatCard
title={title}
value={value}
loading={isLoading}
onClick={...}
/>
// ❌ DON'T: Custom header divs
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">{title}</h1>
</div>
// ✅ DO: Use PageHeader
<PageHeader title={title} icon={icon} />
Patterns
Standards