223 lines
6.6 KiB
TypeScript
223 lines
6.6 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "~/components/ui/dialog";
|
||
import {
|
||
BarChart,
|
||
Bar,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
LabelList,
|
||
Cell,
|
||
} from "recharts";
|
||
import apiService from "~/lib/api";
|
||
import { Skeleton } from "~/components/ui/skeleton";
|
||
import { formatNumber } from "~/lib/utils";
|
||
import { ChartContainer } from "../ui/chart";
|
||
import { TruncatedText } from "../ui/truncatedText";
|
||
|
||
interface StrategicAlignmentData {
|
||
strategic_theme: string;
|
||
operational_fee_sum: number;
|
||
percentage?: number;
|
||
}
|
||
|
||
interface StrategicAlignmentPopupProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
}
|
||
|
||
// ✅ Chart config for shadcn/ui
|
||
const chartConfig = {
|
||
percentage: {
|
||
label: "",
|
||
color: "#3AEA83",
|
||
},
|
||
};
|
||
|
||
const maxHeight = 150;
|
||
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
||
|
||
const ChartSkeleton = () => (
|
||
|
||
<div className="flex justify-center h-96 w-full p-4">
|
||
{/* Chart bars */}
|
||
<div className=" w-full flex items-end gap-10">
|
||
{[...Array(9)].map((_, i) => (
|
||
<div key={i} className="flex flex-col items-center gap-1">
|
||
<Skeleton
|
||
className="w-10 bg-gray-700 rounded-md"
|
||
style={{ height: `${barHeights()}px` }}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Left space for Y-axis label */}
|
||
<div className="flex flex-col justify-between mr-2">
|
||
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||
</div>
|
||
</div>
|
||
);
|
||
export function StrategicAlignmentPopup({
|
||
open,
|
||
onOpenChange,
|
||
}: StrategicAlignmentPopupProps) {
|
||
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
fetchData();
|
||
}
|
||
}, [open]);
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await apiService.select({
|
||
ProcessName: "project",
|
||
OutputFields: [
|
||
"strategic_theme",
|
||
"sum(operational_fee) as operational_fee_sum",
|
||
],
|
||
GroupBy: ["strategic_theme"],
|
||
});
|
||
|
||
const responseData =
|
||
typeof response.data === "string"
|
||
? JSON.parse(response.data)
|
||
: response.data;
|
||
|
||
const processedData = responseData
|
||
.map((item: any) => ({
|
||
strategic_theme: item.strategic_theme || "N/A",
|
||
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
|
||
}))
|
||
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
|
||
|
||
const total = processedData.reduce(
|
||
(acc: number, item: StrategicAlignmentData) =>
|
||
acc + item.operational_fee_sum,
|
||
0
|
||
);
|
||
|
||
const dataWithPercentage = processedData.map(
|
||
(item: StrategicAlignmentData) => ({
|
||
...item,
|
||
percentage:
|
||
total > 0
|
||
? Math.round((item.operational_fee_sum / total) * 100)
|
||
: 0,
|
||
})
|
||
);
|
||
setData(dataWithPercentage || []);
|
||
} catch (error) {
|
||
console.error("Error fetching strategic alignment data:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
|
||
<DialogHeader className="mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
|
||
<DialogTitle className="ml-auto text-sm text-white ">میزان انطباق راهبردی</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
{loading ? (
|
||
<ChartSkeleton />
|
||
) : (
|
||
<>
|
||
<ResponsiveContainer width="100%" height={400}>
|
||
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
||
<BarChart
|
||
data={data}
|
||
margin={{ left: 12, right: 12 }}
|
||
barGap={15}
|
||
barSize={30}
|
||
accessibilityLayer
|
||
>
|
||
<CartesianGrid vertical={false} stroke="#475569" />
|
||
<XAxis
|
||
dataKey="strategic_theme"
|
||
tickLine={false}
|
||
axisLine={false}
|
||
tickMargin={10}
|
||
interval={0}
|
||
style={{ fill: "#94a3b8", fontSize: 14 }}
|
||
tick={(props) => {
|
||
const { x, y, payload } = props;
|
||
return (
|
||
<g transform={`translate(${x},${y})`}>
|
||
<foreignObject width={80} height={20} x={-45} y={0}>
|
||
<TruncatedText
|
||
maxWords={2}
|
||
text={payload.value}
|
||
/>
|
||
</foreignObject>
|
||
</g>
|
||
);
|
||
}}
|
||
/>
|
||
<YAxis
|
||
domain={[0, 100]}
|
||
tickLine={false}
|
||
axisLine={false}
|
||
tickMargin={20}
|
||
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||
tickFormatter={(value) =>
|
||
`${formatNumber(Math.round(value))}`
|
||
}
|
||
|
||
|
||
label={{
|
||
value: "تعداد برنامه ها" ,
|
||
angle: -90,
|
||
position: "insideLeft",
|
||
fill: "#94a3b8",
|
||
fontSize: 11,
|
||
offset: 0,
|
||
dy: 0,
|
||
style: { textAnchor: "middle" },
|
||
}}
|
||
/>
|
||
|
||
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
|
||
{data.map((entry, index) => (
|
||
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
|
||
))}
|
||
<LabelList
|
||
dataKey="percentage"
|
||
position="top"
|
||
offset={15}
|
||
|
||
style={{
|
||
fill: "#ffffff",
|
||
fontSize: "16px",
|
||
fontWeight: "bold",
|
||
}}
|
||
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||
/>
|
||
|
||
</Bar>
|
||
</BarChart>
|
||
</ChartContainer>
|
||
</ResponsiveContainer>
|
||
</>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|