inogen/app/components/dashboard/strategic-alignment-popup.tsx
2025-10-25 19:03:19 +03:30

426 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
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_count: 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: [],
});
const [date, setDate] = useStoredDate();
useEffect(() => {
if (open) {
fetchData();
}
}, [open]);
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
const fetchData = async () => {
setLoading(true);
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["strategic_theme", "count(operational_fee)"],
GroupBy: ["strategic_theme"],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
});
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",
"count(operational_fee)",
],
Conditions: [
["strategic_theme", "=", item, "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
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_count: Math.max(0, Number(item.operational_fee_count)),
}))
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
const total = processedData.reduce(
(acc: number, item: StrategicAlignmentData) =>
acc + item.operational_fee_count,
0
);
const dataWithPercentage = processedData.map(
(item: StrategicAlignmentData) => ({
...item,
percentage:
total > 0
? Math.round((item.operational_fee_count / 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>
);
}