Skip to content

Commit fa7bd19

Browse files
feat(ManagerDashboard): add EmployeesByPositionChart component to visualize employee distribution by position
1 parent 3b10d50 commit fa7bd19

File tree

2 files changed

+109
-5
lines changed

2 files changed

+109
-5
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
2+
import {
3+
ChartConfig,
4+
ChartContainer,
5+
ChartLegend,
6+
ChartLegendContent,
7+
ChartTooltip,
8+
ChartTooltipContent,
9+
} from '@/components/ui/chart';
10+
import { ChartItem } from '@/types';
11+
import { useMemo } from 'react';
12+
import { Label, Pie, PieChart, Sector } from 'recharts';
13+
import { PieSectorDataItem } from 'recharts/types/polar/Pie';
14+
15+
const PieLabel = ({ viewBox, totalEmployees }: { viewBox?: unknown; totalEmployees: number }) => {
16+
if (viewBox && typeof viewBox === 'object' && viewBox !== null && 'cx' in viewBox && 'cy' in viewBox) {
17+
const { cx, cy } = viewBox as { cx: number; cy: number };
18+
19+
return (
20+
<text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle">
21+
<tspan x={cx} y={cy} className="fill-foreground text-3xl font-bold">
22+
{totalEmployees}
23+
</tspan>
24+
25+
<tspan x={cx} y={cy + 24} className="fill-muted-foreground">
26+
Total
27+
</tspan>
28+
</text>
29+
);
30+
}
31+
32+
return null;
33+
};
34+
35+
const ActiveShape = ({ outerRadius = 0, ...props }: PieSectorDataItem) => (
36+
<g>
37+
<Sector {...props} outerRadius={outerRadius + 10} />
38+
<Sector {...props} outerRadius={outerRadius + 25} innerRadius={outerRadius + 12} />
39+
</g>
40+
);
41+
42+
export default function EmployeesByPositionChart({ chartData }: Readonly<{ chartData: ChartItem }>) {
43+
const normalizeKey = (s: string) => s.toLowerCase().replaceAll(/\s+/g, '_');
44+
45+
const transformedData = useMemo(() => {
46+
return chartData.labels.map((label, i) => ({
47+
position: normalizeKey(label),
48+
total: chartData.data[i] ?? 0,
49+
fill: `var(--chart-${(i % 6) + 1})`,
50+
}));
51+
}, [chartData]);
52+
53+
const chartConfig = useMemo(() => {
54+
const cfg: Record<string, { label: string; color?: string }> = {
55+
total: { label: 'Total Employees' },
56+
};
57+
58+
for (const [i, label] of chartData.labels.entries()) {
59+
cfg[normalizeKey(label)] = { label, color: `var(--chart-${(i % 6) + 1})` };
60+
}
61+
62+
return cfg;
63+
}, [chartData]) satisfies ChartConfig;
64+
65+
const totalEmployees = chartData.data.reduce((sum, val) => sum + val, 0);
66+
67+
const activeIndex = useMemo(
68+
() => transformedData.findIndex((entry) => entry.position === 'manager'),
69+
[transformedData],
70+
);
71+
72+
return (
73+
<Card className="flex h-full flex-col">
74+
<CardHeader className="pb-0">
75+
<CardTitle>Employees by Position</CardTitle>
76+
<CardDescription>Number of employees categorized by their job positions.</CardDescription>
77+
</CardHeader>
78+
79+
<CardContent className="max-h-90 flex-1 pb-0">
80+
<ChartContainer config={chartConfig} className="mx-auto aspect-square h-full">
81+
<PieChart accessibilityLayer>
82+
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
83+
<Pie
84+
data={transformedData}
85+
dataKey="total"
86+
nameKey="position"
87+
innerRadius={60}
88+
strokeWidth={5}
89+
activeIndex={activeIndex}
90+
activeShape={<ActiveShape />}
91+
>
92+
<Label content={<PieLabel totalEmployees={totalEmployees} />} />
93+
</Pie>
94+
<ChartLegend
95+
content={<ChartLegendContent nameKey="position" />}
96+
className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center"
97+
/>
98+
</PieChart>
99+
</ChartContainer>
100+
</CardContent>
101+
</Card>
102+
);
103+
}

resources/js/pages/dashboard/manager-dashboard.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import EmployeePaymentByMonthChart from '@/components/charts/employee-payment-by-month-chart';
2+
import EmployeesByPositionChart from '@/components/charts/employees-by-position-chart';
23
import NewPatientsByMonthChart from '@/components/charts/new-patients-by-month-chart';
34
import DashboardHeader from '@/components/helpers/dashboard-header';
45
import AppLayout from '@/layouts/app-layout';
@@ -7,18 +8,21 @@ import { BreadcrumbItem, ChartItem } from '@/types';
78
import { EmployeeInfo } from '@/types/application/employee';
89
import { Head } from '@inertiajs/react';
910

11+
// TODO: Add element to display total patients
1012
export interface TotalPatients {
1113
total: number;
1214
percentage_change: number;
1315
}
1416

17+
// TODO: Add element to display contracts data and chart
1518
export interface ContractsData {
1619
total_contracts: number;
1720
active_contracts: number;
1821
contracts_by_type: ChartItem;
1922
total_earnings_current_month: number;
2023
}
2124

25+
// TODO: Add element to display invoices data
2226
export interface InvoicesData {
2327
total_invoices: number;
2428
total_amount: number;
@@ -53,10 +57,6 @@ const breadcrumbs: BreadcrumbItem[] = [
5357
];
5458

5559
export default function ManagerDashboard({ data }: Readonly<ManagerDashboardProps>) {
56-
console.group('Manager Info');
57-
console.log(data);
58-
console.groupEnd();
59-
6060
return (
6161
<AppLayout breadcrumbs={breadcrumbs}>
6262
<Head title="Manager Dashboard" />
@@ -74,7 +74,8 @@ export default function ManagerDashboard({ data }: Readonly<ManagerDashboardProp
7474
<NewPatientsByMonthChart chartData={data.newPatientsByMonth} />
7575

7676
<EmployeePaymentByMonthChart chartData={data.employeePaymentsData} />
77-
{/* TODO: Implement other charts and data displays for contracts, invoices, and employee payments */}
77+
78+
<EmployeesByPositionChart chartData={data.employeesByPosition} />
7879
</div>
7980
</div>
8081
</AppLayout>

0 commit comments

Comments
 (0)