inogen/app/components/dashboard/dashboard-home.tsx

831 lines
34 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 React, { useState, useEffect } from "react";
import { DashboardLayout } from "./layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Progress } from "~/components/ui/progress";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
} from "recharts";
import apiService from "~/lib/api";
import toast from "react-hot-toast";
import {
Calendar,
TrendingUp,
TrendingDown,
Target,
Lightbulb,
DollarSign,
Minus,
CheckCircle,
BookOpen,
} from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { D3ImageInfo } from "./d3-image-info";
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts";
import { ChartContainer } from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils";
export function DashboardHome() {
const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
// First authenticate if needed
const token = localStorage.getItem("auth_token");
if (!token) {
await apiService.login("inogen_admin", "123456");
}
// Fetch top cards data
const topCardsResponse = await apiService.call({
main_page_first_function: {},
});
// Fetch left section data
const leftCardsResponse = await apiService.call({
main_page_second_function: {},
});
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);
} 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 ? Math.round((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-green-400/30 rounded-t-sm"
style={{ height: `${Math.random() * 80 + 20}%` }}
></div>
<div
className="w-full bg-red-400/30 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="p-3 pb-0 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="p-3 pb-0 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 */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-4 px-0">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
ایدههای فناوری و نوآوری
</h3>
</div>
<div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer
config={chartConfig}
className="w-full h-full max-h-20 max-w-40"
>
<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: "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-red-400 last:fill-background"
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">
<div className="font-light">ثبت شده :</div>
{formatNumber(
dashboardData.topData
?.registered_innovation_technology_idea || "0",
)}
</span>
<span className="flex items-center gap-1 font-bold">
<div className="font-light">در حال اجرا :</div>
{formatNumber(
dashboardData.topData
?.ongoing_innovation_technology_ideas || "0",
)}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Revenue Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-4 px-0">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
افزایش درآمد مبتنی بر فناوری و نوآوری
</h3>
</div>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
{formatNumber(
dashboardData.topData
?.technology_innovation_based_revenue_growth || "0",
)}
</p>
<div className="text-xs text-gray-400 font-persian">
میلیون ریال
</div>
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData
?.technology_innovation_based_revenue_growth_percent,
) || "0",
)}
%
</p>
<div className="text-xs text-gray-400 font-persian">
درصد به کل درآمد
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Cost Reduction Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-4 px-0">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
کاهش هزینه ها مبتنی بر فناوری و نوآوری
</h3>
</div>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
"",
) || "0",
) / 1000000,
),
)}
</p>
<div className="text-xs text-gray-400 font-persian">
میلیون ریال
</div>
</div>
<span className="text-6xl font-thin text-gray-600">/</span>
<div className="text-center">
<p className="text-5xl font-bold text-green-400">
{formatNumber(
Math.round(
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent,
) || "0",
)}
%
</p>
<div className="text-xs text-gray-400 font-persian">
درصد به کل هزینه
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Budget Ratio Card */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-4 px-0">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 pb-2">
<h3 className="text-lg font-bold text-white font-persian px-6">
نسبت تحقق بودجه فناوی و نوآوری
</h3>
</div>
<div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer
config={chartConfig}
className="w-full h-full max-h-20 max-w-40"
>
<RadialBarChart
data={[
{
browser: "budget",
visitors: parseFloat(
dashboardData.topData
?.innovation_budget_achievement_percent || "0",
),
fill: "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-red-400 last:fill-background"
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 gap-1">
<div className="font-light">مصوب :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
/,/g,
"",
) || "0",
) / 1000000000,
),
)}
</span>
<span className="flex items-center gap-1 font-bold">
<div className="font-light">جذب شده :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
/,/g,
"",
) || "0",
) / 1000000000,
),
)}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Main Content with Tabs */}
<Tabs
defaultValue="charts"
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 border m-6 border-gray-600">
<TabsTrigger value="canvas" className="">
شماتیک
</TabsTrigger>
<TabsTrigger value="charts" className=" text-white font-light ">
مقایسه ای
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="charts" className="w-ful h-full">
<InteractiveBarChart />
</TabsContent>
<TabsContent value="canvas" className="w-ful h-full">
<div className="p-4">
<D3ImageInfo
imageUrl="/main-circle.png"
title="نمای شماتیک"
description=":"
/>
</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-lg min-w-[120px]">
شدت فناوری
</CardTitle>
<p className="text-base text-left">
%
{formatNumber(
Math.round(
dashboardData.leftData?.technology_intensity || 0,
),
)}
</p>
<Progress
value={parseFloat(
dashboardData.leftData?.technology_intensity || "0",
)}
className="h-4 flex-1"
/>
</div>
</CardContent>
</Card>
{/* Program Status */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardContent className="py-6 px-0">
<DashboardCustomBarChart
title="وضعیت برنامه‌های فناوری و نوآوری"
loading={loading}
data={[
{
label: "اجرا شده",
value: parseFloat(
dashboardData?.leftData?.executed_project || "0",
),
color: "bg-green-400",
},
{
label: "در حال اجرا",
value: parseFloat(
dashboardData?.leftData?.in_progress_project || "0",
),
color: "bg-blue-400",
},
{
label: "برنامه‌ریزی شده",
value: parseFloat(
dashboardData?.leftData?.planned_project || "0",
),
color: "bg-red-400",
},
]}
/>
</CardContent>
</Card>
{/* Publications */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-lg">
انتشارات فناوری و نوآوری
</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">
<BookOpen className="w-4 h-4 text-blue-400" />
<span className="text-base">کتاب:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-purple-400" />
<span className="text-base">پتنت:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-yellow-400" />
<span className="text-base">گزارش:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-green-400" />
<span className="text-base">مقاله:</span>
</div>
<span className="text-xl 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 border-gray-700/50">
<CardHeader className="pb-2 border-b-2 border-gray-500/20">
<CardTitle className="text-white text-lg">
ترویج فناوری و نوآوری
</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">
<BookOpen className="w-4 h-4 text-purple-400" />
<span className="text-base">کنفرانس:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-blue-400" />
<span className="text-base">شرکت در رویداد:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-yellow-400" />
<span className="text-base">نمایشگاه:</span>
</div>
<span className="text-xl 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">
<BookOpen className="w-4 h-4 text-green-400" />
<span className="text-base">برگزاری رویداد:</span>
</div>
<span className="text-xl font-bold ">
{formatNumber(
dashboardData.leftData?.organized_events_count || "0",
)}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</DashboardLayout>
);
}
export default DashboardHome;