diff --git a/FIGMA_LOGIN_IMPLEMENTATION.md b/FIGMA_LOGIN_IMPLEMENTATION.md index ac9f0ab..7700ec6 100644 --- a/FIGMA_LOGIN_IMPLEMENTATION.md +++ b/FIGMA_LOGIN_IMPLEMENTATION.md @@ -109,7 +109,7 @@ This document describes the exact implementation of the login page based on the داشبورد مدیریت فناوری و نوآوری

- لطفاً نام کاربری و پسورد خود را وارد فهرست خواسته شده وارد + لطفاً نام کاربری و کلمه عبور خود را وارد فهرست خواسته شده وارد
فرمایید.

diff --git a/app/components/dashboard/d3-image-info.tsx b/app/components/dashboard/d3-image-info.tsx new file mode 100644 index 0000000..becf542 --- /dev/null +++ b/app/components/dashboard/d3-image-info.tsx @@ -0,0 +1,241 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import * as d3 from "d3"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; + +export type D3ImageInfoProps = { + imageUrl?: string; + title?: string; + description?: string; + width?: number; // fallback width if container size not measured yet + height?: number; // fallback height +}; + +/** + * D3ImageInfo + * - Renders an image and an information box beside it using D3 within an SVG. + * - Includes a clickable "show" chip that opens a popup dialog with more details. + */ +export function D3ImageInfo({ + imageUrl = "/placeholder.svg", + title = "عنوان آیتم", + description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده می‌شود.", + width = 800, + height = 360, +}: D3ImageInfoProps) { + const containerRef = useRef(null); + const svgRef = useRef(null); + const [open, setOpen] = useState(false); + + // Redraw helper + const draw = () => { + if (!containerRef.current || !svgRef.current) return; + + const container = containerRef.current; + const svg = d3.select(svgRef.current); + + const W = Math.max(480, container.clientWidth || width); + const H = Math.max(260, height); + + svg.attr("width", W).attr("height", H); + + // Clear previous content + svg.selectAll("*").remove(); + + // Layout + const padding = 16; + const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35)); + const infoAreaX = padding + imageAreaWidth + padding; + const infoAreaWidth = W - infoAreaX - padding; + + // Image area (with rounded border) + const imgGroup = svg + .append("g") + .attr("transform", `translate(${padding}, ${padding})`); + + const imgW = imageAreaWidth; + const imgH = H - 2 * padding; + + // Frame + imgGroup + .append("rect") + .attr("width", imgW) + .attr("height", imgH) + .attr("rx", 10) + .attr("ry", 10) + .attr("fill", "#1F2937") // gray-800 + .attr("stroke", "#4B5563") // gray-600 + .attr("stroke-width", 1.5); + + // Image + imgGroup + .append("image") + .attr("href", imageUrl) + .attr("x", 4) + .attr("y", 4) + .attr("width", imgW - 8) + .attr("height", imgH - 8) + .attr("preserveAspectRatio", "xMidYMid slice") + .attr("clip-path", null); + + // Info area + const infoGroup = svg + .append("g") + .attr("transform", `translate(${infoAreaX}, ${padding})`); + + // Info container + infoGroup + .append("rect") + .attr("width", Math.max(220, infoAreaWidth)) + .attr("height", imgH) + .attr("rx", 12) + .attr("ry", 12) + .attr("fill", "url(#infoGradient)") + .attr("stroke", "#6B7280") // gray-500 + .attr("stroke-width", 1); + + // Background gradient + const defs = svg.append("defs"); + const gradient = defs + .append("linearGradient") + .attr("id", "infoGradient") + .attr("x1", "0%") + .attr("y1", "0%") + .attr("x2", "0%") + .attr("y2", "100%"); + + gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900 + gradient + .append("stop") + .attr("offset", "100%") + .attr("stop-color", "#374151"); // gray-700 + + // Title + infoGroup + .append("text") + .attr("x", 16) + .attr("y", 36) + .attr("fill", "#F9FAFB") // gray-50 + .attr("font-weight", 700) + .attr("font-size", 18) + .text(title); + + // Description (wrapped) + const wrapText = (text: string, maxWidth: number) => { + const words = text.split(/\s+/).reverse(); + const lines: string[] = []; + let line: string[] = []; + let t = ""; + while (words.length) { + const word = words.pop()!; + const test = (t + " " + word).trim(); + // Approximate measure using character count + const tooLong = test.length * 8 > maxWidth; // 8px avg char width + if (tooLong && t.length) { + lines.push(t); + t = word; + } else { + t = test; + } + } + if (t) lines.push(t); + return lines; + }; + + const descMaxWidth = Math.max(200, infoAreaWidth - 32); + const descLines = wrapText(description, descMaxWidth); + + descLines.forEach((line, i) => { + infoGroup + .append("text") + .attr("x", 16) + .attr("y", 70 + i * 22) + .attr("fill", "#E5E7EB") // gray-200 + .attr("font-size", 14) + .text(line); + }); + + // Show button-like chip + const chipY = Math.min(imgH - 48, 70 + descLines.length * 22 + 16); + const chip = infoGroup + .append("g") + .attr("class", "show-chip") + .style("cursor", "pointer"); + + const chipW = 120; + const chipH = 36; + + chip + .append("rect") + .attr("x", 16) + .attr("y", chipY) + .attr("width", chipW) + .attr("height", chipH) + .attr("rx", 8) + .attr("ry", 8) + .attr("fill", "#3B82F6") // blue-500 + .attr("stroke", "#60A5FA") + .attr("stroke-width", 1.5) + .attr("opacity", 0.95); + + chip + .append("text") + .attr("x", 16 + chipW / 2) + .attr("y", chipY + chipH / 2 + 5) + .attr("text-anchor", "middle") + .attr("fill", "#FFFFFF") + .attr("font-weight", 700) + .text("نمایش"); + + // Hover & click + chip + .on("mouseenter", function () { + d3.select(this).select("rect").attr("fill", "#2563EB"); // blue-600 + }) + .on("mouseleave", function () { + d3.select(this).select("rect").attr("fill", "#3B82F6"); // blue-500 + }) + .on("click", () => setOpen(true)); + }; + + useEffect(() => { + const ro = new ResizeObserver(() => draw()); + if (containerRef.current) ro.observe(containerRef.current); + draw(); + return () => ro.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imageUrl, title, description]); + + return ( +
+
+ +
+ + + + + {title} + + {description} + + +
+ {title} +
+
+
+
+ ); +} diff --git a/app/components/dashboard/dashboard-custom-bar-chart.tsx b/app/components/dashboard/dashboard-custom-bar-chart.tsx new file mode 100644 index 0000000..fff9e2a --- /dev/null +++ b/app/components/dashboard/dashboard-custom-bar-chart.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { formatNumber } from "~/lib/utils"; + +interface DataItem { + label: string; + value: number; + color: string; +} + +interface DashboardCustomBarChartProps { + title: string; + data: DataItem[]; + loading?: boolean; +} + +export function DashboardCustomBarChart({ + title, + data, + loading = false, +}: DashboardCustomBarChartProps) { + if (loading) { + return ( +
+

+ {title} +

+
+ {[1, 2, 3].map((i) => ( +
+
+
+ ))} +
+
+ ); + } + + // Calculate the maximum value for scaling + const maxValue = Math.max(...data.map((item) => item.value)); + + return ( +
+

+ {title} +

+
+ {data.map((item, index) => { + const widthPercentage = + maxValue > 0 ? (item.value / maxValue) * 100 : 0; + + return ( +
+ {/* Bar container */} +
+ {/* Animated bar */} +
+ + {formatNumber(item.value)} + + + {item.label} + +
+
+
+ ); + })} +
+
+ ); +} diff --git a/app/components/dashboard/dashboard-home.tsx b/app/components/dashboard/dashboard-home.tsx index 19b57a6..bcf8b47 100644 --- a/app/components/dashboard/dashboard-home.tsx +++ b/app/components/dashboard/dashboard-home.tsx @@ -1,39 +1,822 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { DashboardLayout } from "./layout"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Progress } from "~/components/ui/progress"; +import { Badge } from "~/components/ui/badge"; +import { Button } from "~/components/ui/button"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + LineChart, + Line, +} from "recharts"; +import apiService from "~/lib/api"; +import toast from "react-hot-toast"; +import { + Calendar, + TrendingUp, + TrendingDown, + Target, + Lightbulb, + DollarSign, + Minus, + CheckCircle, + BookOpen, +} from "lucide-react"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; +import { CustomBarChart } from "~/components/ui/custom-bar-chart"; +import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart"; +import { InteractiveBarChart } from "./interactive-bar-chart"; +import { D3ImageInfo } from "./d3-image-info"; +import { + Label, + PolarGrid, + PolarRadiusAxis, + RadialBar, + RadialBarChart, +} from "recharts"; +import { ChartContainer } from "~/components/ui/chart"; +import { formatNumber } from "~/lib/utils"; export function DashboardHome() { + const [dashboardData, setDashboardData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchDashboardData(); + }, []); + + const fetchDashboardData = async () => { + try { + setLoading(true); + setError(null); + + // First authenticate if needed + const token = localStorage.getItem("auth_token"); + if (!token) { + await apiService.login("inogen_admin", "123456"); + } + + // Fetch top cards data + const topCardsResponse = await apiService.call({ + main_page_first_function: {}, + }); + + // Fetch left section data + const leftCardsResponse = await apiService.call({ + main_page_second_function: {}, + }); + + const topCardsResponseData = JSON.parse(topCardsResponse?.data); + const leftCardsResponseData = JSON.parse(leftCardsResponse?.data); + + console.log("API Responses:", { + topCardsResponseData, + leftCardsResponseData, + }); + + // Use real API data structure with English keys + const topData = topCardsResponseData || {}; + const leftData = leftCardsResponseData || {}; + const realData = { + topData: topData, + leftData: leftData, + chartData: leftCardsResponseData?.chartData || [], + }; + setDashboardData(realData); + } catch (error) { + console.error("Error fetching dashboard data:", error); + const errorMessage = + error instanceof Error ? error.message : "خطای نامشخص"; + setError(`خطا در بارگذاری داده‌ها: ${errorMessage}`); + toast.error(`خطا در بارگذاری داده‌ها: ${errorMessage}`); + } finally { + setLoading(false); + } + }; + + // RadialBarChart data for ideas visualization + const getIdeasChartData = () => { + if (!dashboardData?.topData) + return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }]; + + const registered = parseFloat( + dashboardData.topData.registered_innovation_technology_idea || "0", + ); + const ongoing = parseFloat( + dashboardData.topData.ongoing_innovation_technology_ideas || "0", + ); + const percentage = + registered > 0 ? Math.round((ongoing / registered) * 100) : 0; + + return [ + { browser: "safari", visitors: percentage, fill: "var(--color-safari)" }, + ]; + }; + + const chartData = getIdeasChartData(); + + const chartConfig = { + visitors: { + label: "Ideas Progress", + }, + safari: { + label: "Safari", + color: "var(--chart-2)", + }, + }; + + // Skeleton component for cards + const SkeletonCard = ({ className = "" }) => ( +
+
+
+
+
+
+
+
+ ); + + // Skeleton for the chart + const SkeletonChart = () => ( +
+
+
+
+
+
+
+
+
+
+ {[...Array(12)].map((_, i) => ( +
+
+
+
+
+ ))} +
+
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+
+ ); + + if (loading) { + return ( + +
+ {/* Top Cards Row */} +
+ + + + +
+ + {/* Middle Section */} +
+ {/* Chart Section */} + +
+ + {/* Right Sidebar */} +
+ + + + +
+
+
+ ); + } + + if (error || !dashboardData) { + return ( + +
+ + +
+
+ +
+

+ خطا در بارگذاری داده‌ها +

+

+ {error || + "خطای نامشخص در بارگذاری داده‌های داشبورد رخ داده است"} +

+ +
+
+
+
+
+ ); + } + return ( -
- {/* Main Content Area - Empty for now */} -
- - - -
-
-
- + {/* Top Cards Row - Redesigned to match other components */} +
+ {/* Ideas Card */} + + +
+
+

+ ایده‌های فناوری و نوآوری +

+
+
+ + 0 + ? Math.round( + (parseFloat( + dashboardData.topData + ?.ongoing_innovation_technology_ideas || + "0", + ) / + parseFloat( + dashboardData.topData + ?.registered_innovation_technology_idea || + "1", + )) * + 100, + ) + : 0, + fill: "green", + }, + ]} + startAngle={90} + endAngle={ + 90 + + ((parseFloat( + dashboardData.topData + ?.registered_innovation_technology_idea || "0", + ) > 0 + ? Math.round( + (parseFloat( + dashboardData.topData + ?.ongoing_innovation_technology_ideas || "0", + ) / + parseFloat( + dashboardData.topData + ?.registered_innovation_technology_idea || + "1", + )) * + 100, + ) + : 0) / + 100) * + 360 + } + innerRadius={35} + outerRadius={55} > - - + + + + + +
+
+ +
ثبت شده :
+ {formatNumber( + dashboardData.topData + ?.registered_innovation_technology_idea || "0", + )} +
+ +
در حال اجرا :
+ {formatNumber( + dashboardData.topData + ?.ongoing_innovation_technology_ideas || "0", + )} +
+
-

