inogen/app/components/dashboard/dashboard-home.tsx
2025-11-02 16:50:52 +03:30

856 lines
32 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { Book, CheckCircle } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { ChartContainer } from "~/components/ui/chart";
import { MetricCard } from "~/components/ui/metric-card";
import { Progress } from "~/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { D3ImageInfo } from "./d3-image-info";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { DashboardLayout } from "./layout";
export function DashboardHome() {
const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Chart and schematic data from select API
const [companyChartData, setCompanyChartData] = useState<
{
category: string;
capacity: number;
revenue: number;
cost: number;
costI: number;
capacityI: number;
revenueI: number;
}[]
>([]);
const [date, setDate] = useStoredDate();
useEffect(() => {
const handler = (date: CalendarDate) => {
if (date) setDate(date);
};
EventBus.on("dateSelected", handler);
return () => {
EventBus.off("dateSelected", handler);
};
}, []);
useEffect(() => {
if (date?.end && date?.start) fetchDashboardData();
}, [date]);
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
// Fetch top cards data
const topCardsResponse = await apiService.call({
main_page_first_function: {
start_date: date.start || null,
end_date: date.end || null,
},
});
// Fetch left section data
const leftCardsResponse = await apiService.call({
main_page_second_function: {
start_date: date.start || null,
end_date: date.end || null,
},
});
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
const leftCardsResponseData = JSON.parse(leftCardsResponse?.data);
console.log("API Responses:", {
topCardsResponseData,
leftCardsResponseData,
});
// Use real API data structure with English keys
const topData = topCardsResponseData || {};
const leftData = leftCardsResponseData || {};
const realData = {
topData: topData,
leftData: leftData,
chartData: leftCardsResponseData?.chartData || [],
};
setDashboardData(realData);
// Fetch company aggregates for chart and schematic (select API)
const selectPayload = {
ProcessName: "project",
OutputFields: [
"related_company",
"sum(pre_innovation_fee)",
"sum(innovation_cost_reduction)",
"sum(pre_project_production_capacity)",
"sum(increased_capacity_after_innovation)",
"sum(pre_project_income)",
"sum(increased_income_after_innovation)",
],
Conditions: [
["start_date", ">=", date.start || null, "and"],
["start_date", "<=", date.end || null],
],
GroupBy: ["related_company"],
};
const selectResp = await apiService.select(selectPayload);
const selectDataRaw = ((): any => {
try {
return typeof selectResp?.data === "string"
? JSON.parse(selectResp.data)
: selectResp?.data;
} catch {
return [];
}
})();
const rows: any[] = Array.isArray(selectDataRaw) ? selectDataRaw : [];
let incCapacityTotal = 0;
const chartRows = rows.map((r) => {
const rel = r?.related_company ?? "-";
const preFee =
Number(r?.pre_innovation_fee_sum ?? 0) >= 0
? r?.pre_innovation_fee_sum
: 0;
const costRed =
Number(r?.innovation_cost_reduction_sum ?? 0) >= 0
? r?.innovation_cost_reduction_sum
: 0;
const preCap =
Number(r?.pre_project_production_capacity_sum ?? 0) >= 0
? r?.pre_project_production_capacity_sum
: 0;
const incCap =
Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0
? r?.increased_capacity_after_innovation_sum
: 0;
const preInc =
Number(r?.pre_project_income_sum ?? 0) >= 0
? r?.pre_project_income_sum
: 0;
const incInc =
Number(r?.increased_income_after_innovation_sum ?? 0) >= 0
? r?.increased_income_after_innovation_sum
: 0;
incCapacityTotal += incCap;
const capacityPct = preCap >= 0 ? (incCap / preCap) * 100 : 0;
const revenuePct = preInc >= 0 ? (incInc / preInc) * 100 : 0;
const costPct = preFee >= 0 ? (costRed / preFee) * 100 : 0;
return {
category: rel,
capacity: isFinite(capacityPct) ? capacityPct : 0,
revenue: isFinite(revenuePct) ? revenuePct : 0,
cost: isFinite(costPct) ? costPct : 0,
costI: costRed,
capacityI: incCap,
revenueI: incInc,
};
});
setCompanyChartData(chartRows);
// setTotalIncreasedCapacity(incCapacityTotal);
} catch (error) {
console.error("Error fetching dashboard data:", error);
const errorMessage =
error instanceof Error ? error.message : "خطای نامشخص";
setError(`خطا در بارگذاری داده‌ها: ${errorMessage}`);
toast.error(`خطا در بارگذاری داده‌ها: ${errorMessage}`);
} finally {
setLoading(false);
}
};
// RadialBarChart data for ideas visualization
// const getIdeasChartData = () => {
// if (!dashboardData?.topData)
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
// const registered = parseFloat(
// dashboardData.topData.registered_innovation_technology_idea || "0"
// );
// const ongoing = parseFloat(
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
// );
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
// return [
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
// ];
// };
// const chartData = getIdeasChartData();
const chartConfig = {
visitors: {
label: "Ideas Progress",
},
safari: {
label: "Safari",
color: "var(--chart-2)",
},
};
// Skeleton component for cards
const SkeletonCard = ({ className = "" }) => (
<div
className={`bg-gray-700/50 rounded-lg overflow-hidden animate-pulse ${className}`}
>
<div className="p-6">
<div className="h-6 bg-gray-600 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-600 rounded w-1/2 mb-6"></div>
<div className="h-3 bg-gray-600 rounded w-full mb-2"></div>
<div className="h-3 bg-gray-600 rounded w-5/6"></div>
</div>
</div>
);
// Skeleton for the chart
const SkeletonChart = () => (
<div className="bg-gray-700/50 rounded-lg overflow-hidden animate-pulse p-6">
<div className="flex justify-between items-center mb-6">
<div className="h-6 bg-gray-600 rounded w-1/4"></div>
<div className="flex space-x-2 rtl:space-x-reverse">
<div className="h-8 w-24 bg-gray-600 rounded"></div>
<div className="h-8 w-24 bg-gray-600 rounded"></div>
<div className="h-8 w-24 bg-gray-600 rounded"></div>
</div>
</div>
<div className="h-64 bg-gray-800/50 rounded-lg flex items-end space-x-1 rtl:space-x-reverse p-4">
{[...Array(12)].map((_, i) => (
<div key={i} className="flex-1 flex space-x-1 rtl:space-x-reverse">
<div
className="w-full bg-blue-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-pr-green rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-pr-red rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
</div>
))}
</div>
<div className="flex justify-between mt-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-3 bg-gray-600 rounded w-1/6"></div>
))}
</div>
</div>
);
if (loading) {
return (
<DashboardLayout>
<div className="grid grid-cols-3 gap-4 animate-pulse">
{/* Top Cards Row */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
{/* Middle Section */}
<div className="col-span-2 space-y-6 h-full">
{/* Chart Section */}
<SkeletonChart />
</div>
{/* Right Sidebar */}
<div className="space-y-2">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</div>
</DashboardLayout>
);
}
if (error || !dashboardData) {
return (
<DashboardLayout>
<div className="">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-red-500/50">
<CardContent className="">
<div className="flex flex-col items-center justify-center space-y-4">
<div className="p-4 bg-red-500/20 rounded-full">
<CheckCircle className="w-12 h-12 text-red-400" />
</div>
<h3 className="text-xl font-bold text-red-400 text-center">
خطا در بارگذاری دادهها
</h3>
<p className="text-gray-300 text-center max-w-md">
{error ||
"خطای نامشخص در بارگذاری داده‌های داشبورد رخ داده است"}
</p>
<Button
onClick={fetchDashboardData}
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/10"
>
تلاش مجدد
</Button>
</div>
</CardContent>
</Card>
</div>
</DashboardLayout>
);
}
return (
<DashboardLayout>
<div className="grid grid-cols-3 gap-4">
{/* Top Cards Row - Redesigned to match other components */}
<div className="flex justify-between gap-6 [&>*]:w-full col-span-3">
{/* Ideas Card */}
<BaseCard title="ایده‌های فناوری و نوآوری">
<div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer
config={chartConfig}
className="aspect-square w-[6rem] h-auto"
>
<RadialBarChart
data={[
{
browser: "ideas",
visitors:
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1"
)) *
100
)
: 0,
fill: "var(--color-green)",
},
]}
startAngle={90}
endAngle={
90 +
((parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea || "1"
)) *
100
)
: 0) /
100) *
360
}
innerRadius={35}
outerRadius={55}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-pr-red last:fill-[#24273A]"
polarRadius={[38, 31]}
/>
<RadialBar dataKey="visitors" background cornerRadius={5} />
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<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-lg font-bold"
>
%
{formatNumber(
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"0"
) > 0
? Math.round(
(parseFloat(
dashboardData.topData
?.ongoing_innovation_technology_ideas ||
"0"
) /
parseFloat(
dashboardData.topData
?.registered_innovation_technology_idea ||
"1"
)) *
100
)
: 0
)}
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center gap-1 text-base">
<div className="font-light text-sm">ثبت شده :</div>
{formatNumber(
dashboardData.topData
?.registered_innovation_technology_idea || "0"
)}
</span>
<span className="flex items-center gap-1 font-bold text-base">
<div className="font-light text-sm">در حال اجرا :</div>
{formatNumber(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0"
)}
</span>
</div>
</div>
</div>
</BaseCard>
{/* Revenue Card */}
<MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
",",
""
) || "0"
}
percentValue={
dashboardData.topData
?.technology_innovation_based_revenue_growth_percent
}
percentLabel="درصد به کل درآمد"
/>
{/* Cost Reduction Card */}
<MetricCard
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
value={Math.round(
parseFloat(
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
""
) || "0"
)
)}
percentValue={
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent || "0"
}
percentLabel="درصد به کل هزینه"
/>
{/* Budget Ratio Card */}
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
<div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer
config={chartConfig}
className="aspect-square w-[6rem] h-auto"
>
<RadialBarChart
data={[
{
browser: "budget",
visitors: parseFloat(
dashboardData.topData
?.innovation_budget_achievement_percent || "0"
),
fill: "var(--color-green)",
},
]}
startAngle={90}
endAngle={
90 +
(dashboardData.topData
?.innovation_budget_achievement_percent /
100) *
360
}
innerRadius={35}
outerRadius={55}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-pr-red last:fill-[#24273A]"
polarRadius={[38, 31]}
/>
<RadialBar dataKey="visitors" background cornerRadius={5} />
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
>
<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-lg font-bold"
>
%
{formatNumber(
Math.round(
dashboardData.topData
?.innovation_budget_achievement_percent ||
0
)
)}
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center text-base gap-1 mr-auto">
<div className="font-light text-sm">مصوب :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
/,/g,
""
) || "0"
)
)
)}
</span>
<span className="flex items-center gap-1 text-base font-bold mr-auto">
<div className="font-light text-sm">جذب شده :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
/,/g,
""
) || "0"
)
)
)}
</span>
</div>
</div>
</div>
</BaseCard>
</div>
{/* Main Content with Tabs */}
<Tabs
defaultValue="canvas"
className="grid overflow-hidden rounded-lg grid-rows-[max-content] items-center col-span-2 row-start-2 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"
>
<div className="flex items-center border-b border-gray-600 justify-between gap-2">
<p className="p-6 font-persian font-semibold text-lg ">
تحقق ارزش ها
</p>
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
<TabsTrigger value="canvas" className="cursor-pointer">
شماتیک
</TabsTrigger>
<TabsTrigger
value="charts"
className=" text-white cursor-pointer font-light "
>
مقایسه ای
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="charts" className="h-full">
<InteractiveBarChart data={companyChartData} />
</TabsContent>
<TabsContent value="canvas" className="w-ful h-full">
<div className="p-4 h-full w-full">
<D3ImageInfo
//پتروشیمی بندر امام
// companies={companyChartData.map((item) => {
// const imageMap: Record<string, string> = {
// بسپاران: "/besparan.png",
// خوارزمی: "/khwarazmi.png",
// "فراورش 1": "/faravash1.png",
// "فراورش 2": "/faravash2.png",
// کیمیا: "/kimia.png",
// "آب نیرو": "/abniro.png",
// };
//پتروشیمی آپادانا
companies={companyChartData.map((item) => {
const imageMap: Record<string, string> = {
"واحد 100": "/abniro.png" ,
"واحد 200": "/besparan.png" ,
"واحد 300": "/khwarazmi.png" ,
"واحد 400": "/faravash1.png"
};
return {
id: item.category,
name: item.category,
imageUrl: imageMap[item.category] || "/placeholder.png",
cost: item?.costI || 0,
capacity: item?.capacityI || 0,
revenue: item?.revenueI || 0,
};
})}
/>
</div>
</TabsContent>
</Tabs>
{/* Left Section - Status Cards */}
<div className="space-y-4 row-start-2 col-span-1">
{/* Technology Intensity */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="p-4">
<div className="flex items-center justify-center gap-1 px-4">
<CardTitle className="text-white text-sm min-w-[100px]">
شدت فناوری
</CardTitle>
<Progress
value={parseFloat(
dashboardData.leftData?.technology_intensity
)}
className="h-4 flex-1"
/>
</div>
</CardContent>
</Card>
{/* Program Status */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardContent className="py-4 px-0">
<DashboardCustomBarChart
title="وضعیت برنامه‌های فناوری و نوآوری"
loading={loading}
data={[
{
label: "اجرا شده",
value: parseFloat(
dashboardData?.leftData?.executed_project || "0"
),
color: "bg-pr-green",
},
{
label: "در حال اجرا",
value: parseFloat(
dashboardData?.leftData?.in_progress_project || "0"
),
color: "bg-pr-blue",
},
{
label: "برنامه‌ریزی شده",
value: parseFloat(
dashboardData?.leftData?.planned_project || "0"
),
color: "bg-pr-red",
},
]}
/>
</CardContent>
</Card>
{/* Publications */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-sm">
انتشارات فناوری و نوآوری
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center">
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-blue-400" />
<span className="text-base">کتاب:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.printed_books_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-purple-400" />
<span className="text-sm">پتنت:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.registered_patents_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-yellow-400" />
<span className="text-sm">گزارش:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.published_reports_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-pr-green" />
<span className="text-sm">مقاله:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.printed_articles_count || "0"
)}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Promotion */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm">
<CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-sm">
ترویج فناوری و نوآوری
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<div className="grid grid-cols-2 grid-rows-2 gap-4 justify-center">
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-purple-400" />
<span className="text-sm">کنفرانس:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_conferences_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-blue-400" />
<span className="text-sm">شرکت در رویداد:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_events_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-yellow-400" />
<span className="text-sm">نمایشگاه:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.attended_exhibitions_count || "0"
)}
</span>
</div>
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
<Book className="w-4 h-4 text-pr-green" />
<span className="text-sm">برگزاری رویداد:</span>
</div>
<span className="text-base font-bold ">
{formatNumber(
dashboardData.leftData?.organized_events_count || "0"
)}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</DashboardLayout>
);
}
export default DashboardHome;