Compare commits
4 Commits
8749cebe7c
...
0dd1fe2ec2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd1fe2ec2 | ||
|
|
efa46a02c2 | ||
|
|
bda2e62411 | ||
|
|
173176bbb5 |
|
|
@ -1,38 +1,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { DashboardLayout } from "./layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
} from "recharts";
|
||||
import apiService from "~/lib/api";
|
||||
import jalaali from "jalaali-js";
|
||||
import { Book, CheckCircle } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
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 {
|
||||
Label,
|
||||
PolarGrid,
|
||||
|
|
@ -40,26 +9,53 @@ import {
|
|||
RadialBar,
|
||||
RadialBarChart,
|
||||
} 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 { 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() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Chart and schematic data from select API
|
||||
const [companyChartData, setCompanyChartData] = useState<
|
||||
{ category: string; capacity: number; revenue: number; cost: number , costI : number,
|
||||
capacityI : number,
|
||||
revenueI : number }[]
|
||||
{
|
||||
category: string;
|
||||
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(() => {
|
||||
fetchDashboardData();
|
||||
}, []);
|
||||
}, [date]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
|
|
@ -68,12 +64,18 @@ export function DashboardHome() {
|
|||
|
||||
// Fetch top cards data
|
||||
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
|
||||
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);
|
||||
|
|
@ -106,6 +108,10 @@ export function DashboardHome() {
|
|||
"sum(pre_project_income)",
|
||||
"sum(increased_income_after_innovation)",
|
||||
],
|
||||
Conditions: [
|
||||
["start_date", ">=", date.start || null, "and"],
|
||||
["start_date", "<=", date.end || null],
|
||||
],
|
||||
GroupBy: ["related_company"],
|
||||
};
|
||||
|
||||
|
|
@ -124,12 +130,30 @@ export function DashboardHome() {
|
|||
let incCapacityTotal = 0;
|
||||
const chartRows = rows.map((r) => {
|
||||
const rel = r?.related_company ?? "-";
|
||||
const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0;
|
||||
const costRed = 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;
|
||||
const preFee =
|
||||
Number(r?.pre_innovation_fee_sum ?? 0) >= 0
|
||||
? r?.pre_innovation_fee_sum
|
||||
: 0;
|
||||
const costRed =
|
||||
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;
|
||||
|
||||
|
|
@ -141,14 +165,14 @@ export function DashboardHome() {
|
|||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||
revenue: isFinite(revenuePct) ? revenuePct : 0,
|
||||
cost: isFinite(costPct) ? costPct : 0,
|
||||
costI : costRed,
|
||||
capacityI : incCap,
|
||||
revenueI : incInc
|
||||
costI: costRed,
|
||||
capacityI: incCap,
|
||||
revenueI: incInc,
|
||||
};
|
||||
});
|
||||
|
||||
setCompanyChartData(chartRows);
|
||||
setTotalIncreasedCapacity(incCapacityTotal);
|
||||
// setTotalIncreasedCapacity(incCapacityTotal);
|
||||
} catch (error) {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
const errorMessage =
|
||||
|
|
@ -161,25 +185,24 @@ export function DashboardHome() {
|
|||
};
|
||||
|
||||
// RadialBarChart data for ideas visualization
|
||||
const getIdeasChartData = () => {
|
||||
if (!dashboardData?.topData)
|
||||
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
||||
// const getIdeasChartData = () => {
|
||||
// if (!dashboardData?.topData)
|
||||
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
|
||||
|
||||
const registered = parseFloat(
|
||||
dashboardData.topData.registered_innovation_technology_idea || "0",
|
||||
);
|
||||
const ongoing = parseFloat(
|
||||
dashboardData.topData.ongoing_innovation_technology_ideas || "0",
|
||||
);
|
||||
const percentage =
|
||||
registered > 0 ? (ongoing / registered) * 100 : 0;
|
||||
// const registered = parseFloat(
|
||||
// dashboardData.topData.registered_innovation_technology_idea || "0"
|
||||
// );
|
||||
// const ongoing = parseFloat(
|
||||
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
|
||||
// );
|
||||
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
|
||||
|
||||
return [
|
||||
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
||||
];
|
||||
};
|
||||
// return [
|
||||
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
|
||||
// ];
|
||||
// };
|
||||
|
||||
const chartData = getIdeasChartData();
|
||||
// const chartData = getIdeasChartData();
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
|
|
@ -323,20 +346,19 @@ export function DashboardHome() {
|
|||
visitors:
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas ||
|
||||
"0",
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1",
|
||||
"1"
|
||||
)) *
|
||||
100,
|
||||
100
|
||||
)
|
||||
: 0,
|
||||
fill: "var(--color-green)",
|
||||
|
|
@ -347,19 +369,18 @@ export function DashboardHome() {
|
|||
90 +
|
||||
((parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas || "0",
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1",
|
||||
?.registered_innovation_technology_idea || "1"
|
||||
)) *
|
||||
100,
|
||||
100
|
||||
)
|
||||
: 0) /
|
||||
100) *
|
||||
|
|
@ -375,11 +396,7 @@ export function DashboardHome() {
|
|||
className="first:fill-pr-red last:fill-[#24273A]"
|
||||
polarRadius={[38, 31]}
|
||||
/>
|
||||
<RadialBar
|
||||
dataKey="visitors"
|
||||
background
|
||||
cornerRadius={5}
|
||||
/>
|
||||
<RadialBar dataKey="visitors" background cornerRadius={5} />
|
||||
<PolarRadiusAxis
|
||||
tick={false}
|
||||
tickLine={false}
|
||||
|
|
@ -405,22 +422,22 @@ export function DashboardHome() {
|
|||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"0",
|
||||
"0"
|
||||
) > 0
|
||||
? Math.round(
|
||||
(parseFloat(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas ||
|
||||
"0",
|
||||
"0"
|
||||
) /
|
||||
parseFloat(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea ||
|
||||
"1",
|
||||
"1"
|
||||
)) *
|
||||
100,
|
||||
100
|
||||
)
|
||||
: 0,
|
||||
: 0
|
||||
)}
|
||||
</tspan>
|
||||
</text>
|
||||
|
|
@ -437,14 +454,14 @@ export function DashboardHome() {
|
|||
<div className="font-light text-sm">ثبت شده :</div>
|
||||
{formatNumber(
|
||||
dashboardData.topData
|
||||
?.registered_innovation_technology_idea || "0",
|
||||
?.registered_innovation_technology_idea || "0"
|
||||
)}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 font-bold text-base">
|
||||
<div className="font-light text-sm">در حال اجرا :</div>
|
||||
{formatNumber(
|
||||
dashboardData.topData
|
||||
?.ongoing_innovation_technology_ideas || "0",
|
||||
?.ongoing_innovation_technology_ideas || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -454,130 +471,144 @@ export function DashboardHome() {
|
|||
{/* Revenue Card */}
|
||||
<MetricCard
|
||||
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
||||
value={dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll("," , "") || "0"}
|
||||
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
|
||||
value={
|
||||
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
|
||||
",",
|
||||
""
|
||||
) || "0"
|
||||
}
|
||||
percentValue={
|
||||
dashboardData.topData
|
||||
?.technology_innovation_based_revenue_growth_percent
|
||||
}
|
||||
percentLabel="درصد به کل درآمد"
|
||||
/>
|
||||
|
||||
{/* Cost Reduction Card */}
|
||||
<MetricCard
|
||||
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
||||
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0"))}
|
||||
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
|
||||
value={Math.round(
|
||||
parseFloat(
|
||||
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
|
||||
/,/g,
|
||||
""
|
||||
) || "0"
|
||||
)
|
||||
)}
|
||||
percentValue={
|
||||
dashboardData.topData
|
||||
?.technology_innovation_based_cost_reduction_percent || "0"
|
||||
}
|
||||
percentLabel="درصد به کل هزینه"
|
||||
/>
|
||||
|
||||
{/* Budget Ratio Card */}
|
||||
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
|
||||
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-square w-[6rem] h-auto"
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
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
|
||||
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}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-lg font-bold"
|
||||
>
|
||||
%
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
dashboardData.topData
|
||||
?.innovation_budget_achievement_percent ||
|
||||
0,
|
||||
),
|
||||
)}
|
||||
</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>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-lg font-bold"
|
||||
>
|
||||
%
|
||||
{formatNumber(
|
||||
Math.round(
|
||||
dashboardData.topData
|
||||
?.innovation_budget_achievement_percent ||
|
||||
0
|
||||
)
|
||||
)}
|
||||
</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>
|
||||
</BaseCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</div>
|
||||
|
||||
{/* Main Content with Tabs */}
|
||||
<Tabs
|
||||
|
|
@ -589,10 +620,13 @@ export function DashboardHome() {
|
|||
تحقق ارزش ها
|
||||
</p>
|
||||
<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 value="charts" className=" text-white cursor-pointer font-light ">
|
||||
<TabsTrigger
|
||||
value="charts"
|
||||
className=" text-white cursor-pointer font-light "
|
||||
>
|
||||
مقایسه ای
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
|
@ -605,27 +639,25 @@ export function DashboardHome() {
|
|||
<TabsContent value="canvas" className="w-ful h-full">
|
||||
<div className="p-4 h-full w-full">
|
||||
<D3ImageInfo
|
||||
companies={
|
||||
companyChartData.map((item) => {
|
||||
const imageMap: Record<string, string> = {
|
||||
"بسپاران": "/besparan.png",
|
||||
"خوارزمی": "/khwarazmi.png",
|
||||
"فراورش 1": "/faravash1.png",
|
||||
"فراورش 2": "/faravash2.png",
|
||||
"کیمیا": "/kimia.png",
|
||||
"آب نیرو": "/abniro.png",
|
||||
};
|
||||
companies={companyChartData.map((item) => {
|
||||
const imageMap: Record<string, string> = {
|
||||
بسپاران: "/besparan.png",
|
||||
خوارزمی: "/khwarazmi.png",
|
||||
"فراورش 1": "/faravash1.png",
|
||||
"فراورش 2": "/faravash2.png",
|
||||
کیمیا: "/kimia.png",
|
||||
"آب نیرو": "/abniro.png",
|
||||
};
|
||||
|
||||
return {
|
||||
id: item.category,
|
||||
name: item.category,
|
||||
imageUrl: imageMap[item.category] || "/placeholder.png",
|
||||
cost: item?.costI || 0,
|
||||
capacity: item?.capacityI || 0,
|
||||
revenue: item?.revenueI || 0,
|
||||
};
|
||||
})
|
||||
}
|
||||
return {
|
||||
id: item.category,
|
||||
name: item.category,
|
||||
imageUrl: imageMap[item.category] || "/placeholder.png",
|
||||
cost: item?.costI || 0,
|
||||
capacity: item?.capacityI || 0,
|
||||
revenue: item?.revenueI || 0,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
@ -643,7 +675,7 @@ export function DashboardHome() {
|
|||
|
||||
<Progress
|
||||
value={parseFloat(
|
||||
dashboardData.leftData?.technology_intensity,
|
||||
dashboardData.leftData?.technology_intensity
|
||||
)}
|
||||
className="h-4 flex-1"
|
||||
/>
|
||||
|
|
@ -661,21 +693,21 @@ export function DashboardHome() {
|
|||
{
|
||||
label: "اجرا شده",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.executed_project || "0",
|
||||
dashboardData?.leftData?.executed_project || "0"
|
||||
),
|
||||
color: "bg-pr-green",
|
||||
},
|
||||
{
|
||||
label: "در حال اجرا",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.in_progress_project || "0",
|
||||
dashboardData?.leftData?.in_progress_project || "0"
|
||||
),
|
||||
color: "bg-pr-blue",
|
||||
},
|
||||
{
|
||||
label: "برنامهریزی شده",
|
||||
value: parseFloat(
|
||||
dashboardData?.leftData?.planned_project || "0",
|
||||
dashboardData?.leftData?.planned_project || "0"
|
||||
),
|
||||
color: "bg-pr-red",
|
||||
},
|
||||
|
|
@ -700,7 +732,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.printed_books_count || "0",
|
||||
dashboardData.leftData?.printed_books_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -711,7 +743,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.registered_patents_count || "0",
|
||||
dashboardData.leftData?.registered_patents_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -722,7 +754,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.published_reports_count || "0",
|
||||
dashboardData.leftData?.published_reports_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -733,7 +765,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.printed_articles_count || "0",
|
||||
dashboardData.leftData?.printed_articles_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -757,7 +789,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_conferences_count || "0",
|
||||
dashboardData.leftData?.attended_conferences_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -768,7 +800,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_events_count || "0",
|
||||
dashboardData.leftData?.attended_events_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -779,7 +811,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.attended_exhibitions_count || "0",
|
||||
dashboardData.leftData?.attended_exhibitions_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -790,7 +822,7 @@ export function DashboardHome() {
|
|||
</div>
|
||||
<span className="text-base font-bold ">
|
||||
{formatNumber(
|
||||
dashboardData.leftData?.organized_events_count || "0",
|
||||
dashboardData.leftData?.organized_events_count || "0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -798,9 +830,8 @@ export function DashboardHome() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useAuth } from "~/contexts/auth-context";
|
||||
import { Link } from "react-router";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
Menu,
|
||||
PanelLeft,
|
||||
|
||||
Server,
|
||||
Settings,
|
||||
User,
|
||||
|
||||
Menu,
|
||||
ChevronDown,
|
||||
Server,
|
||||
ChevronLeft ,
|
||||
|
||||
} 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 { cn, EventBus } from "~/lib/utils";
|
||||
|
||||
interface HeaderProps {
|
||||
onToggleSidebar?: () => void;
|
||||
|
|
@ -24,6 +23,52 @@ interface HeaderProps {
|
|||
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({
|
||||
onToggleSidebar,
|
||||
className,
|
||||
|
|
@ -31,25 +76,131 @@ export function Header({
|
|||
titleIcon,
|
||||
}: HeaderProps) {
|
||||
const { user } = useAuth();
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
const [isNotificationOpen, setIsNotificationOpen] = useState(false);
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
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 getData = await apiService.post("/GenerateSsoCode");
|
||||
const url = `https://inogen-bpms.pelekan.org/redirect/${getData.data}`;
|
||||
window.open(url, "_blank");
|
||||
} catch (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 (
|
||||
<header
|
||||
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",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* Left Section */}
|
||||
|
|
@ -69,24 +220,74 @@ export function Header({
|
|||
{/* Page Title */}
|
||||
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
||||
{/* Right-side icon for current page */}
|
||||
{titleIcon ? (
|
||||
{titleIcon ? (
|
||||
<div className="flex items-center gap-2 mr-4">
|
||||
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
||||
</div>
|
||||
) : (
|
||||
<PanelLeft />
|
||||
)}
|
||||
{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
|
||||
)}
|
||||
|
||||
{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
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Right Section */}
|
||||
|
|
@ -94,14 +295,15 @@ export function Header({
|
|||
{/* User Menu */}
|
||||
<div className="relative">
|
||||
<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"
|
||||
onClick={redirectHandler}>
|
||||
onClick={redirectHandler}
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
ورود به میزکار مدیریت</button>
|
||||
}
|
||||
ورود به میزکار مدیریت
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -109,7 +311,6 @@ export function Header({
|
|||
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||||
className="flex items-center gap-2 text-gray-300"
|
||||
>
|
||||
|
||||
<div className="hidden sm:block text-right">
|
||||
<div className="text-sm font-medium font-persian">
|
||||
{user?.name} {user?.family}
|
||||
|
|
@ -118,7 +319,7 @@ export function Header({
|
|||
{user?.username}
|
||||
</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" />
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
|
||||
import apiService from "~/lib/api";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -18,9 +17,14 @@ export function DashboardLayout({
|
|||
}: DashboardLayoutProps) {
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>(title ?? "صفحه اول");
|
||||
const [currentTitleIcon, setCurrentTitleIcon] = useState<React.ComponentType<{ className?: string }> | null | undefined>(undefined);
|
||||
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] =
|
||||
useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState<string | undefined>(
|
||||
title ?? "صفحه اول"
|
||||
);
|
||||
const [currentTitleIcon, setCurrentTitleIcon] = useState<
|
||||
React.ComponentType<{ className?: string }> | null | undefined
|
||||
>(undefined);
|
||||
|
||||
const toggleSidebarCollapse = () => {
|
||||
setIsSidebarCollapsed(!isSidebarCollapsed);
|
||||
|
|
@ -30,8 +34,6 @@ export function DashboardLayout({
|
|||
setIsMobileSidebarOpen(!isMobileSidebarOpen);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
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",
|
||||
isMobileSidebarOpen
|
||||
? "translate-x-0"
|
||||
: "translate-x-full lg:translate-x-0",
|
||||
: "translate-x-full lg:translate-x-0"
|
||||
)}
|
||||
>
|
||||
<Sidebar
|
||||
isCollapsed={isSidebarCollapsed}
|
||||
onToggleCollapse={toggleSidebarCollapse}
|
||||
className="h-full flex-shrink-0 relative z-10"
|
||||
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
|
||||
onStrategicAlignmentClick={() =>
|
||||
setIsStrategicAlignmentPopupOpen(true)
|
||||
}
|
||||
onTitleChange={(info) => {
|
||||
setCurrentTitle(info.title);
|
||||
setCurrentTitleIcon(info.icon ?? null);
|
||||
}}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -85,7 +88,7 @@ export function DashboardLayout({
|
|||
<main
|
||||
className={cn(
|
||||
"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">
|
||||
|
|
@ -93,7 +96,10 @@ export function DashboardLayout({
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
|
||||
<StrategicAlignmentPopup
|
||||
open={isStrategicAlignmentPopupOpen}
|
||||
onOpenChange={setIsStrategicAlignmentPopupOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
BrainCircuit,
|
||||
ChevronDown,
|
||||
|
|
@ -12,7 +13,7 @@ import {
|
|||
Zap,
|
||||
} from "lucide-react";
|
||||
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 { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
|
@ -34,7 +35,8 @@ import {
|
|||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
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";
|
||||
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
|
|
@ -146,13 +148,18 @@ const columns = [
|
|||
];
|
||||
|
||||
export function DigitalInnovationPage() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [projects, setProjects] = useState<DigitalInnovationMetrics[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
// const [totalCount, setTotalCount] = useState(0);
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [rating, setRating] = useState<ListItem[]>([]);
|
||||
|
|
@ -281,7 +288,11 @@ export function DigitalInnovationPage() {
|
|||
"reduce_costs_percent",
|
||||
],
|
||||
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 },
|
||||
});
|
||||
|
||||
|
|
@ -294,16 +305,16 @@ export function DigitalInnovationPage() {
|
|||
if (reset) {
|
||||
setProjects(parsedData);
|
||||
// calculateAverage(parsedData);
|
||||
setTotalCount(parsedData.length);
|
||||
// setTotalCount(parsedData.length);
|
||||
} else {
|
||||
setProjects((prev) => [...prev, ...parsedData]);
|
||||
setTotalCount((prev) => prev + parsedData.length);
|
||||
// setTotalCount((prev) => prev + parsedData.length);
|
||||
}
|
||||
setHasMore(parsedData.length === pageSize);
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -311,14 +322,14 @@ export function DigitalInnovationPage() {
|
|||
console.error("Error parsing project data:", parseError);
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -326,7 +337,7 @@ export function DigitalInnovationPage() {
|
|||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -335,7 +346,7 @@ export function DigitalInnovationPage() {
|
|||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
|
|
@ -356,7 +367,15 @@ export function DigitalInnovationPage() {
|
|||
fetchTable(true);
|
||||
fetchTotalCount();
|
||||
fetchStats();
|
||||
}, [sortConfig]);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -412,19 +431,23 @@ export function DigitalInnovationPage() {
|
|||
direction:
|
||||
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
|
||||
}));
|
||||
fetchTotalCount();
|
||||
fetchStats();
|
||||
fetchTotalCount(date?.start, date?.end);
|
||||
fetchStats(date?.start, date?.end);
|
||||
setCurrentPage(1);
|
||||
setProjects([]);
|
||||
setHasMore(true);
|
||||
};
|
||||
|
||||
const fetchTotalCount = async () => {
|
||||
const fetchTotalCount = async (startDate?: string, endDate?: string) => {
|
||||
try {
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
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) {
|
||||
|
|
@ -451,7 +474,10 @@ export function DigitalInnovationPage() {
|
|||
try {
|
||||
setStatsLoading(true);
|
||||
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;
|
||||
|
|
@ -529,33 +555,33 @@ export function DigitalInnovationPage() {
|
|||
// fetchStats();
|
||||
// };
|
||||
|
||||
const renderProgress = useMemo(() => {
|
||||
const total = 10;
|
||||
for (let i = 0; i < rating.length; i++) {
|
||||
const currentElm = rating[i];
|
||||
currentElm.house = [];
|
||||
const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
||||
const partialPercent =
|
||||
(total * currentElm.development) / 100 - greenBoxes;
|
||||
for (let j = 0; j < greenBoxes; j++) {
|
||||
currentElm.house.push({
|
||||
index: j,
|
||||
color: "!bg-emerald-400",
|
||||
});
|
||||
}
|
||||
if (partialPercent != 0 && greenBoxes != 10)
|
||||
currentElm.house.push({
|
||||
index: greenBoxes + 1,
|
||||
style: `linear-gradient(
|
||||
to right,
|
||||
oklch(76.5% 0.177 163.223) 0%,
|
||||
oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
||||
oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
||||
oklch(55.1% 0.027 264.364) 100%
|
||||
)`,
|
||||
});
|
||||
}
|
||||
}, [rating]);
|
||||
// const renderProgress = useMemo(() => {
|
||||
// const total = 10;
|
||||
// for (let i = 0; i < rating.length; i++) {
|
||||
// const currentElm = rating[i];
|
||||
// currentElm.house = [];
|
||||
// const greenBoxes = Math.floor((total * currentElm.development) / 100);
|
||||
// const partialPercent =
|
||||
// (total * currentElm.development) / 100 - greenBoxes;
|
||||
// for (let j = 0; j < greenBoxes; j++) {
|
||||
// currentElm.house.push({
|
||||
// index: j,
|
||||
// color: "!bg-emerald-400",
|
||||
// });
|
||||
// }
|
||||
// if (partialPercent != 0 && greenBoxes != 10)
|
||||
// currentElm.house.push({
|
||||
// index: greenBoxes + 1,
|
||||
// style: `linear-gradient(
|
||||
// to right,
|
||||
// oklch(76.5% 0.177 163.223) 0%,
|
||||
// oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
|
||||
// oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
|
||||
// oklch(55.1% 0.027 264.364) 100%
|
||||
// )`,
|
||||
// });
|
||||
// }
|
||||
// }, [rating]);
|
||||
|
||||
const statusColor = (status: projectStatus): any => {
|
||||
let el = null;
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { EventBus, formatNumber } from "~/lib/utils";
|
||||
|
||||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
Building2,
|
||||
ChevronDown,
|
||||
|
|
@ -46,6 +47,7 @@ import {
|
|||
import toast from "react-hot-toast";
|
||||
import apiService from "~/lib/api";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import DashboardLayout from "../layout";
|
||||
|
||||
// moment.loadPersian({ usePersianDigits: true });
|
||||
|
|
@ -157,6 +159,7 @@ const columns = [
|
|||
];
|
||||
|
||||
export function GreenInnovationPage() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [projects, setProjects] = useState<GreenInnovationData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
|
@ -166,6 +169,10 @@ export function GreenInnovationPage() {
|
|||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
const [stats, setStats] = useState<stateCounter>();
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||
field: "start_date",
|
||||
|
|
@ -288,7 +295,11 @@ export function GreenInnovationPage() {
|
|||
"observer",
|
||||
],
|
||||
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 },
|
||||
});
|
||||
if (response.state === 0) {
|
||||
|
|
@ -350,6 +361,14 @@ export function GreenInnovationPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
|
|
@ -359,11 +378,11 @@ export function GreenInnovationPage() {
|
|||
useEffect(() => {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}, [sortConfig]);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
}, [selectedProjects, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -416,7 +435,11 @@ export function GreenInnovationPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
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) {
|
||||
const dataString = response.data;
|
||||
|
|
@ -448,6 +471,8 @@ export function GreenInnovationPage() {
|
|||
selectedProjects.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
let payload: any = raw?.data;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
|
||||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
|
|
@ -40,7 +41,8 @@ import {
|
|||
XAxis,
|
||||
} from "recharts";
|
||||
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";
|
||||
|
||||
interface innovationBuiltInDate {
|
||||
|
|
@ -177,6 +179,7 @@ const dialogChartData = [
|
|||
];
|
||||
|
||||
export function InnovationBuiltInsidePage() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [projects, setProjects] = useState<innovationBuiltInDate[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
|
@ -191,6 +194,10 @@ export function InnovationBuiltInsidePage() {
|
|||
field: "start_date",
|
||||
direction: "asc",
|
||||
});
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
const [tblAvarage, setTblAvarage] = useState<number>(0);
|
||||
const [selectedProjects, setSelectedProjects] =
|
||||
useState<Set<string | number>>();
|
||||
|
|
@ -310,7 +317,11 @@ export function InnovationBuiltInsidePage() {
|
|||
"technology_maturity_level",
|
||||
],
|
||||
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 },
|
||||
});
|
||||
if (response.state === 0) {
|
||||
|
|
@ -416,13 +427,21 @@ export function InnovationBuiltInsidePage() {
|
|||
}
|
||||
}, [hasMore, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects(true);
|
||||
}, [sortConfig]);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
}, [selectedProjects, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -480,6 +499,8 @@ export function InnovationBuiltInsidePage() {
|
|||
selectedProjects && selectedProjects?.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
let payload: any = raw?.data;
|
||||
|
|
@ -624,7 +645,8 @@ export function InnovationBuiltInsidePage() {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
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>
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,3 +1,4 @@
|
|||
import jalaali from "jalaali-js";
|
||||
import {
|
||||
Building2,
|
||||
ChevronDown,
|
||||
|
|
@ -35,7 +36,8 @@ import {
|
|||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
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";
|
||||
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
|
|
@ -117,13 +119,18 @@ const columns = [
|
|||
];
|
||||
|
||||
export function ProcessInnovationPage() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [projects, setProjects] = useState<ProcessInnovationData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(20);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
// const [totalCount, setTotalCount] = useState(0);
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
const [actualTotalCount, setActualTotalCount] = useState(0);
|
||||
const [statsLoading, setStatsLoading] = useState(false);
|
||||
const [stats, setStats] = useState<InnovationStats>({
|
||||
|
|
@ -196,13 +203,13 @@ export function ProcessInnovationPage() {
|
|||
const fetchingRef = useRef(false);
|
||||
|
||||
// Selection handlers
|
||||
const handleSelectAll = () => {
|
||||
if (selectedProjects.size === projects.length) {
|
||||
setSelectedProjects(new Set());
|
||||
} else {
|
||||
setSelectedProjects(new Set(projects.map((p) => p.project_no)));
|
||||
}
|
||||
};
|
||||
// const handleSelectAll = () => {
|
||||
// if (selectedProjects.size === projects.length) {
|
||||
// setSelectedProjects(new Set());
|
||||
// } else {
|
||||
// setSelectedProjects(new Set(projects.map((p) => p.project_no)));
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleSelectProject = (projectNo: string) => {
|
||||
const newSelected = new Set(selectedProjects);
|
||||
|
|
@ -256,7 +263,11 @@ export function ProcessInnovationPage() {
|
|||
"observer",
|
||||
],
|
||||
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 },
|
||||
});
|
||||
|
||||
|
|
@ -268,16 +279,16 @@ export function ProcessInnovationPage() {
|
|||
if (Array.isArray(parsedData)) {
|
||||
if (reset) {
|
||||
setProjects(parsedData);
|
||||
setTotalCount(parsedData.length);
|
||||
// setTotalCount(parsedData.length);
|
||||
} else {
|
||||
setProjects((prev) => [...prev, ...parsedData]);
|
||||
setTotalCount((prev) => prev + parsedData.length);
|
||||
// setTotalCount((prev) => prev + parsedData.length);
|
||||
}
|
||||
setHasMore(parsedData.length === pageSize);
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -285,14 +296,14 @@ export function ProcessInnovationPage() {
|
|||
console.error("Error parsing project data:", parseError);
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
} else {
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -300,7 +311,7 @@ export function ProcessInnovationPage() {
|
|||
toast.error(response.message || "خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
}
|
||||
|
|
@ -309,7 +320,7 @@ export function ProcessInnovationPage() {
|
|||
toast.error("خطا در دریافت اطلاعات پروژهها");
|
||||
if (reset) {
|
||||
setProjects([]);
|
||||
setTotalCount(0);
|
||||
// setTotalCount(0);
|
||||
}
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
|
|
@ -325,14 +336,22 @@ export function ProcessInnovationPage() {
|
|||
}
|
||||
}, [hasMore, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}, [sortConfig]);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [selectedProjects]);
|
||||
}, [selectedProjects, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -382,7 +401,11 @@ export function ProcessInnovationPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
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) {
|
||||
|
|
@ -416,6 +439,8 @@ export function ProcessInnovationPage() {
|
|||
selectedProjects.size > 0
|
||||
? Array.from(selectedProjects).join(" , ")
|
||||
: "",
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,5 +1,6 @@
|
|||
import jalaali from "jalaali-js";
|
||||
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 { Badge } from "~/components/ui/badge";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
|
|
@ -13,8 +14,8 @@ import {
|
|||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import apiService from "~/lib/api";
|
||||
import { formatCurrency } from "~/lib/utils";
|
||||
import { formatNumber } from "~/lib/utils";
|
||||
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
|
||||
import type { CalendarDate } from "~/types/util.type";
|
||||
import { DashboardLayout } from "../layout";
|
||||
|
||||
interface ProjectData {
|
||||
|
|
@ -153,6 +154,7 @@ const columns: ColumnDef[] = [
|
|||
];
|
||||
|
||||
export function ProjectManagementPage() {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [projects, setProjects] = useState<ProjectData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
|
@ -169,6 +171,10 @@ export function ProjectManagementPage() {
|
|||
const fetchingRef = useRef(false);
|
||||
const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
|
||||
const fetchProjects = async (reset = false) => {
|
||||
// Prevent concurrent API calls
|
||||
|
|
@ -200,7 +206,10 @@ export function ProjectManagementPage() {
|
|||
OutputFields: outputFields,
|
||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||
Sorts: sortField ? [[sortField, sortConfig.direction]] : [],
|
||||
Conditions: [],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -265,6 +274,13 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading && !loadingMore && !fetchingRef.current) {
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
|
|
@ -274,7 +290,7 @@ export function ProjectManagementPage() {
|
|||
useEffect(() => {
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
}, [sortConfig]);
|
||||
}, [sortConfig, date]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > 1) {
|
||||
|
|
@ -287,7 +303,8 @@ export function ProjectManagementPage() {
|
|||
const scrollContainer = scrollContainerRef.current;
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
|
||||
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current)
|
||||
return;
|
||||
|
||||
// Clear previous timeout
|
||||
if (scrollTimeoutRef.current) {
|
||||
|
|
@ -307,7 +324,9 @@ export function ProjectManagementPage() {
|
|||
};
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
||||
scrollContainer.addEventListener("scroll", handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
@ -337,7 +356,10 @@ export function ProjectManagementPage() {
|
|||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: ["count(project_no)"],
|
||||
Conditions: [],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
});
|
||||
|
||||
if (response.state === 0) {
|
||||
|
|
@ -358,14 +380,14 @@ export function ProjectManagementPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchingRef.current = false; // Reset fetching state on refresh
|
||||
setCurrentPage(1);
|
||||
setProjects([]);
|
||||
setHasMore(true);
|
||||
fetchProjects(true);
|
||||
fetchTotalCount();
|
||||
};
|
||||
// const handleRefresh = () => {
|
||||
// fetchingRef.current = false; // Reset fetching state on refresh
|
||||
// setCurrentPage(1);
|
||||
// setProjects([]);
|
||||
// setHasMore(true);
|
||||
// fetchProjects(true);
|
||||
// fetchTotalCount();
|
||||
// };
|
||||
|
||||
// ...existing code...
|
||||
|
||||
|
|
@ -630,7 +652,7 @@ export function ProjectManagementPage() {
|
|||
.filter((v) => v !== null) as number[];
|
||||
res["remaining_time"] = remainingValues.length
|
||||
? Math.round(
|
||||
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length,
|
||||
remainingValues.reduce((a, b) => a + b, 0) / remainingValues.length
|
||||
)
|
||||
: null;
|
||||
|
||||
|
|
@ -644,7 +666,7 @@ export function ProjectManagementPage() {
|
|||
const num = Number(
|
||||
String(raw)
|
||||
.toString()
|
||||
.replace(/[^0-9.-]/g, ""),
|
||||
.replace(/[^0-9.-]/g, "")
|
||||
);
|
||||
return Number.isFinite(num) ? num : NaN;
|
||||
})
|
||||
|
|
@ -770,7 +792,10 @@ export function ProjectManagementPage() {
|
|||
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
<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">
|
||||
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
|
||||
<TableRow className="bg-[#3F415A]">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import jalaali from "jalaali-js";
|
||||
import { useEffect, useReducer, useRef, useState } from "react";
|
||||
import {
|
||||
Bar,
|
||||
|
|
@ -12,7 +13,8 @@ import {
|
|||
import { Dialog, DialogContent, DialogHeader } from "~/components/ui/dialog";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
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 {
|
||||
DropdownMenu,
|
||||
|
|
@ -116,6 +118,7 @@ export function StrategicAlignmentPopup({
|
|||
open,
|
||||
onOpenChange,
|
||||
}: StrategicAlignmentPopupProps) {
|
||||
const { jy } = jalaali.toJalaali(new Date());
|
||||
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -125,22 +128,35 @@ export function StrategicAlignmentPopup({
|
|||
dropDownItems: [],
|
||||
});
|
||||
|
||||
const [date, setDate] = useState<CalendarDate>({
|
||||
start: `${jy}/01/01`,
|
||||
end: `${jy}/12/30`,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchData();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiService.select({
|
||||
ProcessName: "project",
|
||||
OutputFields: [
|
||||
"strategic_theme",
|
||||
"count(operational_fee)",
|
||||
],
|
||||
OutputFields: ["strategic_theme", "count(operational_fee)"],
|
||||
GroupBy: ["strategic_theme"],
|
||||
Conditions: [
|
||||
["start_date", ">=", date?.start || null, "and"],
|
||||
["start_date", "<=", date?.end || null],
|
||||
],
|
||||
});
|
||||
|
||||
const responseData =
|
||||
|
|
@ -170,7 +186,11 @@ export function StrategicAlignmentPopup({
|
|||
"value_technology_and_innovation",
|
||||
"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"],
|
||||
});
|
||||
|
||||
|
|
@ -247,7 +267,9 @@ export function StrategicAlignmentPopup({
|
|||
(item: StrategicAlignmentData) => ({
|
||||
...item,
|
||||
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 || []);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
|
|
@ -11,9 +10,11 @@ import {
|
|||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||
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 {
|
||||
id: string;
|
||||
|
|
@ -62,37 +63,52 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
|
||||
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [date, setDate] = useState<CalendarDate>();
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
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();
|
||||
}, []);
|
||||
}, [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
|
||||
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
|
||||
const processYearsData = (
|
||||
data: ProcessActorsResponse[],
|
||||
data: ProcessActorsResponse[]
|
||||
): ProcessActorsData[] => {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
|
|
@ -121,7 +137,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
acc[item.start_year] = item.total_count;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
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]">
|
||||
<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)}
|
||||
</span>
|
||||
</CardTitle>
|
||||
|
|
@ -432,7 +448,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
|
||||
<div className="w-full">
|
||||
<CustomBarChart
|
||||
hasPercent={false}
|
||||
hasPercent={false}
|
||||
data={barData.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
|
|
@ -455,70 +471,82 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
</div>
|
||||
<div className="h-42">
|
||||
{processData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={processData}
|
||||
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={processData}
|
||||
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="fillDesktop"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
||||
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="year"
|
||||
stroke="#9ca3af"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
tickFormatter={formatPersianYear}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#9ca3af"
|
||||
fontSize={12}
|
||||
tickMargin={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatNumber(value)}
|
||||
/>
|
||||
<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>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="year"
|
||||
stroke="#9ca3af"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
tickFormatter={formatPersianYear}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#9ca3af"
|
||||
fontSize={12}
|
||||
tickMargin={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatNumber(value)}
|
||||
/>
|
||||
<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>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
|
||||
دادهای برای نمایش وجود ندارد
|
||||
|
|
@ -526,7 +554,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
|||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
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 apiService from "../../lib/api";
|
||||
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
||||
|
|
@ -59,7 +61,10 @@ function isBrowser(): boolean {
|
|||
return typeof window !== "undefined";
|
||||
}
|
||||
|
||||
export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps) {
|
||||
export function NetworkGraph({
|
||||
onNodeClick,
|
||||
onLoadingChange,
|
||||
}: NetworkGraphProps) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [links, setLinks] = useState<Link[]>([]);
|
||||
|
|
@ -68,6 +73,15 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const { token } = useAuth();
|
||||
|
||||
const [date, setDate] = useState<CalendarDate>();
|
||||
useEffect(() => {
|
||||
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||
if (date) {
|
||||
setDate(date);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser()) {
|
||||
const timer = setTimeout(() => setIsMounted(true), 100);
|
||||
|
|
@ -80,16 +94,21 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
if (!token?.accessToken) return null;
|
||||
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token.accessToken}`;
|
||||
},
|
||||
[token?.accessToken],
|
||||
[token?.accessToken]
|
||||
);
|
||||
|
||||
const callAPI = useCallback(async (stage_id: number) => {
|
||||
return await apiService.call<any>({
|
||||
get_values_workflow_function: {
|
||||
stage_id: stage_id,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
const callAPI = useCallback(
|
||||
async (stage_id: number) => {
|
||||
return await apiService.call<any>({
|
||||
get_values_workflow_function: {
|
||||
stage_id: stage_id,
|
||||
start_date: date?.start || null,
|
||||
end_date: date?.end || null,
|
||||
},
|
||||
});
|
||||
},
|
||||
[date]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
|
|
@ -108,7 +127,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
const data = parseApiResponse(JSON.parse(res.data)?.graph_production);
|
||||
console.log(
|
||||
"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) => ({
|
||||
id: `cat-${index}`,
|
||||
|
|
@ -170,7 +191,8 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
}, [isMounted, token, getImageUrl]);
|
||||
|
||||
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 width = svgRef.current.clientWidth;
|
||||
|
|
@ -225,12 +247,18 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
.forceLink<Node, Link>(links)
|
||||
.id((d) => d.id)
|
||||
.distance(150)
|
||||
.strength(0.2),
|
||||
.strength(0.2)
|
||||
)
|
||||
.force("charge", d3.forceManyBody().strength(-300))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2))
|
||||
.force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)));
|
||||
.force(
|
||||
"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
|
||||
const initialScale = 0.6;
|
||||
|
|
@ -242,12 +270,12 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
zoom.transform,
|
||||
d3.zoomIdentity
|
||||
.translate(initialTranslate[0], initialTranslate[1])
|
||||
.scale(initialScale),
|
||||
.scale(initialScale)
|
||||
);
|
||||
|
||||
// Fix center node
|
||||
const centerNode = nodes.find(n => n.isCenter);
|
||||
const categoryNodes = nodes.filter(n => !n.isCenter && n.stageid === -1);
|
||||
const centerNode = nodes.find((n) => n.isCenter);
|
||||
const categoryNodes = nodes.filter((n) => !n.isCenter && n.stageid === -1);
|
||||
|
||||
if (centerNode) {
|
||||
const centerX = width / 2;
|
||||
|
|
@ -270,22 +298,20 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
// نودهای نهایی **هیچ fx/fy نداشته باشند**
|
||||
// فقط forceLink آنها را به دستهها متصل نگه میدارد
|
||||
|
||||
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
|
||||
|
||||
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
|
||||
|
||||
// categoryNodes.forEach((catNode) => {
|
||||
// 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) => {
|
||||
// const angle = i * angleStep;
|
||||
// node.fx = catNode.fx! + radius * Math.cos(angle);
|
||||
// node.fy = catNode.fy! + radius * Math.sin(angle);
|
||||
// });
|
||||
// });
|
||||
// categoryNodes.forEach((catNode) => {
|
||||
// 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) => {
|
||||
// const angle = i * angleStep;
|
||||
// node.fx = catNode.fx! + radius * Math.cos(angle);
|
||||
// node.fy = catNode.fy! + radius * Math.sin(angle);
|
||||
// });
|
||||
// });
|
||||
|
||||
// Curved links
|
||||
const link = container
|
||||
|
|
@ -305,7 +331,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
.enter()
|
||||
.append("g")
|
||||
.attr("class", "node")
|
||||
.style("cursor", d => d.stageid === -1 ? "default" : "pointer");
|
||||
.style("cursor", (d) => (d.stageid === -1 ? "default" : "pointer"));
|
||||
|
||||
const drag = d3
|
||||
.drag<SVGGElement, Node>()
|
||||
|
|
@ -337,7 +363,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
.attr("width", 200)
|
||||
.attr("height", 80)
|
||||
.attr("x", -100) // نصف عرض جدید منفی
|
||||
.attr("y", -40) // نصف ارتفاع جدید منفی
|
||||
.attr("y", -40) // نصف ارتفاع جدید منفی
|
||||
.attr("rx", 8)
|
||||
.attr("ry", 8)
|
||||
.attr("fill", categoryToColor[d.category] || "#94A3B8")
|
||||
|
|
@ -358,7 +384,7 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
.append("image")
|
||||
.attr("x", 0)
|
||||
.attr("y", 0)
|
||||
.attr("width", 200) // ← هماندازه با مستطیل
|
||||
.attr("width", 200) // ← هماندازه با مستطیل
|
||||
.attr("height", 80)
|
||||
.attr("href", d.isCenter ? "/main-circle.png" : d.imageUrl)
|
||||
.attr("preserveAspectRatio", "xMidYMid slice");
|
||||
|
|
@ -437,12 +463,11 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
.attr("stroke-width", 3);
|
||||
});
|
||||
|
||||
|
||||
nodeGroup.on("click", async function (event, d) {
|
||||
event.stopPropagation();
|
||||
|
||||
// جلوگیری از کلیک روی مرکز و دستهبندیها
|
||||
if (d.isCenter || d.stageid === -1) return;
|
||||
// جلوگیری از کلیک روی مرکز و دستهبندیها
|
||||
if (d.isCenter || d.stageid === -1) return;
|
||||
|
||||
if (onNodeClick && d.stageid) {
|
||||
// Open dialog immediately with basic info
|
||||
|
|
@ -467,15 +492,15 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
const filteredFields = fieldValues.filter(
|
||||
(field: any) =>
|
||||
!["image", "img", "full_name", "about_collaboration"].includes(
|
||||
field.F.toLowerCase(),
|
||||
),
|
||||
field.F.toLowerCase()
|
||||
)
|
||||
);
|
||||
|
||||
const descriptionField = fieldValues.find(
|
||||
(field: any) =>
|
||||
field.F.toLowerCase().includes("description") ||
|
||||
field.F.toLowerCase().includes("about_collaboration") ||
|
||||
field.F.toLowerCase().includes("about"),
|
||||
field.F.toLowerCase().includes("about")
|
||||
);
|
||||
|
||||
const companyDetails: CompanyDetails = {
|
||||
|
|
@ -592,5 +617,4 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
export default NetworkGraph;
|
||||
67
app/components/ui/Calendar.tsx
Normal file
67
app/components/ui/Calendar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import EventEmitter from "events";
|
||||
import moment from "moment-jalaali";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
|
@ -22,8 +23,6 @@ export const formatCurrency = (amount: string | number) => {
|
|||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* محاسبه دامنه nice numbers برای محور Y نمودارها
|
||||
* @param values آرایه از مقادیر دادهها
|
||||
|
|
@ -116,8 +115,8 @@ function calculateNiceNumber(value: number, round: boolean): number {
|
|||
return niceFraction * Math.pow(10, exponent);
|
||||
}
|
||||
|
||||
export const handleDataValue = (val: any): any => {
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
export const handleDataValue = (val: any): any => {
|
||||
moment.loadPersian({ usePersianDigits: true });
|
||||
if (val == null) return val;
|
||||
if (
|
||||
typeof val === "string" &&
|
||||
|
|
@ -132,4 +131,6 @@ moment.loadPersian({ usePersianDigits: true });
|
|||
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
};
|
||||
|
||||
export const EventBus = new EventEmitter();
|
||||
|
|
|
|||
6
app/types/util.type.ts
Normal file
6
app/types/util.type.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export interface CalendarDate {
|
||||
start: string;
|
||||
end: string;
|
||||
sinceMonth?: string;
|
||||
untilMonth?: string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user