add popup for innvation ,also fix the zoom initial in graph popup

This commit is contained in:
Saeed AB 2025-08-19 06:22:03 +03:30
parent a51f8b3105
commit ba7a2499f1
4 changed files with 382 additions and 363 deletions

View File

@ -5,6 +5,7 @@ import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
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 moment from "moment-jalaali";
import type { BarChartData } from "~/components/ui/custom-bar-chart"; import type { BarChartData } from "~/components/ui/custom-bar-chart";
import { import {
Table, Table,
@ -25,11 +26,17 @@ import {
ChevronDown, ChevronDown,
RefreshCw, RefreshCw,
ExternalLink, ExternalLink,
Building2,
PickaxeIcon,
UserIcon,
UsersIcon,
} from "lucide-react"; } from "lucide-react";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import {Funnel, Wrench , CirclePause , DollarSign} from "lucide-react" import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react";
import ProjectDetail from "../projects/project-detail";
moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData { interface ProcessInnovationData {
project_no: string; project_no: string;
title: string; title: string;
@ -39,6 +46,7 @@ interface ProcessInnovationData {
throat_removal: string; throat_removal: string;
amount_currency_reduction: string; amount_currency_reduction: string;
Reduce_rate_failure: string; Reduce_rate_failure: string;
observer: string;
} }
interface SortConfig { interface SortConfig {
@ -72,9 +80,19 @@ const columns = [
{ key: "select", label: "", sortable: false, width: "50px" }, { key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" }, { key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "400px" }, { key: "title", label: "عنوان پروژه", sortable: true, width: "400px" },
{ key: "project_status", label: "وضعیت پروژه", sortable: true, width: "140px" }, {
{ key: "project_rating", label: "امتیاز پروژه", sortable: true, width: "140px" }, key: "project_status",
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" } label: "وضعیت پروژه",
sortable: true,
width: "140px",
},
{
key: "project_rating",
label: "امتیاز پروژه",
sortable: true,
width: "140px",
},
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
]; ];
export function ProcessInnovationPage() { export function ProcessInnovationPage() {
@ -103,9 +121,12 @@ export function ProcessInnovationPage() {
field: "start_date", field: "start_date",
direction: "asc", direction: "asc",
}); });
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(new Set()); const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set(),
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] = useState<ProcessInnovationData | null>(null); const [selectedProjectDetails, setSelectedProjectDetails] =
useState<ProcessInnovationData | null>(null);
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
@ -114,7 +135,7 @@ export function ProcessInnovationPage() {
if (selectedProjects.size === projects.length) { if (selectedProjects.size === projects.length) {
setSelectedProjects(new Set()); setSelectedProjects(new Set());
} else { } else {
setSelectedProjects(new Set(projects.map(p => p.project_no))); setSelectedProjects(new Set(projects.map((p) => p.project_no)));
} }
}; };
@ -129,6 +150,7 @@ export function ProcessInnovationPage() {
}; };
const handleProjectDetails = (project: ProcessInnovationData) => { const handleProjectDetails = (project: ProcessInnovationData) => {
console.log(project);
setSelectedProjectDetails(project); setSelectedProjectDetails(project);
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
}; };
@ -145,9 +167,12 @@ export function ProcessInnovationPage() {
{ {
id: "production-stops-prevention", id: "production-stops-prevention",
title: "جلوگیری از توقفات تولید", title: "جلوگیری از توقفات تولید",
value: formatNumber(stats.productionStopsPreventionSum.toFixed?.(1) ?? stats.productionStopsPreventionSum), value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum,
),
description: "ظرفیت افزایش یافته", description: "ظرفیت افزایش یافته",
icon: <CirclePause/>, icon: <CirclePause />,
color: "text-emerald-400", color: "text-emerald-400",
}, },
{ {
@ -156,25 +181,30 @@ export function ProcessInnovationPage() {
value: formatNumber(stats.bottleneckRemovalCount), value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه", description: "تعداد رفع گلوگاه",
icon: <Funnel />, icon: <Funnel />,
color: "text-emerald-400" color: "text-emerald-400",
}, },
{ {
id: "currency-reduction", id: "currency-reduction",
title: "کاهش ارز بری", title: "کاهش ارز بری",
value: formatNumber(stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum), value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum,
),
description: "دلار کاهش یافته", description: "دلار کاهش یافته",
icon: <DollarSign/>, icon: <DollarSign />,
color: "text-emerald-400", color: "text-emerald-400",
}, },
{ {
id: "frequent-failures-reduction", id: "frequent-failures-reduction",
title: "کاهش خرابیهای پرتکرار", title: "کاهش خرابیهای پرتکرار",
value: formatNumber(stats.frequentFailuresReductionSum.toFixed?.(1) ?? stats.frequentFailuresReductionSum), value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum,
),
description: "مجموع درصد کاهش خرابی", description: "مجموع درصد کاهش خرابی",
icon: <Wrench/>, icon: <Wrench />,
color: "text-emerald-400", color: "text-emerald-400",
} },
]; ];
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
@ -201,16 +231,22 @@ export function ProcessInnovationPage() {
"title", "title",
"project_status", "project_status",
"project_rating", "project_rating",
"reduce_prevention_production_stops",
"throat_removal", "throat_removal",
"reduce_prevention_production_stops",
"amount_currency_reduction", "amount_currency_reduction",
"Reduce_rate_failure", "Reduce_rate_failure",
"project_description",
"start_date",
"done_date",
"approved_budget",
"observer",
], ],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Sorts: [["start_date", "asc"]],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]], Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
console.log(JSON.parse(response.data));
if (response.state === 0) { if (response.state === 0) {
const dataString = response.data; const dataString = response.data;
if (dataString && typeof dataString === "string") { if (dataString && typeof dataString === "string") {
@ -289,7 +325,7 @@ export function ProcessInnovationPage() {
}, [currentPage]); }, [currentPage]);
useEffect(() => { useEffect(() => {
const scrollContainer = document.querySelector('.overflow-auto'); const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => { const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore) return; if (!scrollContainer || !hasMore || loadingMore) return;
@ -303,12 +339,12 @@ export function ProcessInnovationPage() {
}; };
if (scrollContainer) { if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll); scrollContainer.addEventListener("scroll", handleScroll);
} }
return () => { return () => {
if (scrollContainer) { if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll); scrollContainer.removeEventListener("scroll", handleScroll);
} }
}; };
}, [loadMore, hasMore, loadingMore]); }, [loadMore, hasMore, loadingMore]);
@ -359,13 +395,14 @@ export function ProcessInnovationPage() {
try { try {
setStatsLoading(true); setStatsLoading(true);
const raw = await apiService.callInnovationProcess<any>({ const raw = await apiService.callInnovationProcess<any>({
innovation_process_function: { innovation_process_function: {},
},
}); });
let payload: any = raw?.data; let payload: any = raw?.data;
if (typeof payload === "string") { if (typeof payload === "string") {
try { payload = JSON.parse(payload); } catch {} try {
payload = JSON.parse(payload);
} catch {}
} }
const parseNum = (v: unknown): number => { const parseNum = (v: unknown): number => {
@ -382,14 +419,24 @@ export function ProcessInnovationPage() {
const normalized: InnovationStats = { const normalized: InnovationStats = {
totalProjects: parseNum(payload?.count_innovation_process_projects), totalProjects: parseNum(payload?.count_innovation_process_projects),
averageScore: parseNum(payload?.average_project_score), averageScore: parseNum(payload?.average_project_score),
productionStopsPreventionSum: parseNum(payload?.sum_stopping_production), productionStopsPreventionSum: parseNum(
payload?.sum_stopping_production,
),
bottleneckRemovalCount: parseNum(payload?.count_throat_removal), bottleneckRemovalCount: parseNum(payload?.count_throat_removal),
currencyReductionSum: parseNum(payload?.sum_reduction_value_currency), currencyReductionSum: parseNum(payload?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum(payload?.sum_reducing_breakdowns), frequentFailuresReductionSum: parseNum(
percentProductionStops: parseNum(payload?.percent_sum_stopping_production), payload?.sum_reducing_breakdowns,
),
percentProductionStops: parseNum(
payload?.percent_sum_stopping_production,
),
percentBottleneckRemoval: parseNum(payload?.percent_throat_removal), percentBottleneckRemoval: parseNum(payload?.percent_throat_removal),
percentCurrencyReduction: parseNum(payload?.percent_reduction_value_currency), percentCurrencyReduction: parseNum(
percentFailuresReduction: parseNum(payload?.percent_reducing_breakdowns), payload?.percent_reduction_value_currency,
),
percentFailuresReduction: parseNum(
payload?.percent_reducing_breakdowns,
),
}; };
setStats(normalized); setStats(normalized);
@ -481,10 +528,7 @@ export function ProcessInnovationPage() {
); );
case "project_no": case "project_no":
return ( return (
<Badge <Badge variant="outline" className="font-mono">
variant="outline"
className="font-mono"
>
{String(value)} {String(value)}
</Badge> </Badge>
); );
@ -496,7 +540,7 @@ export function ProcessInnovationPage() {
variant="outline" variant="outline"
className="font-medium border-2" className="font-medium border-2"
style={{ style={{
border:"none", border: "none",
}} }}
> >
{String(value)} {String(value)}
@ -504,10 +548,7 @@ export function ProcessInnovationPage() {
); );
case "project_rating": case "project_rating":
return ( return (
<Badge <Badge variant="outline" className="text-lg text-center border-none">
variant="outline"
className="text-lg text-center border-none"
>
{formatNumber(String(value))} {formatNumber(String(value))}
</Badge> </Badge>
); );
@ -529,187 +570,215 @@ export function ProcessInnovationPage() {
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{/* Stats Cards */} {/* Stats Cards */}
<div className="flex gap-6"> <div className="flex gap-6">
<div className="space-y-6 w-full"> <div className="space-y-6 w-full">
{/* Stats Grid */} {/* Stats Grid */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{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 key={`skeleton-${index}`} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden"> <Card
<CardContent className="p-2"> 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 className="h-6 bg-gray-600 rounded animate-pulse" style={{ width: '60%' }} /> <CardContent className="p-2">
<div className="p-3 bg-emerald-500/20 rounded-full w-fit"> <div className="flex flex-col justify-between gap-2">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" /> <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<div
className="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%' }} /> : statsCards.map((card) => (
</div> <Card
</div> key={card.id}
</CardContent> className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
</Card> >
)) <CardContent className="p-2">
) : ( <div className="flex flex-col justify-between gap-2">
statsCards.map((card) => ( <div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<Card key={card.id} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"> <h3 className="text-lg font-bold text-white font-persian">
<CardContent className="p-2"> {card.title}
<div className="flex flex-col justify-between gap-2"> </h3>
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20"> <div
<h3 className="text-lg font-bold text-white font-persian"> className={`p-3 gird placeitems-center rounded-full w-fit `}
{card.title} >
</h3> {card.icon}
<div className={`p-3 gird placeitems-center rounded-full w-fit `}> </div>
{card.icon} </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>
</div> </CardContent>
<div className="flex items-center justify-center flex-col p-1"> </Card>
<p className={`text-3xl font-bold ${card.color} mb-1`}> ))}
{card.value} </div>
</p>
<p className="text-sm text-gray-300 font-persian">
{card.description}
</p>
</div>
</div>
</CardContent>
</Card>
))
)}
</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"> <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden">
<CardContent className="p-4"> <CardContent className="p-4">
<CustomBarChart <CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای" title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
loading={statsLoading} loading={statsLoading}
data={[ data={[
{ {
label: "کاهش توقفات تولید", label: "کاهش توقفات تولید",
value: stats.percentProductionStops || 0, value: stats.percentProductionStops || 0,
color: "bg-emerald-400", color: "bg-emerald-400",
labelColor: "text-white" labelColor: "text-white",
}, },
{ {
label: "رفع گلوگاه تولید", label: "رفع گلوگاه تولید",
value: stats.percentBottleneckRemoval || 0, value: stats.percentBottleneckRemoval || 0,
color: "bg-emerald-400", color: "bg-emerald-400",
labelColor: "text-white" labelColor: "text-white",
}, },
{ {
label: "کاهش ارز بری", label: "کاهش ارز بری",
value: stats.percentCurrencyReduction || 0, value: stats.percentCurrencyReduction || 0,
color: "bg-emerald-400", color: "bg-emerald-400",
labelColor: "text-white" labelColor: "text-white",
}, },
{ {
label: "کاهش خرابی پر تکرار", label: "کاهش خرابی پر تکرار",
value: stats.percentFailuresReduction || 0, value: stats.percentFailuresReduction || 0,
color: "bg-emerald-400", color: "bg-emerald-400",
labelColor: "text-white" labelColor: "text-white",
} },
]} ]}
barHeight="h-5" barHeight="h-5"
showAxisLabels={true} showAxisLabels={true}
/> />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* Data Table */} {/* Data Table */}
<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(90vh-400px)]"> <Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(90vh-400px)]">
<TableHeader> <TableHeader>
<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 sticky top-0 z-20 bg-[#3F415A]" className="text-right font-persian whitespace-nowrap text-gray-200 font-medium sticky top-0 z-20 bg-[#3F415A]"
style={{ width: column.width }} style={{ width: column.width }}
> >
{column.key === "select" ? ( {column.key === "select" ? (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Checkbox <Checkbox
checked={selectedProjects.size === projects.length && projects.length > 0} checked={
onCheckedChange={handleSelectAll} selectedProjects.size === projects.length &&
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" projects.length > 0
}
onCheckedChange={handleSelectAll}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
) : column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 10 }).map((_, index) => (
<TableRow
key={`skeleton-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-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="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }}
/> />
</div> </div>
) : column.sortable ? ( </TableCell>
<button ))}
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 10 }).map((_, index) => (
<TableRow key={`skeleton-${index}`} className="text-sm leading-tight h-8">
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-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="h-2.5 bg-gray-600 rounded animate-pulse" style={{ width: `${Math.random() * 60 + 40}%` }} />
</div>
</TableCell>
))}
</TableRow>
))
) : projects.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<span className="text-gray-400 font-persian">
هیچ پروژهای یافت نشد
</span>
</TableCell>
</TableRow> </TableRow>
) : ( ))
projects.map((project, index) => ( ) : projects.length === 0 ? (
<TableRow key={`${project.project_no}-${index}`} className="text-sm leading-tight h-8"> <TableRow>
{columns.map((column) => ( <TableCell
<TableCell colSpan={columns.length}
key={column.key} className="text-center py-8"
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key==="select" ? "flex justify-center items-center" :""}`} >
> <span className="text-gray-400 font-persian">
{renderCellContent(project, column)} هیچ پروژهای یافت نشد
</TableCell> </span>
))} </TableCell>
</TableRow> </TableRow>
)) ) : (
)} projects.map((project, index) => (
</TableBody> <TableRow
</Table> key={`${project.project_no}-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
>
{renderCellContent(project, column)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div> </div>
{/* Infinite scroll trigger */} {/* Infinite scroll trigger */}
@ -718,8 +787,7 @@ export function ProcessInnovationPage() {
<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-4 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300 text-xs"> <span className="font-persian text-gray-300 text-xs"></span>
</span>
</div> </div>
</div> </div>
)} )}
@ -752,8 +820,12 @@ export function ProcessInnovationPage() {
{/* Footer */} {/* Footer */}
<div className="p-2 px-4 bg-gray-700/50"> <div className="p-2 px-4 bg-gray-700/50">
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian"> <div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian">
<div className="text-center gap-2 items-center flex"> <div className="text-center gap-2 items-center flex">
<div className="text-base text-gray-401 mb-1"> کل پروژه ها : {formatNumber(stats.totalProjects || actualTotalCount)}</div> <div className="text-base text-gray-401 mb-1">
{" "}
کل پروژه ها :{" "}
{formatNumber(stats.totalProjects || actualTotalCount)}
</div>
</div> </div>
{/* Project number column - empty */} {/* Project number column - empty */}
<div></div> <div></div>
@ -763,126 +835,103 @@ export function ProcessInnovationPage() {
<div></div> <div></div>
{/* Project rating column - show average */} {/* Project rating column - show average */}
<div className="flex justify-center items-center gap-2"> <div className="flex justify-center items-center gap-2">
<div className="text-base text-gray-400 mb-1"> میانگین امتیاز :</div> <div className="text-base text-gray-400 mb-1">
{" "}
میانگین امتیاز :
</div>
<div className="font-bold"> <div className="font-bold">
{formatNumber(((stats.averageScore ?? 0) as number).toFixed?.(1) ?? (stats.averageScore ?? 0))} {formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ??
0,
)}
</div> </div>
</div> </div>
{/* Details column - show total count */} {/* Details column - show total count */}
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>
{/* Project Details Dialog */} {/* Project Details Dialog */}
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[#2A2D3E] border-gray-700 max-w-2xl 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 font-persian text-right"> <DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
جزئیات پروژه شرح پروژه
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 flex justify-between text-right px-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian px-2 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>
</div>
{selectedProjectDetails && ( {/* Project Details */}
<div className="space-y-6 text-right"> <div className="flex flex-[3] gap-2 flex-col px-4">
{/* Project Header */} <div className="font-bold text-right ">جزئیات پروژه</div>
<div className="bg-gray-700/30 rounded-lg p-4">
<h3 className="text-lg font-bold text-white font-persian mb-2"> <div className="flex items-center justify-between">
{selectedProjectDetails.title} <h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
</h3> <Building2 className="h-4 text-green-500" />
<div className="flex items-center gap-4"> زمان شروع:
<Badge </h4>
variant="outline" <span className="text-white font-bold font-persian">
className="font-mono text-emerald-400 border-emerald-500/50" {selectedProjectDetails?.start_date
> ? moment(
{selectedProjectDetails.project_no} selectedProjectDetails?.start_date,
</Badge> "YYYY-MM-DD",
<Badge ).format("YYYY/MM/DD")
variant="outline" : "-"}
className="font-medium border-2" </span>
style={{
color: getStatusColor(selectedProjectDetails.project_status),
borderColor: getStatusColor(selectedProjectDetails.project_status),
backgroundColor: `${getStatusColor(selectedProjectDetails.project_status)}20`,
}}
>
{selectedProjectDetails.project_status}
</Badge>
</div>
</div> </div>
{/* Project Metrics */} <div className="flex items-center justify-between">
<div className="grid grid-cols-2 gap-4"> <h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<div className="bg-gray-700/20 rounded-lg p-4"> <PickaxeIcon className="h-4 text-green-500" />
<h4 className="text-sm font-medium text-gray-300 font-persian mb-2"> زمان پایان:
امتیاز پروژه </h4>
</h4> <span className="text-white font-bold font-persian">
<Badge {selectedProjectDetails?.done_date
variant="outline" ? moment(
className="text-lg font-bold border-2" selectedProjectDetails?.done_date,
style={{ "YYYY-MM-DD",
color: getRatingColor(selectedProjectDetails.project_rating), ).format("YYYY/MM/DD")
borderColor: getRatingColor(selectedProjectDetails.project_rating), : "-"}
backgroundColor: `${getRatingColor(selectedProjectDetails.project_rating)}20`, </span>
}}
>
{formatNumber(selectedProjectDetails.project_rating)}
</Badge>
</div>
<div className="bg-gray-700/20 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-300 font-persian mb-2">
کاهش توقفات تولید
</h4>
<span className="text-lg font-bold text-blue-400">
{formatNumber(selectedProjectDetails.reduce_prevention_production_stops)}
</span>
</div>
<div className="bg-gray-700/20 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-300 font-persian mb-2">
رفع گلوگاه تولید
</h4>
<span className="text-lg font-bold text-blue-400">
{formatNumber(selectedProjectDetails.throat_removal)}
</span>
</div>
<div className="bg-gray-700/20 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-300 font-persian mb-2">
کاهش ارز بری
</h4>
<span className="text-lg font-bold text-emerald-400">
{formatCurrency(selectedProjectDetails.amount_currency_reduction)}
</span>
</div>
<div className="bg-gray-700/20 rounded-lg p-4 col-span-2">
<h4 className="text-sm font-medium text-gray-300 font-persian mb-2">
کاهش خرابی پر تکرار
</h4>
<span className="text-lg font-bold text-blue-400">
{formatNumber(selectedProjectDetails.Reduce_rate_failure)}
</span>
</div>
</div> </div>
{/* Action Buttons */} <div className="flex items-center justify-between">
<div className="flex justify-end gap-3 pt-4 border-t border-gray-700"> <h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<Button <UsersIcon className="h-4 text-green-500" />
variant="outline" هزینه برآورد شده:
onClick={() => setDetailsDialogOpen(false)} </h4>
className="border-gray-600 text-gray-300 hover:bg-gray-700" <span className="text-white font-bold font-persian">
> {formatNumber(
بستن Number(
</Button> selectedProjectDetails?.approved_budget.replaceAll(
",",
"",
),
),
) || "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<UserIcon className="h-4 text-green-500" />
نفر مرتبط:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.observer || "-"}
</span>
</div> </div>
</div> </div>
)} </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</DashboardLayout> </DashboardLayout>

View File

@ -199,7 +199,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
// Create zoom behavior // Create zoom behavior
const zoom = d3 const zoom = d3
.zoom<SVGSVGElement, unknown>() .zoom<SVGSVGElement, unknown>()
.scaleExtent([1, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x .scaleExtent([0.8, 2.5]) // Limit zoom out to 1x, zoom in to 2.5x
.on("zoom", (event) => { .on("zoom", (event) => {
container.attr("transform", event.transform); container.attr("transform", event.transform);
}); });
@ -240,6 +240,18 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)), d3.forceCollide().radius((d) => (d.isCenter ? 40 : 30)),
); );
const initialScale = 0.85;
const initialTranslate = [
width / 2 - (width / 2) * initialScale,
height / 2 - (height / 2) * initialScale,
];
svg.call(
zoom.transform,
d3.zoomIdentity
.translate(initialTranslate[0], initialTranslate[1])
.scale(initialScale),
);
// Fix center node position // Fix center node position
const centerNode = nodes.find((n) => n.isCenter); const centerNode = nodes.find((n) => n.isCenter);
if (centerNode) { if (centerNode) {
@ -424,19 +436,13 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
// Filter out image fields and find description // Filter out image fields and find description
const filteredFields = fieldValues.filter( const filteredFields = fieldValues.filter(
(field: any) => (field: any) =>
![ !["image", "img", "full_name"].includes(field.F.toLowerCase()),
"image",
"img",
"des",
"dec",
"description",
"collaboration",
].includes(field.F.toLowerCase()),
); );
const descriptionField = fieldValues.find( const descriptionField = fieldValues.find(
(field: any) => (field: any) =>
field.F.toLowerCase().includes("description") || field.F.toLowerCase().includes("description") ||
field.F.toLowerCase().includes("collaboration") ||
field.F.toLowerCase().includes("about"), field.F.toLowerCase().includes("about"),
); );

View File

@ -17,7 +17,6 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@react-router/node": "^7.7.0", "@react-router/node": "^7.7.0",
"@react-router/serve": "^7.7.1", "@react-router/serve": "^7.7.1",
"@sigma/node-image": "^3.0.0",
"@types/d3": "^7.4.3", "@types/d3": "^7.4.3",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -32,7 +31,6 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-router": "^7.7.0", "react-router": "^7.7.0",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"sigma": "^3.0.2",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -32,9 +32,6 @@ importers:
'@react-router/serve': '@react-router/serve':
specifier: ^7.7.1 specifier: ^7.7.1
version: 7.8.0(react-router@7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.8.3) version: 7.8.0(react-router@7.7.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(typescript@5.8.3)
'@sigma/node-image':
specifier: ^3.0.0
version: 3.0.0(sigma@3.0.2(graphology-types@0.24.8))
'@types/d3': '@types/d3':
specifier: ^7.4.3 specifier: ^7.4.3
version: 7.4.3 version: 7.4.3
@ -77,9 +74,6 @@ importers:
recharts: recharts:
specifier: ^3.1.2 specifier: ^3.1.2
version: 3.1.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@19.1.1)(react@19.1.0)(redux@5.0.1) version: 3.1.2(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react-is@19.1.1)(react@19.1.0)(redux@5.0.1)
sigma:
specifier: ^3.0.2
version: 3.0.2(graphology-types@0.24.8)
tailwind-merge: tailwind-merge:
specifier: ^3.3.1 specifier: ^3.3.1
version: 3.3.1 version: 3.3.1
@ -974,11 +968,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@sigma/node-image@3.0.0':
resolution: {integrity: sha512-i4WLNPugDY4jgQEZtNSiSVj4HHXOraciXLtlgdygeUxMVEhH8PJ/+Q1vQ9f/SlKFnZQ+7vH3HnsSDW6FD9aP+g==}
peerDependencies:
sigma: '>=3.0.0-beta.10'
'@standard-schema/spec@1.0.0': '@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@ -1659,11 +1648,6 @@ packages:
graphology-types@0.24.8: graphology-types@0.24.8:
resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==} resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==}
graphology-utils@2.5.2:
resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==}
peerDependencies:
graphology-types: '>=0.23.0'
graphology@0.26.0: graphology@0.26.0:
resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==} resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==}
peerDependencies: peerDependencies:
@ -2198,9 +2182,6 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
sigma@3.0.2:
resolution: {integrity: sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==}
signal-exit@4.1.0: signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3282,10 +3263,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.45.1': '@rollup/rollup-win32-x64-msvc@4.45.1':
optional: true optional: true
'@sigma/node-image@3.0.0(sigma@3.0.2(graphology-types@0.24.8))':
dependencies:
sigma: 3.0.2(graphology-types@0.24.8)
'@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.0.0': {}
'@standard-schema/utils@0.3.0': {} '@standard-schema/utils@0.3.0': {}
@ -4009,10 +3986,6 @@ snapshots:
graphology-types@0.24.8: {} graphology-types@0.24.8: {}
graphology-utils@2.5.2(graphology-types@0.24.8):
dependencies:
graphology-types: 0.24.8
graphology@0.26.0(graphology-types@0.24.8): graphology@0.26.0(graphology-types@0.24.8):
dependencies: dependencies:
events: 3.3.0 events: 3.3.0
@ -4503,13 +4476,6 @@ snapshots:
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2 side-channel-weakmap: 1.0.2
sigma@3.0.2(graphology-types@0.24.8):
dependencies:
events: 3.3.0
graphology-utils: 2.5.2(graphology-types@0.24.8)
transitivePeerDependencies:
- graphology-types
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
source-map-js@1.2.1: {} source-map-js@1.2.1: {}