- صفحه در دست ساخت -

-

- محتوای این بخش به زودی اضافه خواهد شد -

+
+
+
+
+ {/* Revenue Card */} + + +
+
+

+ افزایش درآمد مبتنی بر فناوری و نوآوری +

+
+
+
+
+

+ {formatNumber( + dashboardData.topData + ?.technology_innovation_based_revenue_growth || "0", + )} +

+
+ میلیون ریال +
+
+ / +
+

+ {formatNumber( + Math.round( + dashboardData.topData + ?.technology_innovation_based_revenue_growth_percent, + ) || "0", + )} + % +

+
+ درصد به کل درآمد +
+
+
+
+
+
+
+ + {/* Cost Reduction Card */} + + +
+
+

+ کاهش هزینه ها مبتنی بر فناوری و نوآوری +

+
+
+
+
+

+ {formatNumber( + Math.round( + parseFloat( + dashboardData.topData?.technology_innovation_based_cost_reduction?.replace( + /,/g, + "", + ) || "0", + ) / 1000000, + ), + )} +

+
+ میلیون ریال +
+
+ / +
+

+ {formatNumber( + Math.round( + dashboardData.topData + ?.technology_innovation_based_cost_reduction_percent, + ) || "0", + )} + % +

+
+ درصد به کل هزینه +
+
+
+
+
+
+
+ + {/* Budget Ratio Card */} + + +
+
+

