refactor_#2 (#13)

Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/13
Co-authored-by: saeed0920 <sd.eed1381@gmail.com>
Co-committed-by: saeed0920 <sd.eed1381@gmail.com>
This commit is contained in:
Saeed AB 2025-09-18 10:57:50 +03:30 committed by Saeed AB
parent efe32f8084
commit aed286660a
8 changed files with 545 additions and 294 deletions

View File

@ -37,6 +37,7 @@
--color-pr-green : #3AEA83; --color-pr-green : #3AEA83;
--color-pr-blue : #69C8EA; --color-pr-blue : #69C8EA;
--color-pr-red : #F76276; --color-pr-red : #F76276;
--color-pr-gray : #3F415A;
} }
html, html,
@ -246,23 +247,6 @@ html[dir="rtl"] body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* Scrollbar styling */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-100 dark:bg-neutral-800;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-300 dark:bg-neutral-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-400 dark:bg-neutral-500;
}
} }
/* Persian/Farsi font class */ /* Persian/Farsi font class */
@ -434,20 +418,15 @@ html[dir="rtl"] body {
} }
.custom-scrollbar:hover::-webkit-scrollbar-thumb { .custom-scrollbar:hover::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.8), rgba(16, 185, 129, 1));
} }
.dark .custom-scrollbar { .dark .custom-scrollbar {
scrollbar-color: rgba(16, 185, 129, 0.6) rgba(30, 41, 59, 0.6); /* thumb track */
} }
.dark .custom-scrollbar::-webkit-scrollbar-track { .dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(30, 41, 59, 0.6); /* slate-800 */
} }
.dark .custom-scrollbar::-webkit-scrollbar-thumb { .dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, rgba(16, 185, 129, 0.5), rgba(16, 185, 129, 0.9));
border-color: rgba(30, 41, 59, 0.6);
} }

View File

