403 lines
11 KiB
TypeScript
403 lines
11 KiB
TypeScript
import { useEffect, useReducer, useRef, useState } from "react";
|
||
import {
|
||
Bar,
|
||
BarChart,
|
||
CartesianGrid,
|
||
Cell,
|
||
LabelList,
|
||
ResponsiveContainer,
|
||
XAxis,
|
||
YAxis,
|
||
} from "recharts";
|
||
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
|
||
import { Skeleton } from "~/components/ui/skeleton";
|
||
import apiService from "~/lib/api";
|
||
import { formatNumber } from "~/lib/utils";
|
||
import { ChartContainer } from "../ui/chart";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuButton,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
} from "../ui/dropdown-menu";
|
||
import { TruncatedText } from "../ui/truncatedText";
|
||
|
||
interface StrategicAlignmentData {
|
||
strategic_theme: string;
|
||
operational_fee_sum: number;
|
||
percentage?: number;
|
||
}
|
||
|
||
interface DropDownConfig {
|
||
isOpen: boolean;
|
||
selectedValue: string;
|
||
dropDownItems: Array<string>;
|
||
}
|
||
|
||
type Action =
|
||
| { type: "OPEN" }
|
||
| { type: "CLOSE" }
|
||
| { type: "SETVALUE"; value: Array<string> }
|
||
| { type: "SELECT"; value: string };
|
||
|
||
// const DropDownItems = [
|
||
// {
|
||
// id: 0,
|
||
// key: "همه مضامین",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// {
|
||
// id: 1,
|
||
// key: "ارزش های هم افزایی نوآورانه",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// {
|
||
// id: 2,
|
||
// key: "ارزش های خودکفایی نوآوورانه",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// {
|
||
// id: 3,
|
||
// key: "ارزش های فناوری های نوین",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// {
|
||
// id: 4,
|
||
// key: "ارزش های توسعه منابع انسانی",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// {
|
||
// id: 5,
|
||
// key: "ارزش های نوآوری سبز",
|
||
// Value: "همه مضامین",
|
||
// },
|
||
// ];
|
||
|
||
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);
|
||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||
const [state, dispatch] = useReducer(reducer, {
|
||
isOpen: false,
|
||
selectedValue: "همه مضامین",
|
||
dropDownItems: [],
|
||
});
|
||
|
||
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;
|
||
|
||
setBarItems(responseData);
|
||
const dropDownItems = responseData.map(
|
||
(item: any) => item.strategic_theme
|
||
);
|
||
|
||
setDropDownValues(["همه مضامین", ...dropDownItems]);
|
||
} catch (error) {
|
||
console.error("Error fetching strategic alignment data:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchDropDownItems = async (item: string) => {
|
||
try {
|
||
if (item !== "همه مضامین") {
|
||
const response = await apiService.select({
|
||
ProcessName: "project",
|
||
OutputFields: [
|
||
"value_technology_and_innovation",
|
||
"sum(operational_fee)",
|
||
],
|
||
Conditions: [["strategic_theme", "=", item]],
|
||
GroupBy: ["value_technology_and_innovation"],
|
||
});
|
||
|
||
const responseData =
|
||
typeof response.data === "string"
|
||
? JSON.parse(response.data)
|
||
: response.data;
|
||
setBarItems(responseData);
|
||
} else fetchData();
|
||
} catch (error) {
|
||
console.error("Error fetching strategic alignment data:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
function reducer(state: DropDownConfig, action: Action): DropDownConfig {
|
||
switch (action.type) {
|
||
case "OPEN":
|
||
return { ...state, isOpen: true };
|
||
case "CLOSE":
|
||
return { ...state, isOpen: false };
|
||
case "SETVALUE":
|
||
return { ...state, dropDownItems: action.value };
|
||
case "SELECT":
|
||
return { ...state, selectedValue: action.value };
|
||
default:
|
||
return state;
|
||
}
|
||
}
|
||
|
||
const toggleMenuHandler = () => {
|
||
dispatch({
|
||
type: "OPEN",
|
||
});
|
||
};
|
||
|
||
const selectItem = (item: string) => {
|
||
dispatch({
|
||
type: "SELECT",
|
||
value: item,
|
||
});
|
||
|
||
dispatch({
|
||
type: "CLOSE",
|
||
});
|
||
|
||
fetchDropDownItems(item);
|
||
};
|
||
|
||
const setDropDownValues = (items: Array<string>) => {
|
||
dispatch({
|
||
type: "SETVALUE",
|
||
value: items,
|
||
});
|
||
};
|
||
|
||
const setBarItems = (responseData: any) => {
|
||
const processedData = responseData
|
||
.map((item: any) => ({
|
||
strategic_theme:
|
||
item.strategic_theme || item.value_technology_and_innovation || "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 || []);
|
||
};
|
||
|
||
const dialogHandler = (status: boolean) => {
|
||
if (onOpenChange) onOpenChange(status);
|
||
dispatch({
|
||
type: "SELECT",
|
||
value: "همه مضامین",
|
||
});
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
if (
|
||
contentRef.current &&
|
||
!contentRef.current.contains(event.target as Node)
|
||
) {
|
||
dispatch({
|
||
type: "CLOSE",
|
||
});
|
||
}
|
||
};
|
||
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
};
|
||
}, [open]);
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={dialogHandler}>
|
||
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
|
||
<DialogHeader className="mb-10 w-full border-b-2 border-gray-500/20">
|
||
<div>
|
||
<div className="flex">
|
||
<DropdownMenu
|
||
modal={true}
|
||
open={state.isOpen}
|
||
onOpenChange={toggleMenuHandler}
|
||
>
|
||
<DropdownMenuButton>{state.selectedValue}</DropdownMenuButton>
|
||
|
||
<DropdownMenuContent
|
||
ref={contentRef}
|
||
forceMount={true}
|
||
className="w-56"
|
||
>
|
||
{state.dropDownItems.map((item: string, key: number) => (
|
||
<div
|
||
onClick={() => selectItem(item)}
|
||
key={`${key}-${item}`}
|
||
>
|
||
<DropdownMenuItem selected={state.selectedValue === item}>
|
||
{item}
|
||
</DropdownMenuItem>
|
||
</div>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|