+ نسبت تحقق بودجه فناوی و نوآوری +

+
+
+ + + + + + + + +
+
+ +
مصوب :
+ {formatNumber( + Math.round( + parseFloat( + dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace( + /,/g, + "", + ) || "0", + ) / 1000000000, + ), + )} +
+ +
جذب شده :
+ {formatNumber( + Math.round( + parseFloat( + dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace( + /,/g, + "", + ) || "0", + ) / 1000000000, + ), + )} +
+
+
+
+
+
+
+
+ + {/* Main Content with Tabs */} + +
+

+ تحقق ارزش ها +

+ + + شماتیک + + + مقایسه ای + + +
+ + + + + + +
+ +
+
+
+ + {/* Left Section - Status Cards */} +
+ {/* Technology Intensity */} + + +
+ + شدت فناوری + +

+ % + {formatNumber( + Math.round( + dashboardData.leftData?.technology_intensity || 0, + ), + )} +

+ +
+
+
+ + {/* Program Status */} + + + + + + + {/* Publications */} + + + + انتشارات فناوری و نوآوری + + + +
+
+
+ + کتاب: +
+ + {formatNumber( + dashboardData.leftData?.printed_books_count || "0", + )} + +
+
+
+ + پتنت: +
+ + {formatNumber( + dashboardData.leftData?.registered_patents_count || "0", + )} + +
+
+
+ + گزارش: +
+ + {formatNumber( + dashboardData.leftData?.published_reports_count || "0", + )} + +
+
+
+ + مقاله: +
+ + {formatNumber( + dashboardData.leftData?.printed_articles_count || "0", + )} + +
+
+
+
+ + {/* Promotion */} + + + + ترویج فناوری و نوآوری + + + +
+
+
+ + کنفرانس: +
+ + {formatNumber( + dashboardData.leftData?.attended_conferences_count || "0", + )} + +
+
+
+ + شرکت در رویداد: +
+ + {formatNumber( + dashboardData.leftData?.attended_events_count || "0", + )} + +
+
+
+ + نمایشگاه: +
+ + {formatNumber( + dashboardData.leftData?.attended_exhibitions_count || "0", + )} + +
+
+
+ + برگزاری رویداد: +
+ + {formatNumber( + dashboardData.leftData?.organized_events_count || "0", + )} +
diff --git a/app/components/dashboard/dashboard-layout.tsx b/app/components/dashboard/dashboard-layout.tsx index a320bda..8406aa6 100644 --- a/app/components/dashboard/dashboard-layout.tsx +++ b/app/components/dashboard/dashboard-layout.tsx @@ -144,145 +144,8 @@ export function DashboardHome() { - -
24
-

- +2 از ماه گذشته -

-
-
- - - - - پروژه‌های فعال - - - - - - - -
12
-

- +1 از هفته گذشته -

-
-
- - - - - پروژه‌های تکمیل شده - - - - - - -
8
-

- +3 از ماه گذشته -

-
-
- - - - - درصد موفقیت - - - - - - -
85%
-

- +5% از ماه گذشته -

-
- - {/* Recent Projects */} - - - پروژه‌های اخیر - - -
- {[ - { - name: "سیستم مدیریت محتوا", - status: "در حال انجام", - progress: 75, - }, - { name: "اپلیکیشن موبایل", status: "تکمیل شده", progress: 100 }, - { - name: "پلتفرم تجارت الکترونیک", - status: "شروع شده", - progress: 25, - }, - { - name: "سیستم مدیریت مالی", - status: "در حال بررسی", - progress: 10, - }, - ].map((project, index) => ( -
-
-
-

- {project.name} -

-

- {project.status} -

-
-
-
-
-
- - {project.progress}% - -
-
- ))} -
-
-
); diff --git a/app/components/dashboard/header.tsx b/app/components/dashboard/header.tsx index 5d39efb..32c7fb0 100644 --- a/app/components/dashboard/header.tsx +++ b/app/components/dashboard/header.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router"; import { cn } from "~/lib/utils"; import { Button } from "~/components/ui/button"; import { -PanelLeft, + PanelLeft, Search, Bell, Settings, @@ -26,7 +26,7 @@ interface HeaderProps { export function Header({ onToggleSidebar, className, - title = "داشبورد", + title = "صفحه اول", }: HeaderProps) { const { user } = useAuth(); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); @@ -54,7 +54,9 @@ export function Header({ )} {/* Page Title */} -

