update the ideas page

This commit is contained in:
Saeed AB 2025-10-04 01:54:21 +03:30
parent e10c25fc3e
commit 67815aec2d
3 changed files with 546 additions and 105 deletions

View File

@ -1,4 +1,4 @@
import { ChevronDown, ChevronUp, RefreshCw, Eye, Star } from "lucide-react";
import { ChevronDown, ChevronUp, RefreshCw, Eye, Star, TrendingUp, Hexagon, Download } from "lucide-react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
@ -21,6 +21,18 @@ import {
import apiService from "~/lib/api";
import { formatCurrency, formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "~/components/ui/chart";
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, CartesianGrid, LabelList, Cell, RadialBarChart, PolarGrid, RadialBar, PolarRadiusAxis } from "recharts";
import { BaseCard } from "~/components/ui/base-card";
import { Label } from "~/components/ui/label";
import { MetricCard } from "~/components/ui/metric-card";
interface IdeaData {
idea_title: string;
@ -48,6 +60,18 @@ interface PersonRanking {
stars: number;
}
interface IdeaStatusData {
idea_status: string;
idea_status_count: number;
}
interface IdeaStatsData {
registered_innovation_technology_idea: string;
ongoing_innovation_technology_ideas: string;
increased_revenue_from_ideas: string;
increased_revenue_from_ideas_percent: string;
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
@ -88,6 +112,14 @@ export function ManageIdeasTechPage() {
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
const [loadingPeople, setLoadingPeople] = useState(false);
// Chart state
const [chartData, setChartData] = useState<IdeaStatusData[]>([]);
const [loadingChart, setLoadingChart] = useState(false);
// Stats state
const [statsData, setStatsData] = useState<IdeaStatsData | null>(null);
const [loadingStats, setLoadingStats] = useState(false);
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -204,6 +236,8 @@ export function ManageIdeasTechPage() {
fetchIdeas(true);
fetchTotalCount();
fetchPeopleRanking();
fetchChartData();
fetchStatsData();
}, [sortConfig]);
useEffect(() => {
@ -350,6 +384,68 @@ export function ManageIdeasTechPage() {
}
};
const fetchChartData = async () => {
try {
setLoadingChart(true);
const response = await apiService.select({
ProcessName: "idea",
OutputFields: ["idea_status", "count(idea_status)"],
GroupBy: ["idea_status"],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData: IdeaStatusData[] = JSON.parse(dataString);
if (Array.isArray(parsedData)) {
setChartData(parsedData?.reverse());
}
} catch (parseError) {
console.error("Error parsing chart data:", parseError);
}
}
} else {
toast.error(response.message || "خطا در دریافت اطلاعات نمودار");
}
} catch (error) {
console.error("Error fetching chart data:", error);
toast.error("خطا در دریافت اطلاعات نمودار");
} finally {
setLoadingChart(false);
}
};
const fetchStatsData = async () => {
try {
setLoadingStats(true);
const response = await apiService.call({
idea_page_function: {}
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData: IdeaStatsData = JSON.parse(dataString);
setStatsData(parsedData);
} catch (parseError) {
console.error("Error parsing stats data:", parseError);
}
}
} else {
toast.error(response.message || "خطا در دریافت آمار ایده‌ها");
}
} catch (error) {
console.error("Error fetching stats data:", error);
toast.error("خطا در دریافت آمار ایده‌ها");
} finally {
setLoadingStats(false);
}
};
const toPersianDigits = (input: string | number): string => {
const str = String(input);
const map: Record<string, string> = {
@ -395,7 +491,30 @@ export function ManageIdeasTechPage() {
}
};
// Chart configuration for shadcn/ui
const chartConfig: ChartConfig = {
count: {
label: "تعداد",
},
};
// Color palette for idea status
// Specific colors for idea statuses
const getChartStatusColor = (status: string) => {
switch (status) {
case "اجرا شده":
return "#69C8EA";
case "تایید شده":
return "#3AEA83";
case "در حال بررسی":
return "#EAD069";
case "رد شده":
return "#F76276";
default:
return "#6B7280";
}
};
const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"];
// Build a mapping of status value -> color based on loaded ideas
@ -469,7 +588,7 @@ export function ManageIdeasTechPage() {
variant="ghost"
size="sm"
onClick={() => handleShowDetails(item)}
className="underline text-pr-green underline-offset-4 text-sm hover:bg-emerald-500/20"
className="underline text-pr-green underline-offset-4 text-sm hover:bg-pr-green/20"
>
جزئیات بیشتر
</Button> );
@ -482,9 +601,104 @@ export function ManageIdeasTechPage() {
}
};
// Custom Vertical Bar Chart Component using shadcn/ui
const VerticalBarChart = () => {
if (loadingChart) {
return (
<div className="p-6">
<div className="bg-gray-600 rounded animate-pulse w-48 mx-auto"></div>
<div className="h-40 bg-gray-700 rounded animate-pulse"></div>
</div>
);
}
if (!chartData.length) {
return (
<div className="p-6 text-center">
<h3 className="text-lg font-persian font-semibold text-white mb-4">وضعیت ایده ها</h3>
<p className="text-gray-400 font-persian">هیچ دادهای یافت نشد</p>
</div>
);
}
// Prepare data for recharts
const rechartData = chartData.map((item) => ({
status: item.idea_status,
count: item.idea_status_count,
fill: getChartStatusColor(item.idea_status),
}));
return (
<ResponsiveContainer width="100%">
<ChartContainer config={chartConfig} className="w-full">
<BarChart
margin={{ top : 25 ,left: 12, right: 12 }}
barGap={15}
barSize={45}
accessibilityLayer
data={rechartData} >
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
dataKey="status"
axisLine={false}
tickLine={false}
tick={{
fill: '#fff',
fontSize: 14,
fontFamily: 'inherit'
}}
interval={0}
angle={0}
tickMargin={10}
textAnchor="middle"
/>
<YAxis
tickMargin={20}
axisLine={false}
tickLine={false}
tick={{
fill: '#9CA3AF',
fontSize: 12,
fontFamily: 'inherit'
}}
tickFormatter={(value) => toPersianDigits(value)}
label={{
value: "تعداد برنامه ها" ,
angle: -90,
position: "insideLeft",
fill: "#94a3b8",
fontSize: 11,
offset: 0,
dy: 0,
style: { textAnchor: "middle" },
}}
/>
<Bar
dataKey="count"
radius={[4, 4, 0, 0]}
>
<LabelList
dataKey="count"
position="top"
offset={12}
style={{
fill: "#ffffff",
fontSize: "16px",
fontWeight: "bold",
}}
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
/>
</Bar>
</BarChart>
</ChartContainer>
</ResponsiveContainer>
);
};
return (
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
<div className="space-y-6 h-full">
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
{/* People Ranking Table */}
<div className="lg:col-span-1">
@ -588,7 +802,7 @@ export function ManageIdeasTechPage() {
<div className="relative">
<Table
containerRef={scrollContainerRef}
containerClassName="overflow-auto custom-scrollbar max-h-[calc(50vh-100px)]"
containerClassName="overflow-auto custom-scrollbar max-h-[calc(50vh-180px)]"
>
<TableHeader className="sticky top-0 z-50 bg-pr-gray">
<TableRow className="bg-pr-gray">
@ -701,127 +915,354 @@ export function ManageIdeasTechPage() {
</div>
</Card>
</div>
{/* Chart Section */}
<BaseCard icon={TrendingUp} className="col-span-1 row-start-2 col-start-3 row-span-1" title="نمودار ایده‌ها">
<VerticalBarChart />
</BaseCard>
<div className="col-span-1 col-start-2 row-start-2 flex flex-col-reverse justify-end gap-2 row-span-1">
<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(
statsData?.registered_innovation_technology_idea || "0"
) > 0
? Math.round(
(parseFloat(
statsData?.registered_innovation_technology_idea || "0",
) /
parseFloat(
statsData
?.registered_innovation_technology_idea ||
"1",
)) *
100,
)
: 0,
fill: "var(--color-green)",
},
]}
startAngle={90}
endAngle={
90 +
((parseFloat(
statsData
?.registered_innovation_technology_idea || "0",
) > 0
? Math.round(
(parseFloat(
statsData
?.ongoing_innovation_technology_ideas || "0",
) /
parseFloat(
statsData
?.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(
statsData
?.registered_innovation_technology_idea ||
"0",
) > 0
? Math.round(
(parseFloat(
statsData
?.ongoing_innovation_technology_ideas ||
"0",
) /
parseFloat(
statsData
?.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(
statsData
?.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(
statsData
?.ongoing_innovation_technology_ideas || "0",
)}
</span>
</div>
</div>
</div>
</BaseCard>
<MetricCard
title="درآمد افزایش یافته"
value={statsData?.increased_revenue_from_ideas?.replaceAll("," , "") || "0"}
percentValue={statsData?.increased_revenue_from_ideas_percent}
percentLabel="درصد به کل درآمد"
/>
</div>
</div>
{/* Details Dialog */}
<Dialog open={isDetailsOpen} onOpenChange={setIsDetailsOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-right font-persian text-xl">
جزئیات ایده: {selectedIdea?.idea_title}
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="border-b border-gray-600/30 pb-4 mb-6">
<DialogTitle className="text-right font-persian text-xl text-white flex items-center justify-between">
<span>عنوان ایده: میکروکاتالیزورهای دما بالا</span>
</DialogTitle>
</DialogHeader>
{selectedIdea && (
<div className="space-y-6 text-right font-persian">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
نام و نام خانوادگی
</label>
<p className="text-foreground">{selectedIdea.full_name || "-"}</p>
{selectedIdea && <div className="flex w-full justify-center gap-4">
<div className="flex gap-4 flex-col text-right font-persian w-full border-l-2 border-l-pr-gray px-4 pb-4">
{/* مشخصات ایده پردازان Section */}
<div className="">
<h3 className="text-base font-bold text-white mb-2">
مشخصات ایده پردازان
</h3>
<div className="flex flex-col gap-4 mr-5">
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]"/>
<span className="text-white text-sm text-light">نام ایده پرداز:</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
شماره پرسنلی
</label>
<p className="text-foreground">{toPersianDigits(selectedIdea.personnel_number) || "-"}</p>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.full_name || "-"}</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
مدیریت
</label>
<p className="text-foreground">{selectedIdea.management || "-"}</p>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">شماره پرسنلی:</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
معاونت مربوطه
</label>
<p className="text-foreground">{selectedIdea.deputy || "-"}</p>
<span className="text-white font-normal text-sm mr-10">{toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
نوع نوآوری
</label>
<p className="text-foreground">{selectedIdea.innovation_type || "-"}</p>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">مدیریت:</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
میزان اصالت ایده
</label>
<p className="text-foreground">{selectedIdea.idea_originality || "-"}</p>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.management || "مدیریت توسعه"}</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">معاونت:</span>
</div>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.deputy || "توسعه"}</span>
</div>
<div className="grid grid-cols-3 items-center gap-2 col-span-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">اعضای تیم:</span>
</div>
<span className="text-white font-normal text-sm mr-10">
{selectedIdea.innovator_team_members || "رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
</span>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
محور ایده
</label>
<p className="text-foreground">{selectedIdea.idea_axis || "-"}</p>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
اعضای تیم نوآور
</label>
<p className="text-foreground">{selectedIdea.innovator_team_members || "-"}</p>
{/* مشخصات ایده Section */}
<div className="">
<h3 className="text-base font-bold text-white mb-2">
مشخصات ایده
</h3>
<div className="flex flex-col gap-4 mr-5">
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">تاریخ ثبت ایده:</span>
</div>
<span className="text-white font-normal text-sm mr-10">{formatDate(selectedIdea.idea_registration_date) || "-"}</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">نوع نوآوری:</span>
</div>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.innovation_type || "-"}</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">اصالت ایده:</span>
</div>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_originality || "-"}</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light min-w-max">محور ایده:</span>
</div>
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_axis || "-"}</span>
</div>
</div>
</div>
{/* نتایج و خروجی ها Section */}
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
<h3 className="text-base font-bold text-white mb-2">
نتایج و خروجی ها
</h3>
<div className="flex flex-col gap-4 mr-5">
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">درآمد حاصل:</span>
</div>
<span className="text-white text-sm font-normal mr-10">{formatNumber(selectedIdea.increased_revenue) || "-"}
<span className="text-[11px] mr-2 font-light">
میلیون ریال
</span>
</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">مقاله چاپ شده:</span>
</div>
<span className="text-white font-normal cursor-pointer text-sm flex items-center gap-2 mr-10">
<Download className="h-4 w-4" />
دانلود
</span>
</div>
<div className="grid grid-cols-3 items-center gap-2">
<div className="flex items-center gap-2">
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
<span className="text-white text-sm text-light">پتنت ثبت شده:</span>
</div>
<span className="text-white cursor-pointer font-normal text-sm flex items-center gap-2 mr-10">
<Download className="h-4 w-4"/>
دانلود
</span>
</div>
</div>
</div>
</div>
<div className="w-full flex flex-col gap-8">
{/* شرح ایده Section */}
<div>
<h3 className="text-base font-bold text-white mb-4">
شرح ایده
</label>
<p className="text-foreground leading-relaxed">
{selectedIdea.idea_description || "-"}
</h3>
<div className="">
<p className="text-white text-sm">
{selectedIdea.idea_description ||
"-"
}
</p>
</div>
</div>
{/* شرح وضعیت موجود ایده Section */}
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
توضیح وضعیت فعلی ایده
</label>
<p className="text-foreground leading-relaxed">
{selectedIdea.idea_current_status_description || "-"}
<h3 className="text-base font-bold text-white mb-4">
شرح وضعیت موجود ایده
</h3>
<div className="">
<p className="text-white leading-relaxed text-sm">
{selectedIdea.idea_current_status_description ||
"-"
}
</p>
</div>
</div>
{/* منافع حاصل از ایده Section */}
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
مزایای اجرای ایده
</label>
<p className="text-foreground leading-relaxed">
{selectedIdea.idea_execution_benefits || "-"}
<h3 className="text-base font-bold text-white mb-4">
منافع حاصل از ایده
</h3>
<div>
<p className="text-white leading-relaxed text-sm">
{selectedIdea.idea_execution_benefits ||
"-"
}
</p>
</div>
</div>
{/* بهبود های فرآیندی ایده Section */}
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
بهبودهای فرآیندی
</label>
<p className="text-foreground leading-relaxed">
{selectedIdea.process_improvements || "-"}
<h3 className="text-base font-bold text-white mb-4">
بهبود های فرآیندی ایده
</h3>
<div>
<p className="text-white leading-relaxed text-sm">
{selectedIdea.process_improvements ||
"-"
}
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-muted-foreground mb-1">
درآمد حاصل از ایده
</label>
<p className="text-success font-medium">
{formatCurrency(selectedIdea.increased_revenue)}
</p>
</div>
</div>
</div>
</div>
)}
</div>}
</DialogContent>
</Dialog>
</div>

View File

@ -616,22 +616,22 @@ export function ProductInnovationPage() {
size="sm"
onClick={() => {
handleProjectDetails(item)}}
className="text-emerald-400 underline underline-offset-4 font-ligth text-base hover:bg-emerald-500/20 p-2 h-auto"
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر
</Button>
);
case "project_no":
return (
<Badge variant="outline" className="font-mono text-base font-light">
<Badge variant="outline" className="font-mono text-sm font-light">
{String(value)}
</Badge>
);
case "title":
return <span className="font-light text-base text-white">{String(value)}</span>;
return <span className="font-light text-sm text-white">{String(value)}</span>;
case "project_status":
return (
<div className="flex items-center text-base font-light gap-1">
<div className="flex items-center text-sm font-light gap-1">
<Badge
variant={statusColor(value as projectStatus)}
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
@ -652,7 +652,7 @@ export function ProductInnovationPage() {
</Badge>
);
default:
return <span className="text-white text-base font-light">{String(value) || "-"}</span>;
return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
}
};