@ -16,7 +16,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card"; import { BaseCard } from "~/components/ui/base-card";
import { Checkbox } from "~/components/ui/checkbox"; import { Checkbox } from "~/components/ui/checkbox";
import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import { import {
@ -36,6 +36,7 @@ import {
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
import { Card , CardContent} from "~/components/ui/card";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData { interface ProcessInnovationData {
@ -49,6 +50,11 @@ interface ProcessInnovationData {
amount_currency_reduction: string; amount_currency_reduction: string;
Reduce_rate_failure: string; Reduce_rate_failure: string;
observer: string; observer: string;
// optional detailed fields returned by API
project_description?: string;
start_date?: string;
done_date?: string;
approved_budget?: string;
} }
interface ProjectStats { interface ProjectStats {
@ -152,7 +158,7 @@ export function ProcessInnovationPage() {
stats.productionStopsPreventionSum stats.productionStopsPreventionSum
), ),
description: "تن افزایش یافته", description: "تن افزایش یافته",
icon: <CirclePause />, icon: CirclePause,
color: "text-emerald-400", color: "text-emerald-400",
}, },
bottleneckremoval: { bottleneckremoval: {
@ -160,7 +166,7 @@ export function ProcessInnovationPage() {
title: "رفع گلوگاه", title: "رفع گلوگاه",
value: formatNumber(stats.bottleneckRemovalCount), value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه", description: "تعداد رفع گلوگاه",
icon: <Funnel />, icon: Funnel,
color: "text-emerald-400", color: "text-emerald-400",
}, },
currencyreduction: { currencyreduction: {
@ -170,7 +176,7 @@ export function ProcessInnovationPage() {
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
), ),
description: "دلار کاهش یافته", description: "دلار کاهش یافته",
icon: <DollarSign />, icon: DollarSign ,
color: "text-emerald-400", color: "text-emerald-400",
}, },
frequentfailuresreduction: { frequentfailuresreduction: {
@ -181,7 +187,7 @@ export function ProcessInnovationPage() {
stats.frequentFailuresReductionSum stats.frequentFailuresReductionSum
), ),
description: "مجموع درصد کاهش خرابی", description: "مجموع درصد کاهش خرابی",
icon: <Wrench />, icon: Wrench,
color: "text-emerald-400", color: "text-emerald-400",
}, },
}); });
@ -528,7 +534,7 @@ export function ProcessInnovationPage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleProjectDetails(item)} onClick={() => handleProjectDetails(item)}
className="text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/20 p-2 h-auto" className="text-pr-green hover:text-emerald-300 underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
> >
جزئیات بیشتر جزئیات بیشتر
</Button> </Button>
@ -541,18 +547,18 @@ export function ProcessInnovationPage() {
); );
case "project_no": case "project_no":
return ( return (
<Badge variant="outline" className="font-mono"> <Badge variant="outline" className="font-normal text-sm">
{String(value)} {String(value)}
</Badge> </Badge>
); );
case "title": case "title":
return <span className="font-medium text-white">{String(value)}</span>; return <span className="font-normal text-sm text-white">{String(value)}</span>;
case "project_status": case "project_status":
return ( return (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Badge <Badge
variant={statusColor(value)} variant={statusColor(value as projectStatus)}
className="font-medium border-2 p-0 block w-2 h-2 rounded-full" className="font-normal text-base border-2 p-0 block w-2 h-2 rounded-full"
style={{ style={{
border: "none", border: "none",
}} }}
@ -562,7 +568,7 @@ export function ProcessInnovationPage() {
); );
case "project_rating": case "project_rating":
return ( return (
<Badge variant="outline" className="text-lg text-center border-none"> <Badge variant="outline" className="text-base font-semibold text-center border-none">
{formatNumber(String(value))} {formatNumber(String(value))}
</Badge> </Badge>
); );
@ -590,106 +596,96 @@ export function ProcessInnovationPage() {
{loading || statsLoading {loading || statsLoading
? // Loading skeleton for stats cards - matching new design ? // Loading skeleton for stats cards - matching new design
Array.from({ length: 4 }).map((_, index) => ( Array.from({ length: 4 }).map((_, index) => (
<Card <BaseCard key={`skeleton-${index}`} className="rounded-2xl overflow-hidden">
key={`skeleton-${index}`} <div className="flex flex-col justify-between gap-2">
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden" <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
> <div
<CardContent className="p-2"> className="h-6 bg-gray-600 rounded animate-pulse"
<div className="flex flex-col justify-between gap-2"> style={{ width: "60%" }}
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> />
<div <div className="p-3 bg-emerald-500/20 rounded-full w-fit">
className="h-6 bg-gray-600 rounded animate-pulse" <div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
style={{ width: "60%" }}
/>
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
</div>
</div>
<div className="flex items-center justify-center flex-col p-1">
<div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div> </div>
</div> </div>
</CardContent> <div className="flex items-center justify-center flex-col p-1">
</Card> <div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div>
</BaseCard>
)) ))
: Object.entries(stateCard).map(([key, card]) => ( : Object.entries(stateCard).map(([key, card]) => {
<Card // map percent values for each card key
key={card.id} const percentMap: Record<string, number | string | undefined> = {
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50" productionstopsprevention: stats.percentProductionStops,
> bottleneckremoval: stats.percentBottleneckRemoval,
<CardContent className="p-2"> currencyreduction: stats.percentCurrencyReduction,
<div className="flex flex-col justify-between gap-2"> frequentfailuresreduction: stats.percentFailuresReduction,
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> };
<h3 className="text-lg font-bold text-white font-persian"> const percentValue = percentMap[key];
{card.title}
</h3> return (
<div <BaseCard
className={`p-3 gird placeitems-center rounded-full w-fit `} key={card.id}
> title={card.title}
{card.icon} className="border-gray-700/50"
icon={card.icon}
>
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl text-pr-green font-bold mb-1">
{(card.value)}
</p>
<div className="text-[11px] text-[#ACACAC] font-light font-persian">
{card.description}
</div>
</div> </div>
</div> </div>
<div className="flex items-center justify-center flex-col p-1">
<p
className={`text-3xl font-bold ${card.color} mb-1`}
>
{card.value}
</p>
<p className="text-sm text-gray-300 font-persian">
{card.description}
</p>
</div>
</div> </div>
</CardContent> </BaseCard>
</Card> );
))} })}
</div> </div>
</div> </div>
{/* Process Impacts Chart */} {/* Process Impacts Chart */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden"> <BaseCard className="rounded-2xl w-full overflow-hidden">
<CardContent> <CustomBarChart
<CustomBarChart title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
title="تاثیرات فرآیندی به صورت درصد مقایسه ای" loading={statsLoading}
loading={statsLoading} data={[
data={[ {
{ label: "کاهش توقفات تولید",
label: "کاهش توقفات تولید", value: Number(stats.percentProductionStops) || 0,
value: stats.percentProductionStops || 0, labelColor: "text-white",
color: "bg-emerald-400", },
labelColor: "text-white", {
}, label: "رفع گلوگاه تولید",
{ value: Number(stats.percentBottleneckRemoval) || 0,
label: "رفع گلوگاه تولید", labelColor: "text-white",
value: stats.percentBottleneckRemoval || 0, },
color: "bg-emerald-400", {
labelColor: "text-white", label: "کاهش ارز بری",
}, value: Number(stats.percentCurrencyReduction) || 0,
{ labelColor: "text-white",
label: "کاهش ارز بری", },
value: stats.percentCurrencyReduction || 0, {
color: "bg-emerald-400", label: "کاهش خرابی پر تکرار",
labelColor: "text-white", value: Number(stats.percentFailuresReduction) || 0,
}, labelColor: "text-white",
{ },
label: "کاهش خرابی پر تکرار", ]}
value: stats.percentFailuresReduction || 0, barHeight="h-6"
color: "bg-emerald-400", showAxisLabels={true}
labelColor: "text-white", />
}, </BaseCard>
]}
barHeight="h-5"
showAxisLabels={true}
/>
</CardContent>
</Card>
</div> </div>
{/* Data Table */} {/* Data Table */}
@ -810,7 +806,7 @@ export function ProcessInnovationPage() {
{/* Footer */} {/* Footer */}
<div className="p-2 px-4 bg-gray-700/50"> <div className="p-2 px-4 bg-[#3F415A]">
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row"> <div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full"> <div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
<div className="text-base text-gray-401 mb-1"> <div className="text-base text-gray-401 mb-1">
@ -841,15 +837,15 @@ export function ProcessInnovationPage() {
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-4xl max-h-[80vh] overflow-y-auto"> <DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right"> <DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian font-semibold text-sm text-right">
شرح پروژه شرح پروژه
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 flex justify-between text-right px-6"> <div className="space-y-4 flex justify-between text-right px-6">
{/* Project Description */} {/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600"> <div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2> <h2 className="font-bold text-base">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian px-2 mt-2"> <p className="text-white font-normal text-base font-persian px-2 mt-2">
{selectedProjectDetails?.project_description || "-"} {selectedProjectDetails?.project_description || "-"}
</p> </p>
</div> </div>
@ -859,11 +855,11 @@ export function ProcessInnovationPage() {
<div className="font-bold text-right ">جزئیات پروژه</div> <div className="font-bold text-right ">جزئیات پروژه</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1"> <h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<Building2 className="h-4 text-green-500" /> <Building2 className="h-4 text-green-500 text-sm font-light" />
زمان شروع: زمان شروع:
</h4> </h4>
<span className="text-white font-bold font-persian"> <span className="text-white font-normal text-base font-persian">
{selectedProjectDetails?.start_date {selectedProjectDetails?.start_date
? moment( ? moment(
selectedProjectDetails?.start_date, selectedProjectDetails?.start_date,
@ -874,11 +870,11 @@ export function ProcessInnovationPage() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1"> <h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<PickaxeIcon className="h-4 text-green-500" /> <PickaxeIcon className="h-4 text-green-500 text-sm font-light" />
زمان پایان: زمان پایان:
</h4> </h4>
<span className="text-white font-bold font-persian"> <span className="text-white font-normal text-base font-persian">
{selectedProjectDetails?.done_date {selectedProjectDetails?.done_date
? moment( ? moment(
selectedProjectDetails?.done_date, selectedProjectDetails?.done_date,
@ -889,27 +885,29 @@ export function ProcessInnovationPage() {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1"> <h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<UsersIcon className="h-4 text-green-500" /> <UsersIcon className="h-4 text-green-500 text-sm font-light" />
هزینه برآورد شده: هزینه برآورد شده:
</h4> </h4>
<span className="text-white font-bold font-persian"> <span className="text-white font-normal text-base font-persian">
{formatNumber( {selectedProjectDetails?.approved_budget
Number( ? formatNumber(
selectedProjectDetails?.approved_budget.replaceAll( Number(
",", selectedProjectDetails.approved_budget.replaceAll(
"" ",",
""
)
)
) )
) : "-"}
) || "-"}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1"> <h4 className="font-light text-sm text-white font-persian mb-2 flex items-center gap-1">
<UserIcon className="h-4 text-green-500" /> <UserIcon className="h-4 text-green-500 text-sm font-light" />
نفر مرتبط: نفر مرتبط:
</h4> </h4>
<span className="text-white font-bold font-persian"> <span className="text-white font-normal text-base font-persian">
{selectedProjectDetails?.observer || "-"} {selectedProjectDetails?.observer || "-"}
</span> </span>
</div> </div>

View File

@ -1,5 +1,5 @@
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState, useMemo } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Card, CardContent } from "~/components/ui/card"; import { Card, CardContent } from "~/components/ui/card";
@ -7,6 +7,7 @@ import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableFooter,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
@ -53,51 +54,51 @@ type ColumnDef = {
}; };
const columns: ColumnDef[] = [ const columns: ColumnDef[] = [
{ key: "title", label: "عنوان پروژه", sortable: true, width: "200px" }, { key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
{ {
key: "importance_project", key: "importance_project",
label: "میزان اهمیت", label: "میزان اهمیت",
sortable: true, sortable: true,
width: "150px", width: "160px",
}, },
{ {
key: "strategic_theme", key: "strategic_theme",
label: "مضمون راهبردی", label: "مضمون راهبردی",
sortable: true, sortable: true,
width: "160px", width: "200px",
}, },
{ {
key: "value_technology_and_innovation", key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری", label: "ارزش فناوری و نوآوری",
sortable: true, sortable: true,
width: "200px", width: "220px",
}, },
{ {
key: "type_of_innovation", key: "type_of_innovation",
label: "انواع نوآوری", label: "انواع نوآوری",
sortable: true, sortable: true,
width: "140px", width: "160px",
}, },
{ key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" }, { key: "innovation", label: "میزان نوآوری", sortable: true, width: "140px" },
{ {
key: "person_executing", key: "person_executing",
label: "مسئول اجرا", label: "مسئول اجرا",
sortable: true, sortable: true,
width: "140px", width: "180px",
}, },
{ {
key: "excellent_observer", key: "excellent_observer",
label: "ناطر عالی", label: "ناطر عالی",
sortable: true, sortable: true,
width: "140px", width: "180px",
}, },
{ key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" }, { key: "observer", label: "ناظر پروژه", sortable: true, width: "180px" },
{ key: "moderator", label: "مجری", sortable: true, width: "140px" }, { key: "moderator", label: "مجری", sortable: true, width: "180px" },
{ {
key: "executive_phase", key: "executive_phase",
label: "فاز اجرایی", label: "فاز اجرایی",
sortable: true, sortable: true,
width: "140px", width: "160px",
}, },
{ key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" }, { key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" },
{ {
@ -495,6 +496,149 @@ export function ProjectManagementPage() {
} }
}; };
// Categories for which we'll generate/display color legends
const categoryDefs = [
{
key: "strategic_theme",
label: "مضمون راهبردی",
palette: ["#6D53FB", "#7C3AED", "#5B21B6", "#4C1D95", "#A78BFA"],
},
{
key: "value_technology_and_innovation",
label: "ارزش فناوری و نوآوری",
palette: ["#A757FF", "#C084FC", "#8B5CF6", "#7C3AED", "#D8B4FE"],
},
{
key: "type_of_innovation",
label: "انواع نوآوری",
palette: ["#E884CE", "#FB7185", "#F472B6", "#F97316", "#FBCFE8"],
},
{
key: "innovation",
label: "میزان نوآوری",
palette: ["#C3BF8B", "#10B981", "#F59E0B", "#EF4444", "#FDE68A"],
},
{
key: "executive_phase",
label: "فاز اجرایی",
palette: ["#C3BF8B", "#10B981", "#F59E0B", "#EF4444", "#FDE68A"],
},
];
// Build a mapping of value -> color for each category based on loaded projects.
// We assign colors deterministically from the category palette in order of appearance.
const categoryColorMaps = useMemo(() => {
const maps: Record<string, Record<string, string>> = {};
categoryDefs.forEach((cat) => {
maps[cat.key] = {};
const seen = new Map<string, string>();
const values: string[] = projects
.map((p) => (p as any)[cat.key])
.filter((v) => v !== undefined && v !== null && String(v).trim() !== "")
.map((v) => String(v));
// preserve order of first appearance
values.forEach((val, idx) => {
if (!seen.has(val)) {
const color = cat.palette[seen.size % cat.palette.length];
seen.set(val, color);
}
});
seen.forEach((color, val) => {
maps[cat.key][val] = color;
});
});
return maps;
}, [projects]);
// Compute counts and totals for each category so footer segments can be proportional
const categoryStats = useMemo(() => {
const stats: Record<string, { counts: Record<string, number>; total: number }> = {};
categoryDefs.forEach((cat) => {
const counts: Record<string, number> = {};
let total = 0;
projects.forEach((p) => {
const val = String((p as any)[cat.key] ?? "").trim();
if (val !== "") {
counts[val] = (counts[val] || 0) + 1;
total += 1;
}
});
stats[cat.key] = { counts, total };
});
// also compute executive_phase counts
const execCounts: Record<string, number> = {};
let execTotal = 0;
projects.forEach((p) => {
const val = String((p as any)["executive_phase"] ?? "").trim();
if (val !== "") {
execCounts[val] = (execCounts[val] || 0) + 1;
execTotal += 1;
}
});
stats["executive_phase"] = { counts: execCounts, total: execTotal };
return stats;
}, [projects]);
// Importance counts (بالا، متوسط، پایین) for footer bar
const importanceCounts = useMemo(() => {
const counts: Record<string, number> = {};
let total = 0;
projects.forEach((p) => {
const val = String((p as any).importance_project ?? "").trim();
if (val !== "") {
counts[val] = (counts[val] || 0) + 1;
total += 1;
}
});
return { counts, total };
}, [projects]);
// Numeric averages for specified columns
const numericAverages = useMemo(() => {
const keys = [
"remaining_time",
"renewed_duration",
"deviation_from_program",
"approved_budget",
"budget_spent",
"cost_deviation",
];
const res: Record<string, number | null> = {};
// remaining_time is computed from end_date
const remainingValues: number[] = projects
.map((p) => calculateRemainingDays((p as any).end_date))
.filter((v) => v !== null) as number[];
res["remaining_time"] = remainingValues.length
? Math.round(remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length)
: null;
// For other keys, parse numeric values
keys.forEach((k) => {
if (k === "remaining_time") return;
const vals: number[] = projects
.map((p) => {
const raw = (p as any)[k];
if (raw == null) return NaN;
const num = Number(String(raw).toString().replace(/[^0-9.-]/g, ""));
return Number.isFinite(num) ? num : NaN;
})
.filter((n) => !Number.isNaN(n));
res[k] = vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : null;
});
return res;
}, [projects]);
const getCategoryColor = (categoryKey: string, value: unknown) => {
const val = value == null ? "" : String(value);
const map = categoryColorMaps[categoryKey] || {};
return map[val] ?? "#6B7280"; // fallback gray
};
const renderCellContent = (item: ProjectData, column: ColumnDef) => { const renderCellContent = (item: ProjectData, column: ColumnDef) => {
const apiField = column.apiField ?? column.key; const apiField = column.apiField ?? column.key;
const value = (item as any)[apiField]; const value = (item as any)[apiField];
@ -509,7 +653,7 @@ export function ProjectManagementPage() {
return ( return (
<span <span
dir="ltr" dir="ltr"
className="font-medium flex justify-end gap-1 items-center" className="flex justify-end gap-1 items-center"
style={{ color }} style={{ color }}
> >
<span>روز</span> {toPersianDigits(days)} <span>روز</span> {toPersianDigits(days)}
@ -520,55 +664,58 @@ export function ProjectManagementPage() {
case "value_technology_and_innovation": case "value_technology_and_innovation":
case "type_of_innovation": case "type_of_innovation":
case "innovation": case "innovation":
case "executive_phase": {
const color = getCategoryColor(column.key, value);
return ( return (
<span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full"> <span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
<span className="text-gray-300">{String(value) || "-"}</span> <span className="text-gray-300">{!!value ? String(value) : "-"}</span>
<span <span
style={{ style={{
backgroundColor: `${column.key === "strategic_theme" ? "#6D53FB" : column.key === "value_technology_and_innovation" ? "#A757FF" : column.key === "type_of_innovation" ? "#E884CE" : "#C3BF8B"}`, backgroundColor: color,
display : !value ? "none" : "block",
}} }}
className="inline-block w-2 h-2 rounded-full bg-emerald-400" className="inline-block w-2 h-2 rounded-full"
/> />
</span> </span>
); );
}
case "approved_budget": case "approved_budget":
case "budget_spent": case "budget_spent":
return ( return (
<span className="font-medium text-emerald-400"> <span className=" text-emerald-400 font-normal">
{formatCurrency(String(value))} {formatCurrency(String(value))}
</span> </span>
); );
case "deviation_from_program": case "deviation_from_program":
case "cost_deviation": case "cost_deviation":
return ( return (
<span className="text-gray-300">{formatNumber(value as any)}</span> <span className="text-sm font-normal">{formatNumber(value as any)}</span>
); );
case "start_date": case "start_date":
case "end_date": case "end_date":
case "done_date": case "done_date":
return ( return (
<span className="text-gray-300">{formatDate(String(value))}</span> <span className=" text-sm font-normal">{formatDate(String(value))}</span>
); );
case "project_no": case "project_no":
return ( return (
<Badge <Badge
variant="outline" variant="teal"
className="font-mono text-emerald-400 border-emerald-500/50" className="border-emerald-500/50"
> >
{String(value)} {String(value)}
</Badge> </Badge>
); );
case "title": case "title":
return <span className="font-medium text-white">{String(value)}</span>; return <span className="text-sm font-normal text-white">{String(value)}</span>;
case "importance_project": case "importance_project":
return ( return (
<Badge <Badge
variant="outline" variant="outline"
className="font-medium border-2" className="border-2 text-sm rounded-lg"
style={{ style={{
color: getImportanceColor(String(value)), color: getImportanceColor(String(value)),
borderColor: getImportanceColor(String(value)), borderColor: getImportanceColor(String(value)),
backgroundColor: `${getImportanceColor(String(value))}20`,
}} }}
> >
{String(value)} {String(value)}
@ -576,7 +723,7 @@ export function ProjectManagementPage() {
); );
default: default:
return ( return (
<span className="text-gray-300"> <span className="font-light text-sm">
{(value && String(value)) || "-"} {(value && String(value)) || "-"}
</span> </span>
); );
@ -592,14 +739,15 @@ export function ProjectManagementPage() {
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden"> <Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0"> <CardContent className="p-0">
<div className="relative"> <div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]"> <div className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]">
<Table className="table-fixed">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]"> <TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]"> <TableRow className="bg-[#3F415A]">
{columns.map((column) => ( {columns.map((column) => (
<TableHead <TableHead
key={column.key} key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20" className={` text-right font-persian whitespace-nowrap text-white font-semibold bg-[#3F415A] sticky top-0 z-20`}
style={{ width: column.width }} style={{ width: column.width}}
> >
{column.sortable ? ( {column.sortable ? (
<button <button
@ -619,11 +767,13 @@ export function ProjectManagementPage() {
</button> </button>
) : ( ) : (
column.label column.label
)} )
}
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading ? ( {loading ? (
// Skeleton loading rows (compact) // Skeleton loading rows (compact)
@ -635,7 +785,7 @@ export function ProjectManagementPage() {
{columns.map((column) => ( {columns.map((column) => (
<TableCell <TableCell
key={column.key} key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2" className="text-right border-emerald-500/20 py-1 px-2 break-words"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" /> <div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
@ -668,7 +818,7 @@ export function ProjectManagementPage() {
{columns.map((column) => ( {columns.map((column) => (
<TableCell <TableCell
key={column.key} key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2" className="text-right border-emerald-500/20 text-sm py-1 px-2 break-words"
> >
{renderCellContent(project, column)} {renderCellContent(project, column)}
</TableCell> </TableCell>
@ -677,7 +827,138 @@ export function ProjectManagementPage() {
)) ))
)} )}
</TableBody> </TableBody>
</Table>
<TableFooter className="sticky py-2 bottom-[-1px] bg-[#3F415A]">
<TableRow>
{columns.map((column, colIndex) => {
// First column: show total projects text similar to API count
if (colIndex === 0) {
return (
<TableCell key={column.key} className="p-3 text-sm text-white font-semibold font-persian">
کل پروژهها: {formatNumber(actualTotalCount)}
</TableCell>
);
}
// importance_project: render importance bar with specified colors
if (column.key === "importance_project") {
const imp = importanceCounts;
const order = ["بالا", "متوسط", "پایین"];
const colorFor = (k: string) => {
switch (k) {
case "بالا":
return "var(--color-pr-green)"; // green
case "متوسط":
return "#69C8EA"; // blue-ish
case "پایین":
return "#F76276"; // red
default:
return "#6B7280";
}
};
return (
<TableCell key={column.key} className="p-1">
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{order.map((k) => {
const cnt = imp.counts[k] || 0;
const widthPercent = imp.total > 0 ? (cnt / imp.total) * 100 : 0;
return (
<div
key={k}
title={`${k} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium"
style={{ width: `${widthPercent}%`, backgroundColor: colorFor(k) }}
>
</div>
);
})}
</div>
</TableCell>
);
}
// For category-like columns: strategic_theme, value_technology_and_innovation, innovation, executive_phase
const categoryLike = [
"strategic_theme",
"value_technology_and_innovation",
"innovation",
"executive_phase",
];
if (categoryLike.includes(column.key)) {
const stat = categoryStats[column.key] || { counts: {}, total: 0 };
const entries = Object.entries(stat.counts);
return (
<TableCell key={column.key} className="p-1">
<div className="w-full bg-gray-800 rounded-sm overflow-hidden h-3 flex">
{entries.length > 0 ? (
entries.map(([val, cnt]) => {
let color = categoryColorMaps[column.key]?.[val] || "#6B7280";
if (column.key === "executive_phase") {
color = (phaseColors as any)[val] || color;
}
const widthPercent = stat.total > 0 ? (cnt / stat.total) * 100 : 0;
return (
<div
key={val}
title={`${val} (${cnt})`}
className="h-3 flex items-center justify-center text-xs font-medium"
style={{ width: `${widthPercent}%`, backgroundColor: color }}
>
</div>
);
})
) : (
<div className="h-3 w-full bg-gray-700" />
)}
</div>
</TableCell>
);
}
// remove bar for type_of_innovation (show empty cell)
if (column.key === "type_of_innovation") {
return <TableCell key={column.key} className="p-1" />;
}
// remaining_time: show average days with color (green/red/white)
if (column.key === "remaining_time") {
const avg = numericAverages["remaining_time"] as number | null;
const color = avg == null ? "#9CA3AF" : avg > 0 ? "#3AEA83" : avg < 0 ? "#F76276" : "#FFFFFF";
return (
<TableCell key={column.key} className="p-2 text-right font-medium" style={{ color }}>
{avg == null ? "-" : `${formatNumber(avg)} روز`}
</TableCell>
);
}
// For numeric columns: show average rounded
const numericKeyMap: Record<string, string> = {
renewed_duration: "renewed_duration",
deviation_from_program: "deviation_from_program",
approved_budget: "approved_budget",
budget_spent: "budget_spent",
cost_deviation: "cost_deviation",
};
const mapped = (numericKeyMap as any)[column.key];
if (mapped) {
const avg = numericAverages[mapped] as number | null;
let display = "-";
if (avg != null) {
display = mapped.includes("budget") ? formatCurrency(String(Math.round(avg))) : formatNumber(Math.round(avg));
}
return (
<TableCell key={column.key} className="p-2 text-right font-medium text-gray-200">
{display}
</TableCell>
);
}
// Default: empty cell to keep alignment
return <TableCell key={column.key} className="p-1" />;
})}
</TableRow>
</TableFooter>
</Table>
</div>
</div> </div>
{/* Infinite scroll trigger */} {/* Infinite scroll trigger */}
@ -685,7 +966,7 @@ export function ProjectManagementPage() {
{loadingMore && ( {loadingMore && (
<div className="flex items-center justify-center py-1"> <div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" /> <RefreshCw className="w-4 h-3 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300 text-xs"></span> <span className="font-persian text-gray-300 text-xs"></span>
</div> </div>
</div> </div>
@ -693,12 +974,7 @@ export function ProjectManagementPage() {
</div> </div>
</CardContent> </CardContent>
{/* Footer */}
<div className="p-4 bg-gray-700/50">
<div className="flex items-center justify-between text-sm text-gray-300 font-persian">
<span>کل پروژهها: {formatNumber(actualTotalCount)}</span>
</div>
</div>
</Card> </Card>
</div> </div>
</DashboardLayout> </DashboardLayout>

View File

@ -401,21 +401,13 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<div className="space-y-4"> <div className="space-y-4">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
<CardHeader className="text-center pt-4 pb-3 border-b-2 border-[#3F415A]"> <CardHeader className="text-center pt-4 pb-3 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-xl text-white"> <CardTitle className="font-persian text-base font-semibold text-white">
وضعیت زیستبوم فناوری و نوآوری وضعیت زیستبوم فناوری و نوآوری
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
{/* Footer - MOU Count */}
{/* <CardContent className="py-3">
<div className="flex font-bold text-xl px-6 justify-between text-gray-300 font-persian mb-1">
تعداد تفاهم نامه ها
<span className="text-2xl">{formatNumber(counts.mou_count)}</span>
</div>
</CardContent> */}
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]"> <CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-xl text-white flex justify-between px-4"> <CardTitle className="font-persian text-sm text-white flex justify-between px-4">
تعداد تفاهم نامه ها تعداد تفاهم نامه ها
<span className="font-bold text-3xl"> <span className="font-bold text-3xl">
{formatNumber(counts.mou_count)} {formatNumber(counts.mou_count)}
@ -424,7 +416,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
</CardHeader> </CardHeader>
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]"> <CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-xl text-white flex justify-between px-4"> <CardTitle className="font-persian text-sm text-white flex justify-between px-4">
تعداد بازیگران تعداد بازیگران
<span className="font-bold text-3xl"> <span className="font-bold text-3xl">
{formatNumber(counts.actor_count)} {formatNumber(counts.actor_count)}
@ -433,7 +425,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
</CardHeader> </CardHeader>
{/* Actor Count Display */} {/* Actor Count Display */}
<CardHeader className="text-right text-xl py-2 pb-4 font-bold w-full"> <CardHeader className="text-right pt-4 mt-2 pb-2 text-sm font-semibold w-full">
تنوع بازیگران تنوع بازیگران
</CardHeader> </CardHeader>
{/* Middle - Bar Chart */} {/* Middle - Bar Chart */}
@ -454,58 +446,78 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
</CardContent> </CardContent>
{/* Area Chart Section */} {/* Area Chart Section */}
<CardContent className="px-2 pb-4 border-b-2 border-[#3F415A] py-4"> <CardContent className="p-2">
<div className="mb-4"> <div className="px-4">
<CardTitle className="font-persian text-lg text-white mb-2"> <CardTitle className="font-persian text-sm font-semibold text-white mb-2">
روند ایجاد بازیگران در طول سالها روند ایجاد بازیگران در طول سالها
</CardTitle> </CardTitle>
</div> </div>
<div className="h-48"> <div className="h-42">
{processData.length > 0 ? ( {processData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart <AreaChart
data={processData} accessibilityLayer
margin={{ top: 10, right: 30, left: 0, bottom: 0 }} data={processData}
> margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
<CartesianGrid >
strokeDasharray="3 3" <defs>
stroke="rgba(255,255,255,0.1)" <linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
/> <stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
<XAxis <stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
dataKey="year" </linearGradient>
stroke="#9ca3af" </defs>
fontSize={12}
tickFormatter={formatPersianYear} <CartesianGrid
/> vertical={false}
<YAxis stroke="rgba(255,255,255,0.1)"
stroke="#9ca3af" />
fontSize={12} <XAxis
tickFormatter={(value) => formatNumber(value)} dataKey="year"
/> stroke="#9ca3af"
<Tooltip fontSize={12}
contentStyle={{ tickLine={false}
backgroundColor: "#374151", tickMargin={8}
border: "1px solid #6b7280", axisLine={false}
borderRadius: "6px", tickFormatter={formatPersianYear}
color: "#f3f4f6", />
}} <YAxis
labelFormatter={(value) => stroke="#9ca3af"
`سال ${formatPersianYear(value.toString())}` fontSize={12}
} tickMargin={12}
formatter={(value) => [ tickLine={false}
formatNumber(value), axisLine={false}
"تعداد بازیگران", tickFormatter={(value) => formatNumber(value)}
]} />
/> <Tooltip cursor={false} content={<></>} />
<Area
type="monotone" {/* ✅ Use gradient for fill */}
dataKey="value" <Area
stroke="#34d399" type="monotone"
fill="rgba(52, 211, 153, 0.25)" dataKey="value"
strokeWidth={2} stroke="#3AEA83"
/> fill="url(#fillDesktop)"
</AreaChart> strokeWidth={2}
</ResponsiveContainer> activeDot={({ cx, cy, payload }) => (
<g>
{/* Small circle */}
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
{/* Year label above point */}
<text
x={cx}
y={cy - 10}
textAnchor="middle"
fontSize={12}
fontWeight="bold"
fill="#3AEA83"
>
{formatPersianYear(payload.year)}
</text>
</g>
)}
/>
</AreaChart>
</ResponsiveContainer>
) : ( ) : (
<div className="flex items-center justify-center h-full text-gray-400 font-persian"> <div className="flex items-center justify-center h-full text-gray-400 font-persian">
دادهای برای نمایش وجود ندارد دادهای برای نمایش وجود ندارد

View File

@ -508,7 +508,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
// Don't render on server side // Don't render on server side
if (!isMounted) { if (!isMounted) {
return ( return (
<div className="w-full h-full flex items-center justify-center bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"> <div className="w-full h-full flex items-center justify-center bg-transparent">
<div className="text-white font-persian text-sm"> <div className="text-white font-persian text-sm">
در حال بارگذاری... در حال بارگذاری...
</div> </div>
@ -518,7 +518,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="w-full h-full relative bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]"> <div className="w-full h-full relative bg-transparent">
{/* Skeleton Graph Container */} {/* Skeleton Graph Container */}
<div className="w-full h-full flex items-center justify-center relative"> <div className="w-full h-full flex items-center justify-center relative">
{/* Center Node Skeleton */} {/* Center Node Skeleton */}
@ -579,7 +579,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
} }
return ( return (
<div className="w-full h-full relative bg-[linear-gradient(to_bottom_left,#464861,10%,#111628)] overflow-hidden"> <div className="w-full h-full relative bg-transparent overflow-hidden">
<svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} /> <svg ref={svgRef} className="w-full h-full" style={{ minHeight: 500 }} />
</div> </div>
); );

View File

@ -7,6 +7,7 @@ interface BaseCardProps {
headerClassName?: string; headerClassName?: string;
contentClassName?: string; contentClassName?: string;
children: React.ReactNode; children: React.ReactNode;
icon ?: React.ComponentType<{ className?: string }>;
withHeader?: boolean; withHeader?: boolean;
} }
@ -17,6 +18,7 @@ export function BaseCard({
contentClassName, contentClassName,
children, children,
withHeader = false, withHeader = false,
icon : Icon,
}: BaseCardProps) { }: BaseCardProps) {
return ( return (
<Card <Card
@ -25,7 +27,12 @@ export function BaseCard({
className className
)} )}
> >
{withHeader && title ? ( {Icon && title ? (
<CardHeader className={cn("border-b-2 border-gray-500/20 py-2 px-0 pb-4", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4 my-auto items-center flex w-full justify-between">{title} {<Icon />} </CardTitle>
</CardHeader>
) :
withHeader && title ? (
<CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}> <CardHeader className={cn("pb-2 border-b-2 border-gray-500/20", headerClassName)}>
<CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle> <CardTitle className="text-white text-sm text-right font-persian px-4">{title}</CardTitle>
</CardHeader> </CardHeader>

View File

@ -67,76 +67,56 @@ export function CustomBarChart({
return ( return (
<div className={`space-y-6 ${className}`} style={{ height }}> <div className={`space-y-6 ${className}`} style={{ height }}>
<div className="border-b"> {title && <div className="border-b-[#3F415A] border-b-2">
{title && (
<h3 className="text-xl font-bold text-white font-persian text-right p-4"> <h3 className="text-sm font-semibold text-white font-persian text-right p-4">
{title} {title}
</h3> </h3>
)} </div>}
</div>
<div className="space-y-4 px-4 pb-4"> <div className="space-y-4 px-4 pb-4">
{data.map((item, index) => { {data.map((item, index) => {
const percentage = const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0; globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const displayValue: any = item.value; const displayValue: any = item.value;
return ( return (
<div key={index} className="flex items-center gap-3"> <div key={index} className="flex items-center gap-3">
{/* Label */}
<span <span
className={`font-persian text-sm min-w-[160px] text-right ${ className={`font-persian text-sm font-normal min-w-[120px] text-right ${
item.labelColor || "text-white" item.labelColor || "text-white"
}`} }`}
> >
{item.label} {item.label}
</span> </span>
{/* Bar Container */}
<div <div
className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`} className={`${showAxisLabels && "bg-pr-gray"} flex-1 flex items-center gap-1 justify-start rounded-full overflow-hidden ${barHeight}`}
> >
<div <div
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${ className={`${barHeight} rounded-full transition-all duration-700 ease-out ${
item.color || "bg-emerald-400" item.color || "bg-pr-green"
}`} }`}
style={{ style={{
width: `${Math.min(percentage, 100)}%`, width: `${Math.min(percentage, 100)}%`,
}} }}
> >
{/* Add a subtle gradient effect for better visual appeal */} <div className="inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
</div> </div>
</div>
{/* Value Label */}
<span <span
className={`font-bold text-sm min-w-[60px] text-left ${ className={`text-base font-normal text-left text-white`}
item.color?.includes("emerald")
? "text-emerald-400"
: item.color?.includes("blue")
? "text-blue-400"
: item.color?.includes("purple")
? "text-purple-400"
: item.color?.includes("red")
? "text-red-400"
: item.color?.includes("yellow")
? "text-yellow-400"
: "text-emerald-400"
}`}
> >
{item.valuePrefix || ""} {item.valuePrefix || ""}
{formatNumber(parseFloat(displayValue))}% {formatNumber(parseFloat(displayValue))}
{item.valueSuffix || ""} {item.valueSuffix || ""}
</span> </span>
</div>
</div> </div>
); );
})} })}
{/* Axis Labels */} {/* Axis Labels */}
{showAxisLabels && globalMaxValue > 0 && ( {showAxisLabels && globalMaxValue > 0 && (
<div className="flex items-center gap-3 mt-6"> <div className="flex w-full items-center gap-3 mt-6">
<span className="min-w-[160px]"></span> <span className="min-w-[120px]"></span>
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700"> <div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
<span className="text-gray-400 text-xs">{formatNumber(0)}</span> <span className="text-gray-400 text-xs">{formatNumber(0)}</span>
<span className="text-gray-400 text-xs"> <span className="text-gray-400 text-xs">
@ -152,7 +132,7 @@ export function CustomBarChart({
{formatNumber(Math.round(globalMaxValue))} {formatNumber(Math.round(globalMaxValue))}
</span> </span>
</div> </div>
<span className="min-w-[60px]"></span> <span className="min-w-[0px]"></span>
</div> </div>
)} )}
</div> </div>

View File

@ -76,8 +76,8 @@ export default function EcosystemPage() {
</div> </div>
<div className="lg:col-span-8 h-full"> <div className="lg:col-span-8 h-full">
<Card className="h-full overflow-hidden"> <Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
<CardContent className="p-0 h-full"> <CardContent className="p-0 h-full bg-transparent">
<NetworkGraph onNodeClick={setSelectedCompany} /> <NetworkGraph onNodeClick={setSelectedCompany} />
</CardContent> </CardContent>
</Card> </Card>
@ -92,11 +92,10 @@ export default function EcosystemPage() {
> >
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]"> <DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-right border-b-2 border-gray-600 py-2 mr-4 text-xl"> <DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
معرفی معرفی
<span> {selectedCompany?.category}</span> <span> {selectedCompany?.category}</span>
</DialogTitle> </DialogTitle>
<DialogDescription className="text-center text-green-400"></DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -109,7 +108,7 @@ export default function EcosystemPage() {
<img <img
src={getImageUrl(selectedCompany.stageid)} src={getImageUrl(selectedCompany.stageid)}
alt={selectedCompany?.label || ""} alt={selectedCompany?.label || ""}
className="w-14 h-14 object-cover rounded-2xl" className="w-12 h-12 object-cover rounded-2xl"
onError={(e) => { onError={(e) => {
// Hide image and show fallback on error // Hide image and show fallback on error
e.currentTarget.style.display = "none"; e.currentTarget.style.display = "none";
@ -147,7 +146,7 @@ export default function EcosystemPage() {
</div> </div>
{selectedCompany?.description ? ( {selectedCompany?.description ? (
<div className="p-4 rounded-lg"> <div className="p-4 rounded-lg">
<p className="font-persian leading-relaxed"> <p className="font-persian text-sm font-normal leading-relaxed">
{selectedCompany.description} {selectedCompany.description}
</p> </p>
</div> </div>
@ -159,22 +158,22 @@ export default function EcosystemPage() {
</div> </div>
{/* Left Column - Company Fields */} {/* Left Column - Company Fields */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className="font-persian gap-1 flex text-lg font-bold"> <h3 className="font-persian gap-1 flex text-sm font-semibold">
اطلاعات اطلاعات
<span>{selectedCompany?.category}</span> <span>{selectedCompany?.category}</span>
</h3> </h3>
{selectedCompany?.fields && {selectedCompany?.fields &&
selectedCompany.fields.length > 0 ? ( selectedCompany.fields.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3 px-4">
{selectedCompany.fields.map((field, index) => ( {selectedCompany.fields.map((field, index) => (
<div <div
key={index} key={index}
className="flex justify-between items-center rounded-lg" className="flex justify-between items-center rounded-lg"
> >
<span className="font-persian font-light"> <span className="font-persian text-sm font-light">
{field.N}: {field.N}:
</span> </span>
<span className="font-persian font-light text-right"> <span className="font-persian text-sm font-light text-right">
{handleValue(field.V)} {handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>} {field.U && <span className="mr-1">({field.U})</span>}
</span> </span>