{title}

+

+ {title} +

{/* Right Section */} diff --git a/app/components/dashboard/interactive-bar-chart.tsx b/app/components/dashboard/interactive-bar-chart.tsx new file mode 100644 index 0000000..703e448 --- /dev/null +++ b/app/components/dashboard/interactive-bar-chart.tsx @@ -0,0 +1,125 @@ +import { Bar, BarChart, CartesianGrid, XAxis, YAxis, LabelList } from "recharts"; +import React, { useState, useEffect } from "react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "~/components/ui/chart"; + +export const description = "An interactive bar chart"; + +const chartData = [ + { category: "کیمیا", ideas: 12, revenue: 850, cost: 320 }, + { category: "ُفرآروزش", ideas: 19, revenue: 1200, cost: 450 }, + { category: "خوارزمی", ideas: 15, revenue: 1400, cost: 520 }, +]; + +const chartConfig = { + ideas: { + label: "ایده‌ها", + color: "#60A5FA", // Blue-400 + }, + revenue: { + label: "درآمد (میلیون)", + color: "#4ADE80", // Green-400 + }, + cost: { + label: "کاهش هزینه (میلیون)", + color: "#F87171", // Red-400 + }, +} satisfies ChartConfig; + +export function InteractiveBarChart() { + const [activeChart, setActiveChart] = + React.useState("ideas"); + + const total = React.useMemo( + () => ({ + ideas: chartData.reduce((acc, curr) => acc + curr.ideas, 0), + revenue: chartData.reduce((acc, curr) => acc + curr.revenue, 0), + cost: chartData.reduce((acc, curr) => acc + curr.cost, 0), + }), + [], + ); + + return ( + + + + + + + `${value}%`} + /> + + + + + + + + + + + + + + ); +} diff --git a/app/components/dashboard/project-management/digital-innovation-page.tsx b/app/components/dashboard/project-management/digital-innovation-page.tsx new file mode 100644 index 0000000..15a18df --- /dev/null +++ b/app/components/dashboard/project-management/digital-innovation-page.tsx @@ -0,0 +1,1097 @@ +import { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { DashboardLayout } from "../layout"; +import { Card, CardContent } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { Checkbox } from "~/components/ui/checkbox"; +import moment from "moment-jalaali"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; +import { + ChevronUp, + ChevronDown, + RefreshCw, + Building2, + PickaxeIcon, + UserIcon, + UsersIcon, +} from "lucide-react"; +import apiService from "~/lib/api"; +import toast from "react-hot-toast"; +import { + Database, + Zap, + TrendingDown, + TrendingUp, + Key, + Sprout, + BrainCircuit, + LoaderCircle, +} from "lucide-react"; +import { CustomBarChart } from "~/components/ui/custom-bar-chart"; + +moment.loadPersian({ usePersianDigits: true }); + +interface SortConfig { + field: string; + direction: "asc" | "desc"; +} + +interface StatsCard { + id: string; + title: string; + value: string; + description?: string; + icon: React.ReactNode; + color: string; +} + +// Raw API response interface for digital innovation metrics +interface DigitalInnovationMetrics { + count_innovation_digital_projects: string; + increased_revenue: string; + increased_revenue_percent: string; + reduce_costs: string; + reduce_costs_percent: string; + reduce_energy_consumption: string; + reduce_energy_consumption_percent: string; + resource_productivity: string; + resource_productivity_percent: string; +} + +// Normalized interface for digital innovation stats +interface DigitalInnovationStats { + // totalDigitalProjects: number; + increasedRevenue: number; + increasedRevenuePercent: number; + reduceCosts: number; + reduceCostsPercent: number; + reduceEnergyConsumption: number; + reduceEnergyConsumptionPercent: number; + resourceProductivity: number; + resourceProductivityPercent: number; +} + +enum DigitalCardLabel { + decreasCost = "کاهش هزینه‌ها", + increaseRevenue = "افزایش درآمد", + performance = "بهره‌وری منابع", + decreaseEnergy = "کاهش مصرف انرژی", +} + +enum projectStatus { + propozal = "پروپوزال", + contract = "پیشنویس قرارداد", + inprogress = "در حال انجام", + stop = "متوقف شده", + mafasa = "مرحله مفاصا", + finish = "پایان یافته", +} +interface ProcessInnovationData { + WorkflowID: number; + desired_strategy: string; + digital_capability: string; + digital_competence: string; + digital_puberty_elements: string; + innovation_cost_reduction: number | string; + operational_plan: string; + originality_digital_solution: string; + project_description: string; + project_no: string; + project_rating: number | string; + project_status: string; + reduce_costs_percent: number; + title: string; +} + +interface HouseItem { + index: number; + color?: string; + style?: string; +} + +interface ListItem { + label: string; + development: number; + house: HouseItem[]; +} + +const columns = [ + // { key: "select", label: "", sortable: false, width: "50px" }, + { key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" }, + { key: "title", label: "عنوان پروژه", sortable: true, width: "400px" }, + { + key: "project_status", + label: "وضعیت پروژه", + sortable: true, + width: "140px", + }, + { + key: "project_rating", + label: "امتیاز پروژه", + sortable: true, + width: "140px", + }, + { key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" }, +]; + +export function DigitalInnovationPage() { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingMore, setLoadingMore] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize] = useState(20); + const [hasMore, setHasMore] = useState(true); + const [totalCount, setTotalCount] = useState(0); + const [actualTotalCount, setActualTotalCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(false); + const [rating, setRating] = useState([]); + const [dialogInfo, setDialogInfo] = useState(); + const [stats, setStats] = useState({ + increasedRevenue: 0, + increasedRevenuePercent: 0, + reduceCosts: 0, + reduceCostsPercent: 0, + reduceEnergyConsumption: 0, + reduceEnergyConsumptionPercent: 0, + resourceProductivity: 0, + resourceProductivityPercent: 0, + }); + const [sortConfig, setSortConfig] = useState({ + field: "start_date", + direction: "asc", + }); + const [selectedProjects, setSelectedProjects] = useState>( + new Set() + ); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [avarage, setAvarage] = useState(0); + const observerRef = useRef(null); + const fetchingRef = useRef(false); + + // Selection handlers + const handleSelectAll = () => { + if (selectedProjects.size === projects.length) { + setSelectedProjects(new Set()); + } else { + setSelectedProjects(new Set(projects.map((p: any) => p.project_no))); + } + }; + + const handleProjectDetails = (project: ProcessInnovationData) => { + const model: ListItem = { + label: `فرآیند-${project.WorkflowID}`, + development: +project.project_rating, + house: [], + }; + setRating([model]); + setDialogInfo(project); + setDetailsDialogOpen(true); + }; + + const formatNumber = (value: string | number) => { + if (!value) return "0"; + const numericValue = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(numericValue)) return "0"; + return new Intl.NumberFormat("fa-IR").format(numericValue); + }; + + const statsCards: StatsCard[] = [ + { + id: "production-stops-prevention", + title: DigitalCardLabel.decreasCost, + value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts), + description: "میلیون ریال کاهش یافته", + icon: , + color: "text-emerald-400", + }, + { + id: "bottleneck-removal", + title: DigitalCardLabel.increaseRevenue, + value: formatNumber(stats.increasedRevenue), + description: "میلیون ریال افزایش یافته", + icon: , + color: "text-emerald-400", + }, + + { + id: "currency-reduction", + title: DigitalCardLabel.performance, + value: formatNumber( + stats.resourceProductivity.toFixed?.(0) ?? stats.resourceProductivity + ), + description: "هزار تن صرفه جوریی شده", + icon: , + color: "text-emerald-400", + }, + { + id: "frequent-failures-reduction", + title: DigitalCardLabel.decreaseEnergy, + value: formatNumber( + stats.reduceEnergyConsumption.toFixed?.(1) ?? + stats.reduceEnergyConsumption + ), + description: "مگاوات کاهش یافته", + icon: , + color: "text-emerald-400", + }, + ]; + + const fetchTable = async (reset = false) => { + if (fetchingRef.current) { + return; + } + + try { + fetchingRef.current = true; + + if (reset) { + setLoading(true); + setCurrentPage(1); + } else { + setLoadingMore(true); + } + + const pageToFetch = reset ? 1 : currentPage; + + const response = await apiService.select({ + ProcessName: "project", + OutputFields: [ + "project_no", + "title", + "project_status", + "project_rating", + "project_description", + "digital_competence", + "originality_digital_solution", + "digital_puberty_elements", + "digital_capability", + "operational_plan", + "desired_strategy", + "innovation_cost_reduction", + "reduce_costs_percent", + ], + Sorts: [[sortConfig.field, sortConfig.direction]], + Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]], + Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, + }); + + // console.log(JSON.parse(response.data)); + if (response.state === 0) { + const dataString = response.data; + if (dataString && typeof dataString === "string") { + try { + const parsedData: ProcessInnovationData = JSON.parse(dataString); + if (Array.isArray(parsedData)) { + if (reset) { + setProjects(parsedData); + calculateAverage(parsedData); + setTotalCount(parsedData.length); + } else { + setProjects((prev) => [...prev, ...parsedData]); + setTotalCount((prev) => prev + parsedData.length); + } + setHasMore(parsedData.length === pageSize); + } else { + if (reset) { + setProjects([]); + setTotalCount(0); + } + setHasMore(false); + } + } catch (parseError) { + console.error("Error parsing project data:", parseError); + if (reset) { + setProjects([]); + setTotalCount(0); + } + setHasMore(false); + } + } else { + if (reset) { + setProjects([]); + setTotalCount(0); + } + setHasMore(false); + } + } else { + toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها"); + if (reset) { + setProjects([]); + setTotalCount(0); + } + setHasMore(false); + } + } catch (error) { + console.error("Error fetching projects:", error); + toast.error("خطا در دریافت اطلاعات پروژه‌ها"); + if (reset) { + setProjects([]); + setTotalCount(0); + } + setHasMore(false); + } finally { + setLoading(false); + setLoadingMore(false); + + fetchingRef.current = false; + } + }; + + const loadMore = useCallback(() => { + if (!loadingMore && hasMore && !loading) { + setCurrentPage((prev) => prev + 1); + } + }, [loadingMore, hasMore, loading]); + + useEffect(() => { + fetchTable(true); + fetchTotalCount(); + fetchStats(); + }, [sortConfig]); + + useEffect(() => { + if (currentPage > 1) { + fetchTable(false); + } + }, [currentPage]); + + useEffect(() => { + const scrollContainer = document.querySelector(".overflow-auto"); + + const handleScroll = () => { + if (!scrollContainer || !hasMore || loadingMore) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + if (scrollPercentage >= 0.9) { + loadMore(); + } + }; + + if (scrollContainer) { + scrollContainer.addEventListener("scroll", handleScroll); + } + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener("scroll", handleScroll); + } + }; + }, [loadMore, hasMore, loadingMore]); + + const handleSort = (field: string) => { + fetchingRef.current = false; + setSortConfig((prev) => ({ + field, + direction: + prev.field === field && prev.direction === "asc" ? "desc" : "asc", + })); + fetchTotalCount(); + fetchStats(); + setCurrentPage(1); + setProjects([]); + setHasMore(true); + }; + + const fetchTotalCount = async () => { + try { + const response = await apiService.select({ + ProcessName: "project", + OutputFields: ["count(project_no)"], + Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]], + }); + + if (response.state === 0) { + const dataString = response.data; + if (dataString && typeof dataString === "string") { + try { + const parsedData = JSON.parse(dataString); + if (Array.isArray(parsedData) && parsedData[0]) { + const count = parsedData[0].project_no_count || 0; + setActualTotalCount(count); + // Keep stats in sync if backend stats not yet loaded + setStats((prev) => ({ ...prev, totalProjects: count })); + } + } catch (parseError) { + console.error("Error parsing count data:", parseError); + } + } + } + } catch (error) { + console.error("Error fetching total count:", error); + } + }; + + // Fetch aggregated stats from backend call API (innovation_process_function) + const fetchStats = async () => { + try { + setStatsLoading(true); + const raw = await apiService.callInnovationProcess({ + innovation_digital_function: {}, + }); + + let payload: DigitalInnovationMetrics = raw?.data; + if (typeof payload === "string") { + try { + payload = JSON.parse(payload); + } catch {} + } + + const parseNum = (v: unknown): number => { + if (v == null) return 0; + if (typeof v === "number") return v; + if (typeof v === "string") { + const cleaned = v.replace(/,/g, "").trim(); + const n = parseFloat(cleaned); + return isNaN(n) ? 0 : n; + } + return 0; + }; + const normalized: DigitalInnovationStats = { + increasedRevenue: parseNum(payload?.increased_revenue), + increasedRevenuePercent: parseNum(payload?.increased_revenue_percent), + reduceCosts: parseNum(payload?.reduce_costs), + reduceCostsPercent: parseNum(payload?.reduce_costs_percent), + reduceEnergyConsumption: parseNum(payload?.reduce_energy_consumption), + reduceEnergyConsumptionPercent: parseNum( + payload?.reduce_energy_consumption_percent + ), + resourceProductivity: parseNum(payload?.resource_productivity), + resourceProductivityPercent: parseNum( + payload?.resource_productivity_percent + ), + }; + + setStats(normalized); + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setStatsLoading(false); + } + }; + + // const handleRefresh = () => { + // fetchingRef.current = false; + // setCurrentPage(1); + // setProjects([]); + // setHasMore(true); + // fetchTable(true); + // fetchTotalCount(); + // fetchStats(); + // }; + + const formatCurrency = (amount: string | number) => { + if (!amount) return "0 ریال"; + const numericAmount = + typeof amount === "string" + ? parseFloat(amount.replace(/,/g, "")) + : amount; + if (isNaN(numericAmount)) return "0 ریال"; + return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; + }; + + const renderProgress = useMemo(() => { + const total = 10; + for (let i = 0; i < rating.length; i++) { + const currentElm = rating[i]; + currentElm.house = []; + const greenBoxes = Math.floor((total * currentElm.development) / 100); + const partialPercent = + (total * currentElm.development) / 100 - greenBoxes; + for (let j = 0; j < greenBoxes; j++) { + currentElm.house.push({ + index: j, + color: "!bg-emerald-400", + }); + } + if (partialPercent != 0 && greenBoxes != 10) + currentElm.house.push({ + index: greenBoxes + 1, + style: `linear-gradient( + to right, + oklch(76.5% 0.177 163.223) 0%, + oklch(76.5% 0.177 163.223) ${partialPercent * 100}%, + oklch(55.1% 0.027 264.364) ${partialPercent * 100}%, + oklch(55.1% 0.027 264.364) 100% + )`, + }); + } + }, [rating]); + + const ststusColor = (status: projectStatus): any => { + let el = null; + switch (status) { + case projectStatus.contract: + el = "teal"; + break; + case projectStatus.finish: + el = "info"; + break; + case projectStatus.stop: + el = "warning"; + break; + case projectStatus.inprogress: + el = "teal"; + break; + case projectStatus.mafasa: + el = "destructive"; + break; + case projectStatus.propozal: + el = "info"; + } + return el; + }; + + const renderCellContent = (item: any, column: any) => { + const value = item[column.key as keyof ProcessInnovationData]; + + switch (column.key) { + case "select": + return ( + handleSelectProject(item.project_no)} + className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" + /> + ); + case "details": + return ( + + ); + case "amount_currency_reduction": + return ( + + {formatCurrency(String(value))} + + ); + case "project_no": + return ( + + {String(value)} + + ); + case "title": + return {String(value)}; + case "project_status": + return ( +
+ + {String(value)} +
+ ); + case "project_rating": + return ( + + {formatNumber(String(value))} + + ); + case "reduce_prevention_production_stops": + case "throat_removal": + case "Reduce_rate_failure": + return ( + + {formatNumber(String(value))} + + ); + default: + return {String(value) || "-"}; + } + }; + + const calculateAverage = (data: Array) => { + let number = 0; + data.map( + (item: ProcessInnovationData) => (number = number + +item.project_rating) + ); + setAvarage(number / data.length); + }; + + return ( + +
+ {/* Stats Cards */} +
+
+ {/* Stats Grid */} +
+ {loading || statsLoading + ? // Loading skeleton for stats cards - matching new design + Array.from({ length: 4 }).map((_, index) => ( + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + )) + : statsCards.map((card) => ( + + +
+
+

+ {card.title} +

+
+ {card.icon} +
+
+
+

+ {card.value} +

+

+ {card.description} +

+
+
+
+
+ ))} +
+
+ + {/* Process Impacts Chart */} + + {/* */} + + {/* */} + +
+ + {/* Data Table */} + + +
+ + + + {columns.map((column) => ( + + {column.key === "select" ? ( +
+ 0 + } + onCheckedChange={handleSelectAll} + className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" + /> +
+ ) : column.sortable ? ( + + ) : ( + column.label + )} +
+ ))} +
+
+ + {loading ? ( + // Skeleton loading rows (compact) + Array.from({ length: 10 }).map((_, index) => ( + + {columns.map((column) => ( + +
+
+
+
+ + ))} + + )) + ) : projects.length === 0 ? ( + + + + هیچ پروژه‌ای یافت نشد + + + + ) : ( + projects.map((project, index) => ( + + {columns.map((column) => ( + + {renderCellContent(project, column)} + + ))} + + )) + )} + +
+
+ + {/* Infinite scroll trigger */} +
+ {loadingMore && ( +
+
+ + +
+
+ )} +
+
+ + {/* Footer */} +
+
+
+
+ کل پروژه ها :{formatNumber(actualTotalCount)} +
+
+ {/* Project number column - empty */} +
+ {/* Title column - empty */} +
+ {/* Project status column - empty */} +
+ + + + +
+ {/* Project rating column - show average */} +
+
+ میانگین امتیاز :‌ +
+
+ {formatNumber(((avarage ?? 0) as number).toFixed?.(1) ?? 0)} +
+
+
+
+
+
+ + {/* Project Details Dialog */} + + + + + شرح پروژه + + +
+
+ + {dialogInfo?.title} + +

