Compare commits
2 Commits
main
...
refactor_#
| Author | SHA1 | Date | |
|---|---|---|---|
| 380c0f43aa | |||
| 737a68d886 |
23
app/app.css
23
app/app.css
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
دادهای برای نمایش وجود ندارد
|
دادهای برای نمایش وجود ندارد
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user