Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"use client";

import * as React from "react";

import {
Card,
CardContent,
CardHeader,
CardFooter,
CardTitle,
} from "@bubba/ui/card";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@bubba/ui/chart";
import { useI18n } from "@/locales/client";
import { Bar, BarChart, XAxis, YAxis, Legend, ResponsiveContainer, LabelList } from "recharts"

interface AssigneeData {
id: string;
name: string;
total: number;
published: number;
draft: number;
archived: number;
needs_review: number;
}

interface PolicyAssigneeChartProps {
data?: AssigneeData[] | null;
}

const CHART_COLORS = {
published: "hsl(var(--chart-positive))", // green
draft: "hsl(var(--chart-neutral))", // yellow
archived: "hsl(var(--chart-warning))", // gray
needs_review: "hsl(var(--chart-destructive))", // red
};

export function PolicyAssigneeChart({ data }: PolicyAssigneeChartProps) {
const t = useI18n();

if (!data || data.length === 0) {
return (
<Card className="flex flex-col">
<CardHeader>
<CardTitle>
{t("policies.dashboard.policies_by_assignee") ||
"Policies by Assignee"}
</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center">
<p className="text-center text-sm text-muted-foreground">
No policies assigned to users
</p>
</CardContent>
<CardFooter>
<div className="flex flex-wrap gap-4 py-2" />
</CardFooter>
</Card>
);
}

// Sort assignees by total policies (descending)
const sortedData = React.useMemo(() => {
return [...data].sort((a, b) => b.total - a.total).slice(0, 4).reverse();
}, [data]);

const chartData = sortedData.map((item) => ({
name: item.name,
published: item.published,
draft: item.draft,
archived: item.archived,
needs_review: item.needs_review,
}));

const chartConfig = {
published: {
label: t("policies.status.published"),
color: CHART_COLORS.published,
},
draft: {
label: t("policies.status.draft"),
color: CHART_COLORS.draft,
},
archived: {
label: t("policies.status.archived"),
color: CHART_COLORS.archived,
},
needs_review: {
label: t("policies.status.needs_review"),
color: CHART_COLORS.needs_review,
},
} satisfies ChartConfig;

return (
<Card className="flex flex-col">
<CardHeader>
<CardTitle>
{t("policies.dashboard.policies_by_assignee") ||
"Policies by Assignee"}
</CardTitle>
</CardHeader>
<CardContent className="flex-1">
<ChartContainer config={chartConfig}>
<ResponsiveContainer width="100%" height={250}>
<BarChart
accessibilityLayer
data={chartData}
layout="vertical"
barSize={30}
barGap={8}
margin={{
top: 0,
right: 0,
bottom: 0,
left: 0
}}
>
<XAxis type="number" hide />
<YAxis
dataKey="name"
type="category"
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.split(' ')[0]}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent />}
/>
<Bar dataKey="published" stackId="a" fill={CHART_COLORS.published} />
<Bar dataKey="draft" stackId="a" fill={CHART_COLORS.draft} />
<Bar dataKey="archived" stackId="a" fill={CHART_COLORS.archived} />
<Bar dataKey="needs_review" stackId="a" fill={CHART_COLORS.needs_review} />
</BarChart>
</ResponsiveContainer>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex flex-wrap gap-4 py-2">
{Object.entries(chartConfig).map(([key, config]) => (
<div key={key} className="flex items-center gap-2">
<div
className="h-3 w-3"
style={{ backgroundColor: config.color }}
/>
<span className="text-xs">{config.label}</span>
</div>
))}
</div>
</CardFooter>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"use client";

import * as React from "react";
import { Pie, PieChart, Label, Tooltip, ResponsiveContainer } from "recharts";

import {
Card,
CardContent,
CardHeader,
CardFooter,
CardTitle,
} from "@bubba/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from "@bubba/ui/chart";
import { useI18n } from "@/locales/client";

interface PolicyOverviewData {
totalPolicies: number;
publishedPolicies: number;
draftPolicies: number;
archivedPolicies: number;
needsReviewPolicies: number;
}

interface PolicyStatusChartProps {
data?: PolicyOverviewData | null;
}

const CHART_COLORS = {
published: "hsl(var(--chart-positive))", // green
draft: "hsl(var(--chart-neutral))", // yellow
archived: "hsl(var(--chart-warning))", // gray
needs_review: "hsl(var(--chart-destructive))", // red
};

// Custom tooltip component for the pie chart
const StatusTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="rounded-md border bg-background p-2 shadow-md">
<p className="text-xs font-medium">{data.name}</p>
<p className="text-xs">
Count: <span className="font-medium">{data.value}</span>
</p>
</div>
);
}
return null;
};

export function PolicyStatusChart({ data }: PolicyStatusChartProps) {
const t = useI18n();

if (!data) {
return (
<Card className="flex flex-col">
<CardHeader>
<CardTitle>{t("policies.dashboard.policy_status") || "Policy Status"}</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center">
<p className="text-center text-sm text-muted-foreground">
No policy data available
</p>
</CardContent>
<CardFooter>
<div className="flex flex-wrap gap-4 py-2" />
</CardFooter>
</Card>
);
}

const chartData = React.useMemo(() => {
const items = [
{
name: t("policies.status.published"),
value: data.publishedPolicies,
fill: CHART_COLORS.published,
},
{
name: t("policies.status.draft"),
value: data.draftPolicies,
fill: CHART_COLORS.draft,
},
{
name: t("policies.status.archived"),
value: data.archivedPolicies,
fill: CHART_COLORS.archived,
},
{
name: t("policies.status.needs_review"),
value: data.needsReviewPolicies,
fill: CHART_COLORS.needs_review,
},
];

return items.filter(item => item.value);
}, [data, t]);

const chartConfig = {
value: {
label: "Count",
},
} satisfies ChartConfig;

return (
<Card className="flex flex-col">
<CardHeader>
<CardTitle>{t("policies.dashboard.policy_status") || "Policy Status"}</CardTitle>
</CardHeader>
<CardContent className="flex-1 flex items-center justify-center">
<ChartContainer
config={chartConfig}
className="mx-auto h-[300px] max-w-[250px]"
>
<PieChart
width={250}
height={300}
margin={{
top: 16,
right: 16,
bottom: 16,
left: 16,
}}
>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent />}
/>
<Pie
data={chartData}
dataKey="value"
nameKey="name"
innerRadius={60}
outerRadius={80}
paddingAngle={2}
strokeWidth={5}
stroke="bg-accent"
cursor="pointer"
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-bold"
>
{data.totalPolicies}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
Total
</tspan>
</text>
);
}
return null;
}}
/>
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
<CardFooter>
<div className="flex flex-wrap gap-4 py-2">
{chartData.map((entry) => (
<div key={entry.name} className="flex items-center gap-2">
<div
className="h-3 w-3"
style={{ backgroundColor: entry.fill }}
/>
<span className="text-xs">
{entry.name}
<span className="ml-1 text-muted-foreground">
({entry.value})
</span>
</span>
</div>
))}
</div>
</CardFooter>
</Card>
);
}
Loading