+ {dialogInfo?.project_description} +

+
+ ویژگی های اصلی پروژه: +
+
+
+ + + شایستگی دیجیتال: + +
+ + {dialogInfo?.digital_capability} + +
+
+
+ + + اصالت راهکار دیجیتال: + +
+ + {dialogInfo?.digital_competence} + +
+
+
+ + + المان های بلوغ دیجیتال: + +
+ + {dialogInfo?.digital_puberty_elements} + +
+
+
+
+
+
+ + توسعه قابلیت های دیجیتال:{" "} + +
+
+ + + {dialogInfo?.digital_capability} + +
+
+
+ +
+ + برنامه های عملیاتی مرتبط: + +
+
+ + + {dialogInfo?.operational_plan} + +
+
+
+
+ + استراتژی های مورد نظر: + +
+
+ + + {dialogInfo?.desired_strategy} + +
+ {/*
+ + قابلیت شماره یک +
*/} + {/*
+ + قابلیت شماره یک +
*/} +
+
+
+
+
+
+ + کاهش هزینه ها + + +
+
+ + %{" "} + {formatNumber( + ( + Math.round( + dialogInfo?.reduce_costs_percent! * 100 + ) / 100 + ).toFixed(2) + )} + + + درصد به کل هزینه ها + +
+ +
+ + {formatNumber(+dialogInfo?.innovation_cost_reduction!)} + + + میلیون ریال + +
+
+
+
+
+
+ عنوان فرآیند + درصد پیشرفت +
+
+ {rating.map((el, index) => { + return ( +
+ {el.label} +
+ {Array.from({ length: 10 }, (_, i) => { + return ( + + ); + })} +
+
+ ); + })} +
+
+
+
+
+
+ + ); +} + +export default DigitalInnovationPage; diff --git a/app/components/dashboard/project-management/process-innovation-page.tsx b/app/components/dashboard/project-management/process-innovation-page.tsx index 0190f3b..6d52d71 100644 --- a/app/components/dashboard/project-management/process-innovation-page.tsx +++ b/app/components/dashboard/project-management/process-innovation-page.tsx @@ -34,7 +34,6 @@ import { import apiService from "~/lib/api"; import toast from "react-hot-toast"; import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react"; -import ProjectDetail from "../projects/project-detail"; moment.loadPersian({ usePersianDigits: true }); interface ProcessInnovationData { @@ -150,7 +149,6 @@ export function ProcessInnovationPage() { }; const handleProjectDetails = (project: ProcessInnovationData) => { - console.log(project); setSelectedProjectDetails(project); setDetailsDialogOpen(true); }; @@ -169,9 +167,9 @@ export function ProcessInnovationPage() { title: "جلوگیری از توقفات تولید", value: formatNumber( stats.productionStopsPreventionSum.toFixed?.(1) ?? - stats.productionStopsPreventionSum, + stats.productionStopsPreventionSum, ), - description: "ظرفیت افزایش یافته", + description: "تن افزایش یافته", icon: , color: "text-emerald-400", }, @@ -196,10 +194,10 @@ export function ProcessInnovationPage() { }, { id: "frequent-failures-reduction", - title: "کاهش خرابیهای پرتکرار", + title: "کاهش خرابی های پرتکرار", value: formatNumber( stats.frequentFailuresReductionSum.toFixed?.(1) ?? - stats.frequentFailuresReductionSum, + stats.frequentFailuresReductionSum, ), description: "مجموع درصد کاهش خرابی", icon: , @@ -394,7 +392,7 @@ export function ProcessInnovationPage() { const fetchStats = async () => { try { setStatsLoading(true); - const raw = await apiService.callInnovationProcess({ + const raw = await apiService.call({ innovation_process_function: {}, }); @@ -402,7 +400,7 @@ export function ProcessInnovationPage() { if (typeof payload === "string") { try { payload = JSON.parse(payload); - } catch {} + } catch { } } const parseNum = (v: unknown): number => { @@ -575,73 +573,73 @@ export function ProcessInnovationPage() {
{loading || statsLoading ? // Loading skeleton for stats cards - matching new design - Array.from({ length: 4 }).map((_, index) => ( - - -
-
-
-
-
-
-
-
-
-
+ Array.from({ length: 4 }).map((_, index) => ( + + +
+
+
+
+
- - - )) +
+
+
+
+
+ + + )) : statsCards.map((card) => ( - - -
-
-

- {card.title} -

-
- {card.icon} -
-
-
-

- {card.value} -

-

- {card.description} -

+ + +
+
+

+ {card.title} +

+
+ {card.icon}
- - - ))} +
+

