Compare commits

...

4 Commits

Author SHA1 Message Date
MehrdadAdabi
0dd1fe2ec2 Merge branch 'main' of http://git.sepehrdata.com/Saeed0920/inogen 2025-10-13 02:44:54 +03:30
MehrdadAdabi
efa46a02c2 fix: change date picker logic to another pages 2025-10-12 21:30:13 +03:30
mahmoodsht
bda2e62411 ... 2025-10-12 12:51:51 +03:30
MehrdadAdabi
173176bbb5 feat: completed designed 2025-10-10 19:11:38 +03:30
16 changed files with 2339 additions and 1479 deletions

View File

@ -1,38 +1,7 @@
import { useState, useEffect } from "react"; import jalaali from "jalaali-js";
import { DashboardLayout } from "./layout"; import { Book, CheckCircle } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { useEffect, useState } from "react";
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 toast from "react-hot-toast";
import {
Calendar,
TrendingUp,
TrendingDown,
Target,
Lightbulb,
DollarSign,
Minus,
CheckCircle,
Book,
} 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 { import {
Label, Label,
PolarGrid, PolarGrid,
@ -40,26 +9,53 @@ import {
RadialBar, RadialBar,
RadialBarChart, RadialBarChart,
} from "recharts"; } from "recharts";
import { ChartContainer } from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils";
import { MetricCard } from "~/components/ui/metric-card";
import { BaseCard } from "~/components/ui/base-card"; import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { ChartContainer } from "~/components/ui/chart";
import { MetricCard } from "~/components/ui/metric-card";
import { Progress } from "~/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { D3ImageInfo } from "./d3-image-info";
import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart";
import { InteractiveBarChart } from "./interactive-bar-chart";
import { DashboardLayout } from "./layout";
export function DashboardHome() { export function DashboardHome() {
const { jy } = jalaali.toJalaali(new Date());
const [dashboardData, setDashboardData] = useState<any | null>(null); const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Chart and schematic data from select API // Chart and schematic data from select API
const [companyChartData, setCompanyChartData] = useState< const [companyChartData, setCompanyChartData] = useState<
{ category: string; capacity: number; revenue: number; cost: number , costI : number, {
capacityI : number, category: string;
revenueI : number }[] capacity: number;
revenue: number;
cost: number;
costI: number;
capacityI: number;
revenueI: number;
}[]
>([]); >([]);
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) setDate(date);
});
}, []);
useEffect(() => { useEffect(() => {
fetchDashboardData(); fetchDashboardData();
}, []); }, [date]);
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
try { try {
@ -68,12 +64,18 @@ export function DashboardHome() {
// Fetch top cards data // Fetch top cards data
const topCardsResponse = await apiService.call({ const topCardsResponse = await apiService.call({
main_page_first_function: {}, main_page_first_function: {
start_date: date.start || null,
end_date: date.end || null,
},
}); });
// Fetch left section data // Fetch left section data
const leftCardsResponse = await apiService.call({ const leftCardsResponse = await apiService.call({
main_page_second_function: {}, main_page_second_function: {
start_date: date.start || null,
end_date: date.end || null,
},
}); });
const topCardsResponseData = JSON.parse(topCardsResponse?.data); const topCardsResponseData = JSON.parse(topCardsResponse?.data);
@ -106,6 +108,10 @@ export function DashboardHome() {
"sum(pre_project_income)", "sum(pre_project_income)",
"sum(increased_income_after_innovation)", "sum(increased_income_after_innovation)",
], ],
Conditions: [
["start_date", ">=", date.start || null, "and"],
["start_date", "<=", date.end || null],
],
GroupBy: ["related_company"], GroupBy: ["related_company"],
}; };
@ -124,12 +130,30 @@ export function DashboardHome() {
let incCapacityTotal = 0; let incCapacityTotal = 0;
const chartRows = rows.map((r) => { const chartRows = rows.map((r) => {
const rel = r?.related_company ?? "-"; const rel = r?.related_company ?? "-";
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0; const preFee =
const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0; Number(r?.pre_innovation_fee_sum ?? 0) >= 0
const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0; ? r?.pre_innovation_fee_sum
const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0; : 0;
const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0; const costRed =
const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 0; Number(r?.innovation_cost_reduction_sum ?? 0) >= 0
? r?.innovation_cost_reduction_sum
: 0;
const preCap =
Number(r?.pre_project_production_capacity_sum ?? 0) >= 0
? r?.pre_project_production_capacity_sum
: 0;
const incCap =
Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0
? r?.increased_capacity_after_innovation_sum
: 0;
const preInc =
Number(r?.pre_project_income_sum ?? 0) >= 0
? r?.pre_project_income_sum
: 0;
const incInc =
Number(r?.increased_income_after_innovation_sum ?? 0) >= 0
? r?.increased_income_after_innovation_sum
: 0;
incCapacityTotal += incCap; incCapacityTotal += incCap;
@ -141,14 +165,14 @@ export function DashboardHome() {
capacity: isFinite(capacityPct) ? capacityPct : 0, capacity: isFinite(capacityPct) ? capacityPct : 0,
revenue: isFinite(revenuePct) ? revenuePct : 0, revenue: isFinite(revenuePct) ? revenuePct : 0,
cost: isFinite(costPct) ? costPct : 0, cost: isFinite(costPct) ? costPct : 0,
costI : costRed, costI: costRed,
capacityI : incCap, capacityI: incCap,
revenueI : incInc revenueI: incInc,
}; };
}); });
setCompanyChartData(chartRows); setCompanyChartData(chartRows);
setTotalIncreasedCapacity(incCapacityTotal); // setTotalIncreasedCapacity(incCapacityTotal);
} catch (error) { } catch (error) {
console.error("Error fetching dashboard data:", error); console.error("Error fetching dashboard data:", error);
const errorMessage = const errorMessage =
@ -161,25 +185,24 @@ export function DashboardHome() {
}; };
// RadialBarChart data for ideas visualization // RadialBarChart data for ideas visualization
const getIdeasChartData = () => { // const getIdeasChartData = () => {
if (!dashboardData?.topData) // if (!dashboardData?.topData)
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }]; // return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
const registered = parseFloat( // const registered = parseFloat(
dashboardData.topData.registered_innovation_technology_idea || "0", // dashboardData.topData.registered_innovation_technology_idea || "0"
); // );
const ongoing = parseFloat( // const ongoing = parseFloat(
dashboardData.topData.ongoing_innovation_technology_ideas || "0", // dashboardData.topData.ongoing_innovation_technology_ideas || "0"
); // );
const percentage = // const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
registered > 0 ? (ongoing / registered) * 100 : 0;
return [ // return [
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" }, // { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
]; // ];
}; // };
const chartData = getIdeasChartData(); // const chartData = getIdeasChartData();
const chartConfig = { const chartConfig = {
visitors: { visitors: {
@ -323,20 +346,19 @@ export function DashboardHome() {
visitors: visitors:
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || "0", ?.registered_innovation_technology_idea || "0"
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
dashboardData.topData dashboardData.topData
?.ongoing_innovation_technology_ideas || ?.ongoing_innovation_technology_ideas || "0"
"0",
) / ) /
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || ?.registered_innovation_technology_idea ||
"1", "1"
)) * )) *
100, 100
) )
: 0, : 0,
fill: "var(--color-green)", fill: "var(--color-green)",
@ -347,19 +369,18 @@ export function DashboardHome() {
90 + 90 +
((parseFloat( ((parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || "0", ?.registered_innovation_technology_idea || "0"
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
dashboardData.topData dashboardData.topData
?.ongoing_innovation_technology_ideas || "0", ?.ongoing_innovation_technology_ideas || "0"
) / ) /
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || ?.registered_innovation_technology_idea || "1"
"1",
)) * )) *
100, 100
) )
: 0) / : 0) /
100) * 100) *
@ -375,11 +396,7 @@ export function DashboardHome() {
className="first:fill-pr-red last:fill-[#24273A]" className="first:fill-pr-red last:fill-[#24273A]"
polarRadius={[38, 31]} polarRadius={[38, 31]}
/> />
<RadialBar <RadialBar dataKey="visitors" background cornerRadius={5} />
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis <PolarRadiusAxis
tick={false} tick={false}
tickLine={false} tickLine={false}
@ -405,22 +422,22 @@ export function DashboardHome() {
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || ?.registered_innovation_technology_idea ||
"0", "0"
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
dashboardData.topData dashboardData.topData
?.ongoing_innovation_technology_ideas || ?.ongoing_innovation_technology_ideas ||
"0", "0"
) / ) /
parseFloat( parseFloat(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || ?.registered_innovation_technology_idea ||
"1", "1"
)) * )) *
100, 100
) )
: 0, : 0
)} )}
</tspan> </tspan>
</text> </text>
@ -437,14 +454,14 @@ export function DashboardHome() {
<div className="font-light text-sm">ثبت شده :</div> <div className="font-light text-sm">ثبت شده :</div>
{formatNumber( {formatNumber(
dashboardData.topData dashboardData.topData
?.registered_innovation_technology_idea || "0", ?.registered_innovation_technology_idea || "0"
)} )}
</span> </span>
<span className="flex items-center gap-1 font-bold text-base"> <span className="flex items-center gap-1 font-bold text-base">
<div className="font-light text-sm">در حال اجرا :</div> <div className="font-light text-sm">در حال اجرا :</div>
{formatNumber( {formatNumber(
dashboardData.topData dashboardData.topData
?.ongoing_innovation_technology_ideas || "0", ?.ongoing_innovation_technology_ideas || "0"
)} )}
</span> </span>
</div> </div>
@ -454,130 +471,144 @@ export function DashboardHome() {
{/* Revenue Card */} {/* Revenue Card */}
<MetricCard <MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری" title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll("," , "") || "0"} value={
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent} dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
",",
""
) || "0"
}
percentValue={
dashboardData.topData
?.technology_innovation_based_revenue_growth_percent
}
percentLabel="درصد به کل درآمد" percentLabel="درصد به کل درآمد"
/> />
{/* Cost Reduction Card */} {/* Cost Reduction Card */}
<MetricCard <MetricCard
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری" title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0"))} value={Math.round(
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"} parseFloat(
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
""
) || "0"
)
)}
percentValue={
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent || "0"
}
percentLabel="درصد به کل هزینه" percentLabel="درصد به کل هزینه"
/> />
{/* Budget Ratio Card */} {/* Budget Ratio Card */}
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری"> <BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
<div className="flex items-center gap-2 justify-center flex-row-reverse"> <div className="flex items-center gap-2 justify-center flex-row-reverse">
<ChartContainer <ChartContainer
config={chartConfig} config={chartConfig}
className="aspect-square w-[6rem] h-auto" className="aspect-square w-[6rem] h-auto"
>
<RadialBarChart
data={[
{
browser: "budget",
visitors: parseFloat(
dashboardData.topData
?.innovation_budget_achievement_percent || "0"
),
fill: "var(--color-green)",
},
]}
startAngle={90}
endAngle={
90 +
(dashboardData.topData
?.innovation_budget_achievement_percent /
100) *
360
}
innerRadius={35}
outerRadius={55}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-pr-red last:fill-[#24273A]"
polarRadius={[38, 31]}
/>
<RadialBar dataKey="visitors" background cornerRadius={5} />
<PolarRadiusAxis
tick={false}
tickLine={false}
axisLine={false}
> >
<RadialBarChart <Label
data={[ content={({ viewBox }) => {
{ if (viewBox && "cx" in viewBox && "cy" in viewBox) {
browser: "budget", return (
visitors: parseFloat( <text
dashboardData.topData x={viewBox.cx}
?.innovation_budget_achievement_percent || "0", y={viewBox.cy}
), textAnchor="middle"
fill: "var(--color-green)", dominantBaseline="middle"
}, >
]} <tspan
startAngle={90} x={viewBox.cx}
endAngle={ y={viewBox.cy}
90 + className="fill-foreground text-lg font-bold"
(dashboardData.topData >
?.innovation_budget_achievement_percent / %
100) * {formatNumber(
360 Math.round(
} dashboardData.topData
innerRadius={35} ?.innovation_budget_achievement_percent ||
outerRadius={55} 0
> )
<PolarGrid )}
gridType="circle" </tspan>
radialLines={false} </text>
stroke="none" );
className="first:fill-pr-red last:fill-[#24273A]" }
polarRadius={[38, 31]} }}
/> />
<RadialBar </PolarRadiusAxis>
dataKey="visitors" </RadialBarChart>
background </ChartContainer>
cornerRadius={5} <div className="font-bold font-persian text-center">
/> <div className="flex flex-col justify-between items-center gap-2">
<PolarRadiusAxis <span className="flex font-bold items-center text-base gap-1 mr-auto">
tick={false} <div className="font-light text-sm">مصوب :</div>
tickLine={false} {formatNumber(
axisLine={false} Math.round(
> parseFloat(
<Label dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
content={({ viewBox }) => { /,/g,
if (viewBox && "cx" in viewBox && "cy" in viewBox) { ""
return ( ) || "0"
<text )
x={viewBox.cx} )
y={viewBox.cy} )}
textAnchor="middle" </span>
dominantBaseline="middle" <span className="flex items-center gap-1 text-base font-bold mr-auto">
> <div className="font-light text-sm">جذب شده :</div>
<tspan {formatNumber(
x={viewBox.cx} Math.round(
y={viewBox.cy} parseFloat(
className="fill-foreground text-lg font-bold" dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
> /,/g,
% ""
{formatNumber( ) || "0"
Math.round( )
dashboardData.topData )
?.innovation_budget_achievement_percent || )}
0, </span>
),
)}
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
<div className="font-bold font-persian text-center">
<div className="flex flex-col justify-between items-center gap-2">
<span className="flex font-bold items-center text-base gap-1 mr-auto">
<div className="font-light text-sm">مصوب :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
/,/g,
"",
) || "0",
),
),
)}
</span>
<span className="flex items-center gap-1 text-base font-bold mr-auto">
<div className="font-light text-sm">جذب شده :</div>
{formatNumber(
Math.round(
parseFloat(
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
/,/g,
"",
) || "0",
),
),
)}
</span>
</div>
</div>
</div> </div>
</BaseCard> </div>
</div> </div>
</BaseCard>
</div>
{/* Main Content with Tabs */} {/* Main Content with Tabs */}
<Tabs <Tabs
@ -589,10 +620,13 @@ export function DashboardHome() {
تحقق ارزش ها تحقق ارزش ها
</p> </p>
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]"> <TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
<TabsTrigger value="canvas" className="cursor-pointer"> <TabsTrigger value="canvas" className="cursor-pointer">
شماتیک شماتیک
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="charts" className=" text-white cursor-pointer font-light "> <TabsTrigger
value="charts"
className=" text-white cursor-pointer font-light "
>
مقایسه ای مقایسه ای
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -605,27 +639,25 @@ export function DashboardHome() {
<TabsContent value="canvas" className="w-ful h-full"> <TabsContent value="canvas" className="w-ful h-full">
<div className="p-4 h-full w-full"> <div className="p-4 h-full w-full">
<D3ImageInfo <D3ImageInfo
companies={ companies={companyChartData.map((item) => {
companyChartData.map((item) => { const imageMap: Record<string, string> = {
const imageMap: Record<string, string> = { بسپاران: "/besparan.png",
"بسپاران": "/besparan.png", خوارزمی: "/khwarazmi.png",
"خوارزمی": "/khwarazmi.png", "فراورش 1": "/faravash1.png",
"فراورش 1": "/faravash1.png", "فراورش 2": "/faravash2.png",
"فراورش 2": "/faravash2.png", کیمیا: "/kimia.png",
"کیمیا": "/kimia.png", "آب نیرو": "/abniro.png",
"آب نیرو": "/abniro.png", };
};
return { return {
id: item.category, id: item.category,
name: item.category, name: item.category,
imageUrl: imageMap[item.category] || "/placeholder.png", imageUrl: imageMap[item.category] || "/placeholder.png",
cost: item?.costI || 0, cost: item?.costI || 0,
capacity: item?.capacityI || 0, capacity: item?.capacityI || 0,
revenue: item?.revenueI || 0, revenue: item?.revenueI || 0,
}; };
}) })}
}
/> />
</div> </div>
</TabsContent> </TabsContent>
@ -643,7 +675,7 @@ export function DashboardHome() {
<Progress <Progress
value={parseFloat( value={parseFloat(
dashboardData.leftData?.technology_intensity, dashboardData.leftData?.technology_intensity
)} )}
className="h-4 flex-1" className="h-4 flex-1"
/> />
@ -661,21 +693,21 @@ export function DashboardHome() {
{ {
label: "اجرا شده", label: "اجرا شده",
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.executed_project || "0", dashboardData?.leftData?.executed_project || "0"
), ),
color: "bg-pr-green", color: "bg-pr-green",
}, },
{ {
label: "در حال اجرا", label: "در حال اجرا",
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.in_progress_project || "0", dashboardData?.leftData?.in_progress_project || "0"
), ),
color: "bg-pr-blue", color: "bg-pr-blue",
}, },
{ {
label: "برنامه‌ریزی شده", label: "برنامه‌ریزی شده",
value: parseFloat( value: parseFloat(
dashboardData?.leftData?.planned_project || "0", dashboardData?.leftData?.planned_project || "0"
), ),
color: "bg-pr-red", color: "bg-pr-red",
}, },
@ -700,7 +732,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.printed_books_count || "0", dashboardData.leftData?.printed_books_count || "0"
)} )}
</span> </span>
</div> </div>
@ -711,7 +743,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.registered_patents_count || "0", dashboardData.leftData?.registered_patents_count || "0"
)} )}
</span> </span>
</div> </div>
@ -722,7 +754,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.published_reports_count || "0", dashboardData.leftData?.published_reports_count || "0"
)} )}
</span> </span>
</div> </div>
@ -733,7 +765,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.printed_articles_count || "0", dashboardData.leftData?.printed_articles_count || "0"
)} )}
</span> </span>
</div> </div>
@ -757,7 +789,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_conferences_count || "0", dashboardData.leftData?.attended_conferences_count || "0"
)} )}
</span> </span>
</div> </div>
@ -768,7 +800,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_events_count || "0", dashboardData.leftData?.attended_events_count || "0"
)} )}
</span> </span>
</div> </div>
@ -779,7 +811,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.attended_exhibitions_count || "0", dashboardData.leftData?.attended_exhibitions_count || "0"
)} )}
</span> </span>
</div> </div>
@ -790,7 +822,7 @@ export function DashboardHome() {
</div> </div>
<span className="text-base font-bold "> <span className="text-base font-bold ">
{formatNumber( {formatNumber(
dashboardData.leftData?.organized_events_count || "0", dashboardData.leftData?.organized_events_count || "0"
)} )}
</span> </span>
</div> </div>
@ -798,9 +830,8 @@ export function DashboardHome() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@ -1,21 +1,20 @@
import React, { useEffect, useState } from "react"; import jalaali from "jalaali-js";
import { useAuth } from "~/contexts/auth-context";
import { Link } from "react-router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import { import {
Calendar,
ChevronLeft,
Menu,
PanelLeft, PanelLeft,
Server,
Settings, Settings,
User, User,
Menu,
ChevronDown,
Server,
ChevronLeft ,
} from "lucide-react"; } from "lucide-react";
import React, { useEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { Button } from "~/components/ui/button";
import { Calendar as CustomCalendar } from "~/components/ui/Calendar";
import { useAuth } from "~/contexts/auth-context";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { cn, EventBus } from "~/lib/utils";
interface HeaderProps { interface HeaderProps {
onToggleSidebar?: () => void; onToggleSidebar?: () => void;
@ -24,6 +23,52 @@ interface HeaderProps {
titleIcon?: React.ComponentType<{ className?: string }> | null; titleIcon?: React.ComponentType<{ className?: string }> | null;
} }
interface MonthItem {
id: string;
label: string;
start: string;
end: string;
}
interface CurrentDay {
start?: string;
end?: string;
sinceMonth?: string;
fromMonth?: string;
}
interface SelectedDate {
since?: number;
until?: number;
}
const monthList: Array<MonthItem> = [
{
id: "month-1",
label: "بهار",
start: "01/01",
end: "03/31",
},
{
id: "month-2",
label: "تابستان",
start: "04/01",
end: "06/31",
},
{
id: "month-3",
label: "پاییز",
start: "07/01",
end: "09/31",
},
{
id: "month-4",
label: "زمستان",
start: "10/01",
end: "12/29",
},
];
export function Header({ export function Header({
onToggleSidebar, onToggleSidebar,
className, className,
@ -31,25 +76,131 @@ export function Header({
titleIcon, titleIcon,
}: HeaderProps) { }: HeaderProps) {
const { user } = useAuth(); const { user } = useAuth();
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); const { jy } = jalaali.toJalaali(new Date());
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
const calendarRef = useRef<HTMLDivElement>(null);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
const [currentYear, setCurrentYear] = useState<SelectedDate>({
since: jy,
until: jy,
});
const [selectedDate, setSelectedDate] = useState<CurrentDay>({
sinceMonth: "بهار",
fromMonth: "زمستان",
start: `${currentYear.since}/01/01`,
end: `${currentYear.until}/12/30`,
});
const redirectHandler = async () => { const redirectHandler = async () => {
try { try {
const getData = await apiService.post('/GenerateSsoCode') const getData = await apiService.post("/GenerateSsoCode");
//const url = `http://localhost:3000/redirect/${getData.data}`; const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
window.open(url, "_blank"); window.open(url, "_blank");
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
} };
const nextFromYearHandler = () => {
if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) {
const data = {
...currentYear,
since: currentYear.since! + 1,
};
setCurrentYear(data);
EventBus.emit("dateSelected", {
...selectedDate,
start: `${data.since}/${selectedDate.start?.split("/").slice(1).join("/")}`,
});
}
};
const prevFromYearHandler = () => {
const data = {
...currentYear,
since: currentYear.since! - 1,
};
setCurrentYear(data);
EventBus.emit("dateSelected", {
...selectedDate,
start: `${data.since}/${selectedDate.start?.split("/").slice(1).join("/")}`,
});
};
const selectFromDateHandler = (val: MonthItem) => {
const data = {
...selectedDate,
start: `${currentYear.since}/${val.start}`,
sinceMonth: val.label,
};
setSelectedDate(data);
EventBus.emit("dateSelected", data);
};
const nextUntilYearHandler = () => {
const data = {
...currentYear,
until: currentYear.until! + 1,
};
setCurrentYear(data);
EventBus.emit("dateSelected", {
...selectedDate,
end: `${data.until}/${selectedDate?.end?.split("/").slice(1).join("/")}`,
});
};
const prevUntilYearHandler = () => {
if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) {
const data = {
...currentYear,
until: currentYear.until! - 1,
};
setCurrentYear(data);
EventBus.emit("dateSelected", {
...selectedDate,
end: `${data.until}/${selectedDate.end?.split("/").slice(1).join("/")}`,
});
}
};
const selectUntilDateHandler = (val: MonthItem) => {
const data = {
...selectedDate,
end: `${currentYear.until}/${val.end}`,
fromMonth: val.label,
};
setSelectedDate(data);
EventBus.emit("dateSelected", data);
toggleCalendar();
};
const toggleCalendar = () => {
setOpenCalendar(!openCalendar);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
calendarRef.current &&
!calendarRef.current.contains(event.target as Node)
) {
setOpenCalendar(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return ( return (
<header <header
className={cn( className={cn(
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30", "backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
className, className
)} )}
> >
{/* Left Section */} {/* Left Section */}
@ -69,24 +220,74 @@ export function Header({
{/* Page Title */} {/* Page Title */}
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian"> <h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
{/* Right-side icon for current page */} {/* Right-side icon for current page */}
{titleIcon ? ( {titleIcon ? (
<div className="flex items-center gap-2 mr-4"> <div className="flex items-center gap-2 mr-4">
{React.createElement(titleIcon, { className: "w-5 h-5 " })} {React.createElement(titleIcon, { className: "w-5 h-5 " })}
</div> </div>
) : ( ) : (
<PanelLeft /> <PanelLeft />
)}
{title.includes("-") ? (
<div className="flex row items-center gap-4">
<div className="flex items-center gap-1">
{title.split("-")[0]}
<ChevronLeft className="inline-block w-4 h-4" />
{title.split("-")[1]}
</div>
</div>
) : (
title
)} )}
{title.includes("-") ? (
<span className="flex items-center gap-1">
{title.split("-")[0]}
<ChevronLeft className="inline-block w-4 h-4" />
{title.split("-")[1]}
</span>
) : (
title
)}
</h1> </h1>
<div ref={calendarRef} className="flex flex-col gap-3 relative">
<div
onClick={toggleCalendar}
className="flex flex-row w-full gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-64 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
>
<Calendar size={20} />
{selectedDate ? (
<div className="flex flex-row justify-between w-full min-w-36 font-bold gap-1">
<div className="flex flex-row gap-1.5 w-max">
<span className="text-md">از</span>
<span className="text-md">{selectedDate?.sinceMonth}</span>
<span className="text-md">{currentYear.since}</span>
</div>
<div className="flex flex-row gap-1.5 w-max">
<span className="text-md">تا</span>
<span className="text-md">{selectedDate?.fromMonth}</span>
<span className="text-md">{currentYear.until}</span>
</div>
</div>
) : (
"تاریخ مورد نظر خود را انتخاب نمایید"
)}
</div>
{openCalendar && (
<div className="flex flex-row gap-2.5 absolute top-14 right-[-40px] p-2.5 !pt-3.5 w-80 rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
<CustomCalendar
title="از"
nextYearHandler={nextFromYearHandler}
prevYearHandler={prevFromYearHandler}
currentYear={currentYear?.since}
monthList={monthList}
selectedDate={selectedDate?.sinceMonth}
selectDateHandler={selectFromDateHandler}
/>
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
<CustomCalendar
title="تا"
nextYearHandler={nextUntilYearHandler}
prevYearHandler={prevUntilYearHandler}
currentYear={currentYear?.until}
monthList={monthList}
selectedDate={selectedDate?.fromMonth}
selectDateHandler={selectUntilDateHandler}
/>
</div>
)}
</div>
</div> </div>
{/* Right Section */} {/* Right Section */}
@ -94,14 +295,15 @@ export function Header({
{/* User Menu */} {/* User Menu */}
<div className="relative"> <div className="relative">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{user?.id === 2041 && (
{ <button
user?.id === 2041 && <button
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian" className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300 font-persian"
onClick={redirectHandler}> onClick={redirectHandler}
>
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
ورود به میزکار مدیریت</button> ورود به میزکار مدیریت
} </button>
)}
<Button <Button
variant="ghost" variant="ghost"
@ -109,7 +311,6 @@ export function Header({
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)} onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
className="flex items-center gap-2 text-gray-300" className="flex items-center gap-2 text-gray-300"
> >
<div className="hidden sm:block text-right"> <div className="hidden sm:block text-right">
<div className="text-sm font-medium font-persian"> <div className="text-sm font-medium font-persian">
{user?.name} {user?.family} {user?.name} {user?.family}
@ -118,7 +319,7 @@ export function Header({
{user?.username} {user?.username}
</div> </div>
</div> </div>
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
</div> </div>
</Button> </Button>

View File

@ -1,9 +1,8 @@
import { useState } from "react"; import { useState } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Sidebar } from "./sidebar";
import { Header } from "./header"; import { Header } from "./header";
import { Sidebar } from "./sidebar";
import { StrategicAlignmentPopup } from "./strategic-alignment-popup"; import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
import apiService from "~/lib/api";
interface DashboardLayoutProps { interface DashboardLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -18,9 +17,14 @@ export function DashboardLayout({
}: DashboardLayoutProps) { }: DashboardLayoutProps) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false); const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] =
const [currentTitle, setCurrentTitle] = useState<string | undefined>(title ?? "صفحه اول"); useState(false);
const [currentTitleIcon, setCurrentTitleIcon] = useState<React.ComponentType<{ className?: string }> | null | undefined>(undefined); const [currentTitle, setCurrentTitle] = useState<string | undefined>(
title ?? "صفحه اول"
);
const [currentTitleIcon, setCurrentTitleIcon] = useState<
React.ComponentType<{ className?: string }> | null | undefined
>(undefined);
const toggleSidebarCollapse = () => { const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(!isSidebarCollapsed); setIsSidebarCollapsed(!isSidebarCollapsed);
@ -30,8 +34,6 @@ export function DashboardLayout({
setIsMobileSidebarOpen(!isMobileSidebarOpen); setIsMobileSidebarOpen(!isMobileSidebarOpen);
}; };
return ( return (
<div <div
className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden" className="h-screen flex overflow-hidden bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)] relative overflow-x-hidden"
@ -55,19 +57,20 @@ export function DashboardLayout({
"fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out", "fixed inset-y-0 right-0 z-50 flex flex-col lg:static lg:inset-auto lg:translate-x-0 transition-transform duration-300 ease-in-out",
isMobileSidebarOpen isMobileSidebarOpen
? "translate-x-0" ? "translate-x-0"
: "translate-x-full lg:translate-x-0", : "translate-x-full lg:translate-x-0"
)} )}
> >
<Sidebar <Sidebar
isCollapsed={isSidebarCollapsed} isCollapsed={isSidebarCollapsed}
onToggleCollapse={toggleSidebarCollapse} onToggleCollapse={toggleSidebarCollapse}
className="h-full flex-shrink-0 relative z-10" className="h-full flex-shrink-0 relative z-10"
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)} onStrategicAlignmentClick={() =>
setIsStrategicAlignmentPopupOpen(true)
}
onTitleChange={(info) => { onTitleChange={(info) => {
setCurrentTitle(info.title); setCurrentTitle(info.title);
setCurrentTitleIcon(info.icon ?? null); setCurrentTitleIcon(info.icon ?? null);
}} }}
/> />
</div> </div>
@ -85,7 +88,7 @@ export function DashboardLayout({
<main <main
className={cn( className={cn(
"flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0", "flex-1 overflow-x-hidden overflow-y-auto focus:outline-none transition-all duration-300 min-w-0",
className, className
)} )}
> >
<div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden p-5"> <div className="relative h-full min-w-0 w-full z-10 overflow-x-hidden p-5">
@ -93,7 +96,10 @@ export function DashboardLayout({
</div> </div>
</main> </main>
</div> </div>
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} /> <StrategicAlignmentPopup
open={isStrategicAlignmentPopupOpen}
onOpenChange={setIsStrategicAlignmentPopupOpen}
/>
</div> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import jalaali from "jalaali-js";
import { import {
BrainCircuit, BrainCircuit,
ChevronDown, ChevronDown,
@ -12,7 +13,7 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import moment from "moment-jalaali"; import moment from "moment-jalaali";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 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";
@ -34,7 +35,8 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency, formatNumber } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
@ -146,13 +148,18 @@ const columns = [
]; ];
export function DigitalInnovationPage() { export function DigitalInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<DigitalInnovationMetrics[]>([]); const [projects, setProjects] = useState<DigitalInnovationMetrics[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20); const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [totalCount, setTotalCount] = useState(0); // const [totalCount, setTotalCount] = useState(0);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [rating, setRating] = useState<ListItem[]>([]); const [rating, setRating] = useState<ListItem[]>([]);
@ -281,7 +288,11 @@ export function DigitalInnovationPage() {
"reduce_costs_percent", "reduce_costs_percent",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]], Conditions: [
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
@ -294,16 +305,16 @@ export function DigitalInnovationPage() {
if (reset) { if (reset) {
setProjects(parsedData); setProjects(parsedData);
// calculateAverage(parsedData); // calculateAverage(parsedData);
setTotalCount(parsedData.length); // setTotalCount(parsedData.length);
} else { } else {
setProjects((prev) => [...prev, ...parsedData]); setProjects((prev) => [...prev, ...parsedData]);
setTotalCount((prev) => prev + parsedData.length); // setTotalCount((prev) => prev + parsedData.length);
} }
setHasMore(parsedData.length === pageSize); setHasMore(parsedData.length === pageSize);
} else { } else {
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -311,14 +322,14 @@ export function DigitalInnovationPage() {
console.error("Error parsing project data:", parseError); console.error("Error parsing project data:", parseError);
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
} else { } else {
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -326,7 +337,7 @@ export function DigitalInnovationPage() {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها"); toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -335,7 +346,7 @@ export function DigitalInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها"); toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} finally { } finally {
@ -356,7 +367,15 @@ export function DigitalInnovationPage() {
fetchTable(true); fetchTable(true);
fetchTotalCount(); fetchTotalCount();
fetchStats(); fetchStats();
}, [sortConfig]); }, [sortConfig, date]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -412,19 +431,23 @@ export function DigitalInnovationPage() {
direction: direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc", prev.field === field && prev.direction === "asc" ? "desc" : "asc",
})); }));
fetchTotalCount(); fetchTotalCount(date?.start, date?.end);
fetchStats(); fetchStats(date?.start, date?.end);
setCurrentPage(1); setCurrentPage(1);
setProjects([]); setProjects([]);
setHasMore(true); setHasMore(true);
}; };
const fetchTotalCount = async () => { const fetchTotalCount = async (startDate?: string, endDate?: string) => {
try { try {
const response = await apiService.select({ const response = await apiService.select({
ProcessName: "project", ProcessName: "project",
OutputFields: ["count(project_no)"], OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]], Conditions: [
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -451,7 +474,10 @@ export function DigitalInnovationPage() {
try { try {
setStatsLoading(true); setStatsLoading(true);
const raw = await apiService.call<any>({ const raw = await apiService.call<any>({
innovation_digital_function: {}, innovation_digital_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
}); });
// let payload: DigitalInnovationMetrics = raw?.data; // let payload: DigitalInnovationMetrics = raw?.data;
@ -529,33 +555,33 @@ export function DigitalInnovationPage() {
// fetchStats(); // fetchStats();
// }; // };
const renderProgress = useMemo(() => { // const renderProgress = useMemo(() => {
const total = 10; // const total = 10;
for (let i = 0; i < rating.length; i++) { // for (let i = 0; i < rating.length; i++) {
const currentElm = rating[i]; // const currentElm = rating[i];
currentElm.house = []; // currentElm.house = [];
const greenBoxes = Math.floor((total * currentElm.development) / 100); // const greenBoxes = Math.floor((total * currentElm.development) / 100);
const partialPercent = // const partialPercent =
(total * currentElm.development) / 100 - greenBoxes; // (total * currentElm.development) / 100 - greenBoxes;
for (let j = 0; j < greenBoxes; j++) { // for (let j = 0; j < greenBoxes; j++) {
currentElm.house.push({ // currentElm.house.push({
index: j, // index: j,
color: "!bg-emerald-400", // color: "!bg-emerald-400",
}); // });
} // }
if (partialPercent != 0 && greenBoxes != 10) // if (partialPercent != 0 && greenBoxes != 10)
currentElm.house.push({ // currentElm.house.push({
index: greenBoxes + 1, // index: greenBoxes + 1,
style: `linear-gradient( // style: `linear-gradient(
to right, // to right,
oklch(76.5% 0.177 163.223) 0%, // oklch(76.5% 0.177 163.223) 0%,
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%, // 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) ${partialPercent * 100}%,
oklch(55.1% 0.027 264.364) 100% // oklch(55.1% 0.027 264.364) 100%
)`, // )`,
}); // });
} // }
}, [rating]); // }, [rating]);
const statusColor = (status: projectStatus): any => { const statusColor = (status: projectStatus): any => {
let el = null; let el = null;

View File

@ -26,8 +26,9 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -46,6 +47,7 @@ import {
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
// moment.loadPersian({ usePersianDigits: true }); // moment.loadPersian({ usePersianDigits: true });
@ -157,6 +159,7 @@ const columns = [
]; ];
export function GreenInnovationPage() { export function GreenInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<GreenInnovationData[]>([]); const [projects, setProjects] = useState<GreenInnovationData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -166,6 +169,10 @@ export function GreenInnovationPage() {
const [totalCount, setTotalCount] = useState(0); const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [stats, setStats] = useState<stateCounter>(); const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({ const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date", field: "start_date",
@ -288,7 +295,11 @@ export function GreenInnovationPage() {
"observer", "observer",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]], Conditions: [
["type_of_innovation", "=", "نوآوری سبز", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
if (response.state === 0) { if (response.state === 0) {
@ -350,6 +361,14 @@ export function GreenInnovationPage() {
} }
}; };
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (hasMore && !loading) { if (hasMore && !loading) {
setCurrentPage((prev) => prev + 1); setCurrentPage((prev) => prev + 1);
@ -359,11 +378,11 @@ export function GreenInnovationPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -416,7 +435,11 @@ export function GreenInnovationPage() {
const response = await apiService.select({ const response = await apiService.select({
ProcessName: "project", ProcessName: "project",
OutputFields: ["count(project_no)"], OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]], Conditions: [
["type_of_innovation", "=", "نوآوری سبز", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
const dataString = response.data; const dataString = response.data;
@ -448,6 +471,8 @@ export function GreenInnovationPage() {
selectedProjects.size > 0 selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ") ? Array.from(selectedProjects).join(" , ")
: "", : "",
start_date: date?.start || null,
end_date: date?.end || null,
}, },
}); });
let payload: any = raw?.data; let payload: any = raw?.data;

View File

@ -19,6 +19,7 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import jalaali from "jalaali-js";
import { import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@ -40,7 +41,8 @@ import {
XAxis, XAxis,
} from "recharts"; } from "recharts";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency, formatNumber } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
interface innovationBuiltInDate { interface innovationBuiltInDate {
@ -177,6 +179,7 @@ const dialogChartData = [
]; ];
export function InnovationBuiltInsidePage() { export function InnovationBuiltInsidePage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<innovationBuiltInDate[]>([]); const [projects, setProjects] = useState<innovationBuiltInDate[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -191,6 +194,10 @@ export function InnovationBuiltInsidePage() {
field: "start_date", field: "start_date",
direction: "asc", direction: "asc",
}); });
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [tblAvarage, setTblAvarage] = useState<number>(0); const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] = const [selectedProjects, setSelectedProjects] =
useState<Set<string | number>>(); useState<Set<string | number>>();
@ -310,7 +317,11 @@ export function InnovationBuiltInsidePage() {
"technology_maturity_level", "technology_maturity_level",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]], Conditions: [
["type_of_innovation", "=", "نوآوری ساخت داخل", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
if (response.state === 0) { if (response.state === 0) {
@ -416,13 +427,21 @@ export function InnovationBuiltInsidePage() {
} }
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
}, [sortConfig]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -480,6 +499,8 @@ export function InnovationBuiltInsidePage() {
selectedProjects && selectedProjects?.size > 0 selectedProjects && selectedProjects?.size > 0
? Array.from(selectedProjects).join(" , ") ? Array.from(selectedProjects).join(" , ")
: "", : "",
start_date: date?.start || null,
end_date: date?.end || null,
}, },
}); });
let payload: any = raw?.data; let payload: any = raw?.data;
@ -624,7 +645,8 @@ export function InnovationBuiltInsidePage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleProjectDetails(item)} onClick={() => handleProjectDetails(item)}
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"> className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto"
>
جزئیات بیشتر جزئیات بیشتر
</Button> </Button>
); );

View File

@ -1,3 +1,4 @@
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -35,7 +36,8 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
@ -117,13 +119,18 @@ const columns = [
]; ];
export function ProcessInnovationPage() { export function ProcessInnovationPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<ProcessInnovationData[]>([]); const [projects, setProjects] = useState<ProcessInnovationData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(20); const [pageSize] = useState(20);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [totalCount, setTotalCount] = useState(0); // const [totalCount, setTotalCount] = useState(0);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const [actualTotalCount, setActualTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false); const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<InnovationStats>({ const [stats, setStats] = useState<InnovationStats>({
@ -196,13 +203,13 @@ export function ProcessInnovationPage() {
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
// Selection handlers // Selection handlers
const handleSelectAll = () => { // const handleSelectAll = () => {
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)));
} // }
}; // };
const handleSelectProject = (projectNo: string) => { const handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects); const newSelected = new Set(selectedProjects);
@ -256,7 +263,11 @@ export function ProcessInnovationPage() {
"observer", "observer",
], ],
Sorts: [["start_date", "asc"]], Sorts: [["start_date", "asc"]],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]], Conditions: [
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
}); });
@ -268,16 +279,16 @@ export function ProcessInnovationPage() {
if (Array.isArray(parsedData)) { if (Array.isArray(parsedData)) {
if (reset) { if (reset) {
setProjects(parsedData); setProjects(parsedData);
setTotalCount(parsedData.length); // setTotalCount(parsedData.length);
} else { } else {
setProjects((prev) => [...prev, ...parsedData]); setProjects((prev) => [...prev, ...parsedData]);
setTotalCount((prev) => prev + parsedData.length); // setTotalCount((prev) => prev + parsedData.length);
} }
setHasMore(parsedData.length === pageSize); setHasMore(parsedData.length === pageSize);
} else { } else {
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -285,14 +296,14 @@ export function ProcessInnovationPage() {
console.error("Error parsing project data:", parseError); console.error("Error parsing project data:", parseError);
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
} else { } else {
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -300,7 +311,7 @@ export function ProcessInnovationPage() {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها"); toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} }
@ -309,7 +320,7 @@ export function ProcessInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها"); toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
setTotalCount(0); // setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} finally { } finally {
@ -325,14 +336,22 @@ export function ProcessInnovationPage() {
} }
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects]); }, [selectedProjects, date]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -382,7 +401,11 @@ export function ProcessInnovationPage() {
const response = await apiService.select({ const response = await apiService.select({
ProcessName: "project", ProcessName: "project",
OutputFields: ["count(project_no)"], OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]], Conditions: [
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -416,6 +439,8 @@ export function ProcessInnovationPage() {
selectedProjects.size > 0 selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ") ? Array.from(selectedProjects).join(" , ")
: "", : "",
start_date: date?.start || null,
end_date: date?.end || null,
}, },
}); });

View File

@ -1,5 +1,6 @@
import jalaali from "jalaali-js";
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { useCallback, useEffect, useMemo, 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 { Card, CardContent } from "~/components/ui/card"; import { Card, CardContent } from "~/components/ui/card";
@ -13,8 +14,8 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import { formatNumber } from "~/lib/utils"; import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
interface ProjectData { interface ProjectData {
@ -153,6 +154,7 @@ const columns: ColumnDef[] = [
]; ];
export function ProjectManagementPage() { export function ProjectManagementPage() {
const { jy } = jalaali.toJalaali(new Date());
const [projects, setProjects] = useState<ProjectData[]>([]); const [projects, setProjects] = useState<ProjectData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
@ -169,6 +171,10 @@ export function ProjectManagementPage() {
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null); const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
// Prevent concurrent API calls // Prevent concurrent API calls
@ -200,7 +206,10 @@ export function ProjectManagementPage() {
OutputFields: outputFields, OutputFields: outputFields,
Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
Sorts: sortField ? [[sortField, sortConfig.direction]] : [], Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
Conditions: [], Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -265,6 +274,13 @@ export function ProjectManagementPage() {
} }
}; };
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
if (hasMore && !loading && !loadingMore && !fetchingRef.current) { if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
setCurrentPage((prev) => prev + 1); setCurrentPage((prev) => prev + 1);
@ -274,7 +290,7 @@ export function ProjectManagementPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig]); }, [sortConfig, date]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -287,7 +303,8 @@ export function ProjectManagementPage() {
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
const handleScroll = () => { const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return; if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
return;
// Clear previous timeout // Clear previous timeout
if (scrollTimeoutRef.current) { if (scrollTimeoutRef.current) {
@ -307,7 +324,9 @@ export function ProjectManagementPage() {
}; };
if (scrollContainer) { if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, { passive: true }); scrollContainer.addEventListener("scroll", handleScroll, {
passive: true,
});
} }
return () => { return () => {
@ -337,7 +356,10 @@ export function ProjectManagementPage() {
const response = await apiService.select({ const response = await apiService.select({
ProcessName: "project", ProcessName: "project",
OutputFields: ["count(project_no)"], OutputFields: ["count(project_no)"],
Conditions: [], Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -358,14 +380,14 @@ export function ProjectManagementPage() {
} }
}; };
const handleRefresh = () => { // const handleRefresh = () => {
fetchingRef.current = false; // Reset fetching state on refresh // fetchingRef.current = false; // Reset fetching state on refresh
setCurrentPage(1); // setCurrentPage(1);
setProjects([]); // setProjects([]);
setHasMore(true); // setHasMore(true);
fetchProjects(true); // fetchProjects(true);
fetchTotalCount(); // fetchTotalCount();
}; // };
// ...existing code... // ...existing code...
@ -630,7 +652,7 @@ export function ProjectManagementPage() {
.filter((v) => v !== null) as number[]; .filter((v) => v !== null) as number[];
res["remaining_time"] = remainingValues.length res["remaining_time"] = remainingValues.length
? Math.round( ? Math.round(
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length, remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length
) )
: null; : null;
@ -644,7 +666,7 @@ export function ProjectManagementPage() {
const num = Number( const num = Number(
String(raw) String(raw)
.toString() .toString()
.replace(/[^0-9.-]/g, ""), .replace(/[^0-9.-]/g, "")
); );
return Number.isFinite(num) ? num : NaN; return Number.isFinite(num) ? num : NaN;
}) })
@ -770,7 +792,10 @@ 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">
<div ref={scrollContainerRef} className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"> <div
ref={scrollContainerRef}
className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"
>
<Table className="table-fixed"> <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]">

View File

@ -1,3 +1,4 @@
import jalaali from "jalaali-js";
import { useEffect, useReducer, useRef, useState } from "react"; import { useEffect, useReducer, useRef, useState } from "react";
import { import {
Bar, Bar,
@ -12,7 +13,8 @@ import {
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
import { Skeleton } from "~/components/ui/skeleton"; import { Skeleton } from "~/components/ui/skeleton";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { ChartContainer } from "../ui/chart"; import { ChartContainer } from "../ui/chart";
import { import {
DropdownMenu, DropdownMenu,
@ -116,6 +118,7 @@ export function StrategicAlignmentPopup({
open, open,
onOpenChange, onOpenChange,
}: StrategicAlignmentPopupProps) { }: StrategicAlignmentPopupProps) {
const { jy } = jalaali.toJalaali(new Date());
const [data, setData] = useState<StrategicAlignmentData[]>([]); const [data, setData] = useState<StrategicAlignmentData[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const contentRef = useRef<HTMLDivElement | null>(null); const contentRef = useRef<HTMLDivElement | null>(null);
@ -125,22 +128,35 @@ export function StrategicAlignmentPopup({
dropDownItems: [], dropDownItems: [],
}); });
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
useEffect(() => { useEffect(() => {
if (open) { if (open) {
fetchData(); fetchData();
} }
}, [open]); }, [open]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await apiService.select({ const response = await apiService.select({
ProcessName: "project", ProcessName: "project",
OutputFields: [ OutputFields: ["strategic_theme", "count(operational_fee)"],
"strategic_theme",
"count(operational_fee)",
],
GroupBy: ["strategic_theme"], GroupBy: ["strategic_theme"],
Conditions: [
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
const responseData = const responseData =
@ -170,7 +186,11 @@ export function StrategicAlignmentPopup({
"value_technology_and_innovation", "value_technology_and_innovation",
"count(operational_fee)", "count(operational_fee)",
], ],
Conditions: [["strategic_theme", "=", item]], Conditions: [
["strategic_theme", "=", item, "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
GroupBy: ["value_technology_and_innovation"], GroupBy: ["value_technology_and_innovation"],
}); });
@ -247,7 +267,9 @@ export function StrategicAlignmentPopup({
(item: StrategicAlignmentData) => ({ (item: StrategicAlignmentData) => ({
...item, ...item,
percentage: percentage:
total > 0 ? Math.round((item.operational_fee_count / total) * 100) : 0, total > 0
? Math.round((item.operational_fee_count / total) * 100)
: 0,
}) })
); );
setData(dataWithPercentage || []); setData(dataWithPercentage || []);

View File

@ -1,7 +1,6 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { import {
Area, Area,
AreaChart, AreaChart,
@ -11,9 +10,11 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils"; import { EventBus, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
export interface CompanyDetails { export interface CompanyDetails {
id: string; id: string;
@ -62,37 +63,52 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
const [counts, setCounts] = useState<EcosystemCounts | null>(null); const [counts, setCounts] = useState<EcosystemCounts | null>(null);
const [processData, setProcessData] = useState<ProcessActorsData[]>([]); const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [date, setDate] = useState<CalendarDate>();
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
const fetchCounts = async () => {
setIsLoading(true);
try {
const [countsRes, processRes] = await Promise.all([
apiService.call<EcosystemCounts>({
ecosystem_count_function: {},
}),
apiService.call<ProcessActorsResponse[]>({
process_creating_actors_function: {},
}),
]);
setCounts(
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
);
// Process the years data and fill missing years
const processedData = processYearsData(
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
);
setProcessData(processedData);
} catch (err) {
console.error("Failed to fetch data:", err);
} finally {
setIsLoading(false);
}
};
fetchCounts(); fetchCounts();
}, []); }, [date]);
const fetchCounts = async () => {
setIsLoading(true);
try {
const [countsRes, processRes] = await Promise.all([
apiService.call<EcosystemCounts>({
ecosystem_count_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
}),
apiService.call<ProcessActorsResponse[]>({
process_creating_actors_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
}),
]);
setCounts(
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0]
);
// Process the years data and fill missing years
const processedData = processYearsData(
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors)
);
setProcessData(processedData);
} catch (err) {
console.error("Failed to fetch data:", err);
} finally {
setIsLoading(false);
}
};
// Helper function to safely parse numbers // Helper function to safely parse numbers
const parseNumber = (value: string | undefined): number => { const parseNumber = (value: string | undefined): number => {
@ -103,7 +119,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
// Helper function to process years data and fill missing years // Helper function to process years data and fill missing years
const processYearsData = ( const processYearsData = (
data: ProcessActorsResponse[], data: ProcessActorsResponse[]
): ProcessActorsData[] => { ): ProcessActorsData[] => {
if (!data || data.length === 0) return []; if (!data || data.length === 0) return [];
@ -121,7 +137,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
acc[item.start_year] = item.total_count; acc[item.start_year] = item.total_count;
return acc; return acc;
}, },
{} as Record<string, number>, {} as Record<string, number>
); );
for (let year = minYear; year <= maxYear; year++) { for (let year = minYear; year <= maxYear; year++) {
@ -408,8 +424,8 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<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-sm 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)}
</span> </span>
</CardTitle> </CardTitle>
@ -432,7 +448,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]"> <CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
<div className="w-full"> <div className="w-full">
<CustomBarChart <CustomBarChart
hasPercent={false} hasPercent={false}
data={barData.map((item) => ({ data={barData.map((item) => ({
label: item.label, label: item.label,
value: item.value, value: item.value,
@ -455,70 +471,82 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
</div> </div>
<div className="h-42"> <div className="h-42">
{processData.length > 0 ? ( {processData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart <AreaChart
accessibilityLayer accessibilityLayer
data={processData} data={processData}
margin={{ top: 25, right: 30, left: 0, bottom: 0 }} margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
> >
<defs> <defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1"> <linearGradient
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} /> id="fillDesktop"
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} /> x1="0"
</linearGradient> y1="0"
</defs> x2="0"
y2="1"
>
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid <CartesianGrid
vertical={false} vertical={false}
stroke="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.1)"
/> />
<XAxis <XAxis
dataKey="year" dataKey="year"
stroke="#9ca3af" stroke="#9ca3af"
fontSize={12} fontSize={12}
tickLine={false} tickLine={false}
tickMargin={8} tickMargin={8}
axisLine={false} axisLine={false}
tickFormatter={formatPersianYear} tickFormatter={formatPersianYear}
/> />
<YAxis <YAxis
stroke="#9ca3af" stroke="#9ca3af"
fontSize={12} fontSize={12}
tickMargin={12} tickMargin={12}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
tickFormatter={(value) => formatNumber(value)} tickFormatter={(value) => formatNumber(value)}
/> />
<Tooltip cursor={false} content={<></>} /> <Tooltip cursor={false} content={<></>} />
{/* ✅ Use gradient for fill */}
<Area
type="monotone"
dataKey="value"
stroke="#3AEA83"
fill="url(#fillDesktop)"
strokeWidth={2}
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>
{/* ✅ Use gradient for fill */}
<Area
type="monotone"
dataKey="value"
stroke="#3AEA83"
fill="url(#fillDesktop)"
strokeWidth={2}
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">
دادهای برای نمایش وجود ندارد دادهای برای نمایش وجود ندارد
@ -526,7 +554,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );

View File

@ -1,7 +1,9 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as d3 from "d3"; import * as d3 from "d3";
import apiService from "../../lib/api"; import { useCallback, useEffect, useRef, useState } from "react";
import { EventBus } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { useAuth } from "../../contexts/auth-context"; import { useAuth } from "../../contexts/auth-context";
import apiService from "../../lib/api";
const API_BASE_URL = const API_BASE_URL =
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api"; import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
@ -59,7 +61,10 @@ function isBrowser(): boolean {
return typeof window !== "undefined"; return typeof window !== "undefined";
} }
export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps) { export function NetworkGraph({
onNodeClick,
onLoadingChange,
}: NetworkGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
const [nodes, setNodes] = useState<Node[]>([]); const [nodes, setNodes] = useState<Node[]>([]);
const [links, setLinks] = useState<Link[]>([]); const [links, setLinks] = useState<Link[]>([]);
@ -68,6 +73,15 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const { token } = useAuth(); const { token } = useAuth();
const [date, setDate] = useState<CalendarDate>();
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
if (isBrowser()) { if (isBrowser()) {
const timer = setTimeout(() => setIsMounted(true), 100); const timer = setTimeout(() => setIsMounted(true), 100);
@ -80,16 +94,21 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
if (!token?.accessToken) return null; if (!token?.accessToken) return null;
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`; return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
}, },
[token?.accessToken], [token?.accessToken]
); );
const callAPI = useCallback(async (stage_id: number) => { const callAPI = useCallback(
return await apiService.call<any>({ async (stage_id: number) => {
get_values_workflow_function: { return await apiService.call<any>({
stage_id: stage_id, get_values_workflow_function: {
}, stage_id: stage_id,
}); start_date: date?.start || null,
}, []); end_date: date?.end || null,
},
});
},
[date]
);
useEffect(() => { useEffect(() => {
if (!isMounted) return; if (!isMounted) return;
@ -108,7 +127,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
const data = parseApiResponse(JSON.parse(res.data)?.graph_production); const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
console.log( console.log(
"All available fields in first item:", "All available fields in first item:",
Object.keys(data[0] || {}), Object.keys(data[0] || {})
); );
// نود مرکزی // نود مرکزی
@ -121,7 +140,9 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
}; };
// دسته‌بندی‌ها // دسته‌بندی‌ها
const categories = Array.from(new Set(data.map((item: any) => item.category))); const categories = Array.from(
new Set(data.map((item: any) => item.category))
);
const categoryNodes: Node[] = categories.map((cat, index) => ({ const categoryNodes: Node[] = categories.map((cat, index) => ({
id: `cat-${index}`, id: `cat-${index}`,
@ -170,7 +191,8 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
}, [isMounted, token, getImageUrl]); }, [isMounted, token, getImageUrl]);
useEffect(() => { useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) return; if (!isMounted || !svgRef.current || isLoading || nodes.length === 0)
return;
const svg = d3.select(svgRef.current); const svg = d3.select(svgRef.current);
const width = svgRef.current.clientWidth; const width = svgRef.current.clientWidth;
@ -225,12 +247,18 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
.forceLink<Node, Link>(links) .forceLink<Node, Link>(links)
.id((d) => d.id) .id((d) => d.id)
.distance(150) .distance(150)
.strength(0.2), .strength(0.2)
) )
.force("charge", d3.forceManyBody().strength(-300)) .force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2)) .force("center", d3.forceCenter(width / 2, height / 2))
.force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2)) .force(
.force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35))); "radial",
d3.forceRadial((d) => (d.isCenter ? 0 : 300), width / 2, height / 2)
)
.force(
"collision",
d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35))
);
// Initial zoom to show entire graph // Initial zoom to show entire graph
const initialScale = 0.6; const initialScale = 0.6;
@ -242,23 +270,23 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
zoom.transform, zoom.transform,
d3.zoomIdentity d3.zoomIdentity
.translate(initialTranslate[0], initialTranslate[1]) .translate(initialTranslate[0], initialTranslate[1])
.scale(initialScale), .scale(initialScale)
); );
// Fix center node // Fix center node
const centerNode = nodes.find(n => n.isCenter); const centerNode = nodes.find((n) => n.isCenter);
const categoryNodes = nodes.filter(n => !n.isCenter && n.stageid === -1); const categoryNodes = nodes.filter((n) => !n.isCenter && n.stageid === -1);
if (centerNode) { if (centerNode) {
const centerX = width / 2; const centerX = width / 2;
const centerY = height / 2; const centerY = height / 2;
centerNode.fx = centerX; centerNode.fx = centerX;
centerNode.fy = centerY; centerNode.fy = centerY;
const baseRadius = 450; // شعاع پایه const baseRadius = 450; // شعاع پایه
const variation = 100; // تغییر طول یکی در میان const variation = 100; // تغییر طول یکی در میان
const angleStep = (2 * Math.PI) / categoryNodes.length; const angleStep = (2 * Math.PI) / categoryNodes.length;
categoryNodes.forEach((catNode, i) => { categoryNodes.forEach((catNode, i) => {
const angle = i * angleStep; const angle = i * angleStep;
const radius = baseRadius + (i % 2 === 0 ? -variation : variation); const radius = baseRadius + (i % 2 === 0 ? -variation : variation);
@ -266,26 +294,24 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
catNode.fy = centerY + radius * Math.sin(angle); catNode.fy = centerY + radius * Math.sin(angle);
}); });
} }
// نودهای نهایی **هیچ fx/fy نداشته باشند** // نودهای نهایی **هیچ fx/fy نداشته باشند**
// فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد // فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// categoryNodes.forEach((catNode) => { // const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
// const childCount = childNodes.length;
// const radius = 100; // فاصله از دسته
// const angleStep = (2 * Math.PI) / childCount;
// childNodes.forEach((node, i) => { // categoryNodes.forEach((catNode) => {
// const angle = i * angleStep; // const childNodes = finalNodes.filter(n => n.category === catNode.category);
// node.fx = catNode.fx! + radius * Math.cos(angle); // const childCount = childNodes.length;
// node.fy = catNode.fy! + radius * Math.sin(angle); // const radius = 100; // فاصله از دسته
// }); // const angleStep = (2 * Math.PI) / childCount;
// });
// childNodes.forEach((node, i) => {
// const angle = i * angleStep;
// node.fx = catNode.fx! + radius * Math.cos(angle);
// node.fy = catNode.fy! + radius * Math.sin(angle);
// });
// });
// Curved links // Curved links
const link = container const link = container
@ -305,8 +331,8 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
.enter() .enter()
.append("g") .append("g")
.attr("class", "node") .attr("class", "node")
.style("cursor", d => d.stageid === -1 ? "default" : "pointer"); .style("cursor", (d) => (d.stageid === -1 ? "default" : "pointer"));
const drag = d3 const drag = d3
.drag<SVGGElement, Node>() .drag<SVGGElement, Node>()
.on("start", (event, d) => { .on("start", (event, d) => {
@ -337,7 +363,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
.attr("width", 200) .attr("width", 200)
.attr("height", 80) .attr("height", 80)
.attr("x", -100) // نصف عرض جدید منفی .attr("x", -100) // نصف عرض جدید منفی
.attr("y", -40) // نصف ارتفاع جدید منفی .attr("y", -40) // نصف ارتفاع جدید منفی
.attr("rx", 8) .attr("rx", 8)
.attr("ry", 8) .attr("ry", 8)
.attr("fill", categoryToColor[d.category] || "#94A3B8") .attr("fill", categoryToColor[d.category] || "#94A3B8")
@ -358,7 +384,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
.append("image") .append("image")
.attr("x", 0) .attr("x", 0)
.attr("y", 0) .attr("y", 0)
.attr("width", 200) // ← هم‌اندازه با مستطیل .attr("width", 200) // ← هم‌اندازه با مستطیل
.attr("height", 80) .attr("height", 80)
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl) .attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
.attr("preserveAspectRatio", "xMidYMid slice"); .attr("preserveAspectRatio", "xMidYMid slice");
@ -437,12 +463,11 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
.attr("stroke-width", 3); .attr("stroke-width", 3);
}); });
nodeGroup.on("click", async function (event, d) { nodeGroup.on("click", async function (event, d) {
event.stopPropagation(); event.stopPropagation();
// جلوگیری از کلیک روی مرکز و دسته‌بندی‌ها // جلوگیری از کلیک روی مرکز و دسته‌بندی‌ها
if (d.isCenter || d.stageid === -1) return; if (d.isCenter || d.stageid === -1) return;
if (onNodeClick && d.stageid) { if (onNodeClick && d.stageid) {
// Open dialog immediately with basic info // Open dialog immediately with basic info
@ -454,10 +479,10 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
fields: [], fields: [],
}; };
onNodeClick(basicDetails); onNodeClick(basicDetails);
// Start loading // Start loading
onLoadingChange?.(true); onLoadingChange?.(true);
try { try {
const res = await callAPI(d.stageid); const res = await callAPI(d.stageid);
const responseData = JSON.parse(res.data); const responseData = JSON.parse(res.data);
@ -467,15 +492,15 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
const filteredFields = fieldValues.filter( const filteredFields = fieldValues.filter(
(field: any) => (field: any) =>
!["image", "img", "full_name", "about_collaboration"].includes( !["image", "img", "full_name", "about_collaboration"].includes(
field.F.toLowerCase(), 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("about_collaboration") || field.F.toLowerCase().includes("about_collaboration") ||
field.F.toLowerCase().includes("about"), field.F.toLowerCase().includes("about")
); );
const companyDetails: CompanyDetails = { const companyDetails: CompanyDetails = {
@ -592,5 +617,4 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
); );
} }
export default NetworkGraph;
export default NetworkGraph;

View File

@ -0,0 +1,67 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import React from "react";
interface MonthItem {
id: string;
label: string;
start: string;
end: string;
}
// interface CurrentDay {
// start: string;
// end: string;
// month: string;
// }
interface CalendarProps {
title: string;
nextYearHandler: () => void;
prevYearHandler: () => void;
currentYear?: number;
monthList: Array<MonthItem>;
selectedDate?: string;
selectDateHandler: (item: MonthItem) => void;
}
export const Calendar: React.FC<CalendarProps> = ({
title,
nextYearHandler,
prevYearHandler,
currentYear,
monthList,
selectedDate,
selectDateHandler,
}) => {
return (
<div className="filter-box bg-pr-gray w-full px-1">
<header className="flex flex-row border-b border-[#5F6284] pb-1.5 justify-center">
<span className="font-light">{title}</span>
<div className="flex flex-row items-center gap-3">
<ChevronRight
className="inline-block w-6 h-6 cursor-pointer"
onClick={nextYearHandler}
/>
<span className="font-light">{currentYear}</span>
<ChevronLeft
className="inline-block w-6 h-6 cursor-pointer"
onClick={prevYearHandler}
/>
</div>
</header>
<div className="content flex flex-col gap-2 text-center pt-1 cursor-pointer">
{monthList.map((item, index) => (
<span
key={`${item.id}-${index}`}
className={`text-lg hover:bg-[#33364D] p-1 rounded-xl transition-all duration-300 ${
selectedDate === item.label ? `bg-[#33364D]` : ""
}`}
onClick={() => selectDateHandler(item)}
>
{item.label}
</span>
))}
</div>
</div>
);
};

View File

@ -1,6 +1,7 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import EventEmitter from "events";
import moment from "moment-jalaali"; import moment from "moment-jalaali";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -22,8 +23,6 @@ export const formatCurrency = (amount: string | number) => {
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
}; };
/** /**
* محاسبه دامنه nice numbers برای محور Y نمودارها * محاسبه دامنه nice numbers برای محور Y نمودارها
* @param values آرایه از مقادیر دادهها * @param values آرایه از مقادیر دادهها
@ -46,7 +45,7 @@ export function calculateNiceRange(
// پیدا کردن حداکثر مقدار در داده‌ها // پیدا کردن حداکثر مقدار در داده‌ها
const dataMax = Math.max(...values); const dataMax = Math.max(...values);
// اگر همه مقادیر صفر یا منفی هستند // اگر همه مقادیر صفر یا منفی هستند
if (dataMax <= 0) { if (dataMax <= 0) {
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] }; return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
@ -57,19 +56,19 @@ export function calculateNiceRange(
// محاسبه nice upper limit // محاسبه nice upper limit
const niceMax = calculateNiceNumber(maxWithMargin, true); const niceMax = calculateNiceNumber(maxWithMargin, true);
// محاسبه فاصله مناسب tick ها بر اساس niceMax // محاسبه فاصله مناسب tick ها بر اساس niceMax
const range = niceMax - minValue; const range = niceMax - minValue;
const targetTicks = 5; // هدف: 5 tick const targetTicks = 5; // هدف: 5 tick
const roughTickInterval = range / (targetTicks - 1); const roughTickInterval = range / (targetTicks - 1);
const niceTickInterval = calculateNiceNumber(roughTickInterval, false); const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
// ایجاد آرایه tick ها // ایجاد آرایه tick ها
const ticks: number[] = []; const ticks: number[] = [];
for (let i = minValue; i <= niceMax; i += niceTickInterval) { for (let i = minValue; i <= niceMax; i += niceTickInterval) {
ticks.push(Math.round(i)); ticks.push(Math.round(i));
} }
// اطمینان از اینکه niceMax در آرایه tick ها باشد // اطمینان از اینکه niceMax در آرایه tick ها باشد
if (ticks[ticks.length - 1] !== niceMax) { if (ticks[ticks.length - 1] !== niceMax) {
ticks.push(niceMax); ticks.push(niceMax);
@ -90,13 +89,13 @@ export function calculateNiceRange(
*/ */
function calculateNiceNumber(value: number, round: boolean): number { function calculateNiceNumber(value: number, round: boolean): number {
if (value <= 0) return 0; if (value <= 0) return 0;
// پیدا کردن قدرت 10 // پیدا کردن قدرت 10
const exponent = Math.floor(Math.log10(value)); const exponent = Math.floor(Math.log10(value));
const fraction = value / Math.pow(10, exponent); const fraction = value / Math.pow(10, exponent);
let niceFraction: number; let niceFraction: number;
if (round) { if (round) {
// برای حداکثر: به سمت بالا گرد می‌کنیم با دقت بیشتر // برای حداکثر: به سمت بالا گرد می‌کنیم با دقت بیشتر
if (fraction <= 1.0) niceFraction = 1; if (fraction <= 1.0) niceFraction = 1;
@ -112,12 +111,12 @@ function calculateNiceNumber(value: number, round: boolean): number {
else if (fraction <= 5.0) niceFraction = 5; else if (fraction <= 5.0) niceFraction = 5;
else niceFraction = 10; else niceFraction = 10;
} }
return niceFraction * Math.pow(10, exponent); return niceFraction * Math.pow(10, exponent);
} }
export const handleDataValue = (val: any): any => { export const handleDataValue = (val: any): any => {
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
if (val == null) return val; if (val == null) return val;
if ( if (
typeof val === "string" && typeof val === "string" &&
@ -132,4 +131,6 @@ moment.loadPersian({ usePersianDigits: true });
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]); return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
} }
return val; return val;
} };
export const EventBus = new EventEmitter();

6
app/types/util.type.ts Normal file
View File

@ -0,0 +1,6 @@
export interface CalendarDate {
start: string;
end: string;
sinceMonth?: string;
untilMonth?: string;
}