+ {card.value} +

+

+ {card.description} +

+
+
+
+
+ ))}
{/* Process Impacts Chart */} - + {formatNumber( ((stats.averageScore ?? 0) as number).toFixed?.(1) ?? - stats.averageScore ?? - 0, + stats.averageScore ?? + 0, )}
@@ -883,9 +881,9 @@ export function ProcessInnovationPage() { {selectedProjectDetails?.start_date ? moment( - selectedProjectDetails?.start_date, - "YYYY-MM-DD", - ).format("YYYY/MM/DD") + selectedProjectDetails?.start_date, + "YYYY-MM-DD", + ).format("YYYY/MM/DD") : "-"}
@@ -898,9 +896,9 @@ export function ProcessInnovationPage() { {selectedProjectDetails?.done_date ? moment( - selectedProjectDetails?.done_date, - "YYYY-MM-DD", - ).format("YYYY/MM/DD") + selectedProjectDetails?.done_date, + "YYYY-MM-DD", + ).format("YYYY/MM/DD") : "-"}
diff --git a/app/components/dashboard/sidebar.tsx b/app/components/dashboard/sidebar.tsx index 44e887b..717f204 100644 --- a/app/components/dashboard/sidebar.tsx +++ b/app/components/dashboard/sidebar.tsx @@ -149,21 +149,21 @@ export function Sidebar({ React.useEffect(() => { const autoExpandParents = () => { const newExpandedItems: string[] = []; - + menuItems.forEach((item) => { if (item.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href + (child) => child.href && location.pathname === child.href, ); if (hasActiveChild) { newExpandedItems.push(item.id); } } }); - + setExpandedItems(newExpandedItems); }; - + autoExpandParents(); }, [location.pathname]); @@ -171,10 +171,10 @@ export function Sidebar({ setExpandedItems((prev) => { // If trying to collapse, check if any child is active if (prev.includes(itemId)) { - const item = menuItems.find(menuItem => menuItem.id === itemId); + const item = menuItems.find((menuItem) => menuItem.id === itemId); if (item?.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href + (child) => child.href && location.pathname === child.href, ); // Don't collapse if a child is active if (hasActiveChild) { @@ -200,10 +200,12 @@ export function Sidebar({ const renderMenuItem = (item: MenuItem, level = 0) => { const isActive = isActiveRoute(item.href, item.children); - const isExpanded = expandedItems.includes(item.id) || - (item.children && item.children.some(child => - child.href && location.pathname === child.href - )); + const isExpanded = + expandedItems.includes(item.id) || + (item.children && + item.children.some( + (child) => child.href && location.pathname === child.href, + )); const hasChildren = item.children && item.children.length > 0; const ItemIcon = item.icon; @@ -228,7 +230,8 @@ export function Sidebar({ ? " text-emerald-400 border-r-2 border-emerald-400" : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", isCollapsed && level === 0 && "justify-center px-2", - item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400", + item.id === "logout" && + "hover:bg-red-500/10 hover:text-red-400", )} >
@@ -265,14 +268,16 @@ export function Sidebar({
) : ( - )} - {/* Submenu */} {hasChildren && isExpanded && !isCollapsed && (
{item.children?.map((child) => renderMenuItem(child, level + 1))}
)} - {/* Tooltip for collapsed state */} {isCollapsed && level === 0 && (
@@ -361,21 +369,26 @@ export function Sidebar({ {!isCollapsed ? (
- سیستم اینوژن + داشبورد اینوژن
نسخه ۰.۱
) : (
- +
)}
diff --git a/app/components/ecosystem/info-panel.tsx b/app/components/ecosystem/info-panel.tsx index c455b4d..d373924 100644 --- a/app/components/ecosystem/info-panel.tsx +++ b/app/components/ecosystem/info-panel.tsx @@ -68,15 +68,17 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) { setIsLoading(true); try { const [countsRes, processRes] = await Promise.all([ - apiService.callInnovationProcess({ - ecosystem_counts_function: {}, + apiService.call({ + ecosystem_count_function: {}, }), - apiService.callInnovationProcess({ + apiService.call({ process_creating_actors_function: {}, }), ]); - setCounts(JSON.parse(countsRes.data)); + setCounts( + JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0], + ); // Process the years data and fill missing years const processedData = processYearsData( @@ -164,7 +166,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) { }, { label: "شتابدهنده", value: parseNumber(counts.accelerator_count) }, { label: "دانشگاه", value: parseNumber(counts.university_count) }, - { label: "صندوق", value: parseNumber(counts.fund_count) }, + { label: "صندوق های مالی", value: parseNumber(counts.fund_count) }, { label: "شرکت", value: parseNumber(counts.company_count) }, ] : []; @@ -404,6 +406,23 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) { + {/* Footer - MOU Count */} + {/* +
+ تعداد تفاهم نامه ها + {formatNumber(counts.mou_count)} +
+
*/} + + + + تعداد تفاهم نامه ها + + {formatNumber(counts.mou_count)} + + + + تعداد بازیگران @@ -495,13 +514,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
- {/* Footer - MOU Count */} - -
- تعداد تفاهم نامه ها - {formatNumber(counts.mou_count)} -
-
); diff --git a/app/components/ecosystem/network-graph.tsx b/app/components/ecosystem/network-graph.tsx index 4a82b98..88c3a71 100644 --- a/app/components/ecosystem/network-graph.tsx +++ b/app/components/ecosystem/network-graph.tsx @@ -88,7 +88,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { (async () => { setIsLoading(true); try { - const res = await apiService.callInnovationProcess({ + const res = await apiService.call({ graph_production_function: {}, }); if (aborted) return; @@ -102,7 +102,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { // Create center node const centerNode: Node = { id: "center", - label: "مرکز اکوسیستم", + label: "", //مرکز زیست بوم category: "center", stageid: 0, isCenter: true, @@ -155,7 +155,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { // Import apiService for the onClick handler const callAPI = useCallback(async (stage_id: number) => { - return await apiService.callInnovationProcess({ + return await apiService.call({ get_values_workflow_function: { stage_id: stage_id, }, @@ -317,7 +317,8 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { .attr("ry", 8) .attr("fill", categoryToColor[d.category] || "#94A3B8") .attr("stroke", "#FFFFFF") - .attr("stroke-width", 3); + .attr("stroke-width", 3) + .style("pointer-events", "none"); // Add center image if available if (d.imageUrl || d.isCenter) { @@ -399,6 +400,7 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { // Add hover effects nodeGroup .on("mouseenter", function (event, d) { + if (d.isCenter) return; d3.select(this) .select(d.isCenter ? "rect" : "circle") .attr("filter", "url(#glow)") @@ -419,7 +421,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { // Add click handlers nodeGroup.on("click", async function (event, d) { - setIsLoading(true); event.stopPropagation(); // Don't handle center node clicks @@ -471,7 +472,6 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { onNodeClick(basicDetails); } } - setIsLoading(false); }); // Update positions on simulation tick diff --git a/app/components/ui/chart.tsx b/app/components/ui/chart.tsx new file mode 100644 index 0000000..12c0d60 --- /dev/null +++ b/app/components/ui/chart.tsx @@ -0,0 +1,351 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "~/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] +}) { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +