Compare commits
4 Commits
8749cebe7c
...
0dd1fe2ec2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dd1fe2ec2 | ||
|
|
efa46a02c2 | ||
|
|
bda2e62411 | ||
|
|
173176bbb5 |
|
|
@ -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,16 +471,34 @@ 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="درصد به کل هزینه"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -480,7 +515,7 @@ export function DashboardHome() {
|
||||||
browser: "budget",
|
browser: "budget",
|
||||||
visitors: parseFloat(
|
visitors: parseFloat(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.innovation_budget_achievement_percent || "0",
|
?.innovation_budget_achievement_percent || "0"
|
||||||
),
|
),
|
||||||
fill: "var(--color-green)",
|
fill: "var(--color-green)",
|
||||||
},
|
},
|
||||||
|
|
@ -503,11 +538,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}
|
||||||
|
|
@ -533,8 +564,8 @@ export function DashboardHome() {
|
||||||
Math.round(
|
Math.round(
|
||||||
dashboardData.topData
|
dashboardData.topData
|
||||||
?.innovation_budget_achievement_percent ||
|
?.innovation_budget_achievement_percent ||
|
||||||
0,
|
0
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</tspan>
|
</tspan>
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -554,10 +585,10 @@ export function DashboardHome() {
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
||||||
/,/g,
|
/,/g,
|
||||||
"",
|
""
|
||||||
) || "0",
|
) || "0"
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
||||||
|
|
@ -567,10 +598,10 @@ export function DashboardHome() {
|
||||||
parseFloat(
|
parseFloat(
|
||||||
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
||||||
/,/g,
|
/,/g,
|
||||||
"",
|
""
|
||||||
) || "0",
|
) || "0"
|
||||||
),
|
)
|
||||||
),
|
)
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -592,7 +623,10 @@ export function DashboardHome() {
|
||||||
<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,14 +639,13 @@ 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",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -624,8 +657,7 @@ export function DashboardHome() {
|
||||||
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>
|
||||||
|
|
@ -800,7 +832,6 @@ export function DashboardHome() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
@ -77,16 +228,66 @@ export function Header({
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
)}
|
)}
|
||||||
{title.includes("-") ? (
|
{title.includes("-") ? (
|
||||||
<span className="flex items-center gap-1">
|
<div className="flex row items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
{title.split("-")[0]}
|
{title.split("-")[0]}
|
||||||
<ChevronLeft className="inline-block w-4 h-4" />
|
<ChevronLeft className="inline-block w-4 h-4" />
|
||||||
{title.split("-")[1]}
|
{title.split("-")[1]}
|
||||||
</span>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
|
) : (
|
||||||
title
|
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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,40 @@
|
||||||
import { ChevronDown, ChevronUp, RefreshCw, Eye, Star, TrendingUp, Hexagon, Download } from "lucide-react";
|
import jalaali from "jalaali-js";
|
||||||
import { useCallback, useEffect, useRef, useState, useMemo, memo } from "react";
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Download,
|
||||||
|
Hexagon,
|
||||||
|
RefreshCw,
|
||||||
|
Star,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { memo, 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 {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Label,
|
||||||
|
LabelList,
|
||||||
|
PolarGrid,
|
||||||
|
PolarRadiusAxis,
|
||||||
|
RadialBar,
|
||||||
|
RadialBarChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import { BaseCard } from "~/components/ui/base-card";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
import { ChartContainer, type ChartConfig } from "~/components/ui/chart";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
|
import { MetricCard } from "~/components/ui/metric-card";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -19,20 +44,9 @@ 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";
|
||||||
import {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
type ChartConfig,
|
|
||||||
} from "~/components/ui/chart";
|
|
||||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, CartesianGrid, LabelList, Cell, RadialBarChart, PolarGrid, RadialBar, PolarRadiusAxis } from "recharts";
|
|
||||||
import { BaseCard } from "~/components/ui/base-card";
|
|
||||||
import { Label } from "recharts"
|
|
||||||
import { MetricCard } from "~/components/ui/metric-card";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface IdeaData {
|
interface IdeaData {
|
||||||
idea_title: string;
|
idea_title: string;
|
||||||
|
|
@ -86,9 +100,19 @@ type ColumnDef = {
|
||||||
|
|
||||||
const columns: ColumnDef[] = [
|
const columns: ColumnDef[] = [
|
||||||
{ key: "idea_title", label: "عنوان ایده", sortable: true, width: "250px" },
|
{ key: "idea_title", label: "عنوان ایده", sortable: true, width: "250px" },
|
||||||
{ key: "idea_registration_date", label: "تاریخ ثبت ایده", sortable: true, width: "180px" },
|
{
|
||||||
|
key: "idea_registration_date",
|
||||||
|
label: "تاریخ ثبت ایده",
|
||||||
|
sortable: true,
|
||||||
|
width: "180px",
|
||||||
|
},
|
||||||
{ key: "idea_status", label: "وضعیت ایده", sortable: true, width: "150px" },
|
{ key: "idea_status", label: "وضعیت ایده", sortable: true, width: "150px" },
|
||||||
{ key: "increased_revenue", label: "درآمد حاصل از ایده", sortable: true, width: "180px" },
|
{
|
||||||
|
key: "increased_revenue",
|
||||||
|
label: "درآمد حاصل از ایده",
|
||||||
|
sortable: true,
|
||||||
|
width: "180px",
|
||||||
|
},
|
||||||
{ key: "details", label: "جزئیات بیشتر", sortable: false, width: "120px" },
|
{ key: "details", label: "جزئیات بیشتر", sortable: false, width: "120px" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -100,7 +124,15 @@ const VerticalBarChart = memo<{
|
||||||
getChartStatusColor: (status: string) => string;
|
getChartStatusColor: (status: string) => string;
|
||||||
toPersianDigits: (input: string | number) => string;
|
toPersianDigits: (input: string | number) => string;
|
||||||
formatNumber: (value: number) => string;
|
formatNumber: (value: number) => string;
|
||||||
}>(({ chartData, loadingChart, chartConfig, getChartStatusColor, toPersianDigits, formatNumber }) => {
|
}>(
|
||||||
|
({
|
||||||
|
chartData,
|
||||||
|
loadingChart,
|
||||||
|
chartConfig,
|
||||||
|
getChartStatusColor,
|
||||||
|
toPersianDigits,
|
||||||
|
formatNumber,
|
||||||
|
}) => {
|
||||||
if (loadingChart) {
|
if (loadingChart) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
|
|
@ -112,7 +144,10 @@ const VerticalBarChart = memo<{
|
||||||
{/* Y-axis labels */}
|
{/* Y-axis labels */}
|
||||||
<div className="absolute left-2 top-4 space-y-6">
|
<div className="absolute left-2 top-4 space-y-6">
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
<div key={i} className="h-3 bg-gray-600 rounded animate-pulse w-6"></div>
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-3 bg-gray-600 rounded animate-pulse w-6"
|
||||||
|
></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -138,18 +173,24 @@ const VerticalBarChart = memo<{
|
||||||
if (!chartData.length) {
|
if (!chartData.length) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 text-center">
|
<div className="p-6 text-center">
|
||||||
<h3 className="text-lg font-persian font-semibold text-white mb-4">وضعیت ایده ها</h3>
|
<h3 className="text-lg font-persian font-semibold text-white mb-4">
|
||||||
|
وضعیت ایده ها
|
||||||
|
</h3>
|
||||||
<p className="text-gray-400 font-persian">هیچ دادهای یافت نشد</p>
|
<p className="text-gray-400 font-persian">هیچ دادهای یافت نشد</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare data for recharts
|
// Prepare data for recharts
|
||||||
const rechartData = useMemo(() => chartData.map((item) => ({
|
const rechartData = useMemo(
|
||||||
|
() =>
|
||||||
|
chartData.map((item) => ({
|
||||||
status: item.idea_status,
|
status: item.idea_status,
|
||||||
count: item.idea_status_count,
|
count: item.idea_status_count,
|
||||||
fill: getChartStatusColor(item.idea_status),
|
fill: getChartStatusColor(item.idea_status),
|
||||||
})), [chartData, getChartStatusColor]);
|
})),
|
||||||
|
[chartData, getChartStatusColor]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%">
|
<ResponsiveContainer width="100%">
|
||||||
|
|
@ -167,9 +208,9 @@ const VerticalBarChart = memo<{
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tick={{
|
tick={{
|
||||||
fill: '#fff',
|
fill: "#fff",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'inherit'
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
interval={0}
|
interval={0}
|
||||||
angle={0}
|
angle={0}
|
||||||
|
|
@ -181,9 +222,9 @@ const VerticalBarChart = memo<{
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
tick={{
|
tick={{
|
||||||
fill: '#9CA3AF',
|
fill: "#9CA3AF",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'inherit'
|
fontFamily: "inherit",
|
||||||
}}
|
}}
|
||||||
tickFormatter={(value) => toPersianDigits(value)}
|
tickFormatter={(value) => toPersianDigits(value)}
|
||||||
label={{
|
label={{
|
||||||
|
|
@ -197,10 +238,7 @@ const VerticalBarChart = memo<{
|
||||||
style: { textAnchor: "middle" },
|
style: { textAnchor: "middle" },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
|
||||||
dataKey="count"
|
|
||||||
radius={[4, 4, 0, 0]}
|
|
||||||
>
|
|
||||||
<LabelList
|
<LabelList
|
||||||
dataKey="count"
|
dataKey="count"
|
||||||
position="top"
|
position="top"
|
||||||
|
|
@ -217,11 +255,13 @@ const VerticalBarChart = memo<{
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const MemoizedVerticalBarChart = VerticalBarChart;
|
const MemoizedVerticalBarChart = VerticalBarChart;
|
||||||
|
|
||||||
export function ManageIdeasTechPage() {
|
export function ManageIdeasTechPage() {
|
||||||
|
const { jy } = jalaali.toJalaali(new Date());
|
||||||
const [ideas, setIdeas] = useState<IdeaData[]>([]);
|
const [ideas, setIdeas] = useState<IdeaData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
|
@ -236,6 +276,10 @@ export function ManageIdeasTechPage() {
|
||||||
field: "idea_title",
|
field: "idea_title",
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
});
|
});
|
||||||
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
start: `${jy}/01/01`,
|
||||||
|
end: `${jy}/12/30`,
|
||||||
|
});
|
||||||
|
|
||||||
// People ranking state
|
// People ranking state
|
||||||
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
const [peopleRanking, setPeopleRanking] = useState<PersonRanking[]>([]);
|
||||||
|
|
@ -293,7 +337,10 @@ export function ManageIdeasTechPage() {
|
||||||
],
|
],
|
||||||
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
|
||||||
Sorts: [[sortConfig.field, sortConfig.direction]],
|
Sorts: [[sortConfig.field, sortConfig.direction]],
|
||||||
Conditions: [],
|
Conditions: [
|
||||||
|
["idea_registration_date", ">=", date?.start || null, "and"],
|
||||||
|
["idea_registration_date", "<=", date?.end || null],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -361,13 +408,21 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
}, [hasMore, loading, loadingMore]);
|
}, [hasMore, loading, loadingMore]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
|
if (date) {
|
||||||
|
setDate(date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchIdeas(true);
|
fetchIdeas(true);
|
||||||
fetchTotalCount();
|
fetchTotalCount();
|
||||||
fetchPeopleRanking();
|
fetchPeopleRanking();
|
||||||
fetchChartData();
|
fetchChartData();
|
||||||
fetchStatsData();
|
fetchStatsData();
|
||||||
}, [sortConfig]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -380,7 +435,8 @@ export function ManageIdeasTechPage() {
|
||||||
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;
|
||||||
|
|
||||||
if (scrollTimeoutRef.current) {
|
if (scrollTimeoutRef.current) {
|
||||||
clearTimeout(scrollTimeoutRef.current);
|
clearTimeout(scrollTimeoutRef.current);
|
||||||
|
|
@ -397,7 +453,9 @@ export function ManageIdeasTechPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scrollContainer) {
|
if (scrollContainer) {
|
||||||
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
|
scrollContainer.addEventListener("scroll", handleScroll, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -427,7 +485,10 @@ export function ManageIdeasTechPage() {
|
||||||
const response = await apiService.select({
|
const response = await apiService.select({
|
||||||
ProcessName: "idea",
|
ProcessName: "idea",
|
||||||
OutputFields: ["count(idea_title)"],
|
OutputFields: ["count(idea_title)"],
|
||||||
Conditions: [],
|
Conditions: [
|
||||||
|
["idea_registration_date", ">=", date?.start || null, "and"],
|
||||||
|
["idea_registration_date", "<=", date?.end || null],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -456,6 +517,10 @@ export function ManageIdeasTechPage() {
|
||||||
ProcessName: "idea",
|
ProcessName: "idea",
|
||||||
OutputFields: ["full_name", "count(full_name)"],
|
OutputFields: ["full_name", "count(full_name)"],
|
||||||
GroupBy: ["full_name"],
|
GroupBy: ["full_name"],
|
||||||
|
Conditions: [
|
||||||
|
["idea_registration_date", ">=", date?.start || null, "and"],
|
||||||
|
["idea_registration_date", "<=", date?.end || null],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -465,12 +530,14 @@ export function ManageIdeasTechPage() {
|
||||||
const parsedData = JSON.parse(dataString);
|
const parsedData = JSON.parse(dataString);
|
||||||
if (Array.isArray(parsedData)) {
|
if (Array.isArray(parsedData)) {
|
||||||
// Calculate rankings and stars
|
// Calculate rankings and stars
|
||||||
const counts = parsedData.map(item => item.full_name_count);
|
const counts = parsedData.map((item) => item.full_name_count);
|
||||||
const maxCount = Math.max(...counts);
|
const maxCount = Math.max(...counts);
|
||||||
const minCount = Math.min(...counts);
|
const minCount = Math.min(...counts);
|
||||||
|
|
||||||
// Sort by count first (highest first)
|
// Sort by count first (highest first)
|
||||||
const sortedData = parsedData.sort((a, b) => b.full_name_count - a.full_name_count);
|
const sortedData = parsedData.sort(
|
||||||
|
(a, b) => b.full_name_count - a.full_name_count
|
||||||
|
);
|
||||||
|
|
||||||
const rankedPeople = [];
|
const rankedPeople = [];
|
||||||
let currentRank = 1;
|
let currentRank = 1;
|
||||||
|
|
@ -480,11 +547,15 @@ export function ManageIdeasTechPage() {
|
||||||
const item = sortedData[i];
|
const item = sortedData[i];
|
||||||
|
|
||||||
// If this is not the first person and their count is different from previous
|
// If this is not the first person and their count is different from previous
|
||||||
if (i > 0 && sortedData[i - 1].full_name_count !== item.full_name_count) {
|
if (
|
||||||
|
i > 0 &&
|
||||||
|
sortedData[i - 1].full_name_count !== item.full_name_count
|
||||||
|
) {
|
||||||
currentRank = sum + 1; // New rank based on position
|
currentRank = sum + 1; // New rank based on position
|
||||||
sum++;
|
sum++;
|
||||||
}
|
}
|
||||||
const normalizedScore = maxCount === minCount
|
const normalizedScore =
|
||||||
|
maxCount === minCount
|
||||||
? 1
|
? 1
|
||||||
: (item.full_name_count - minCount) / (maxCount - minCount);
|
: (item.full_name_count - minCount) / (maxCount - minCount);
|
||||||
const stars = Math.max(1, Math.round(normalizedScore * 5));
|
const stars = Math.max(1, Math.round(normalizedScore * 5));
|
||||||
|
|
@ -503,7 +574,9 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.message || "خطا در دریافت اطلاعات رتبهبندی افراد");
|
toast.error(
|
||||||
|
response.message || "خطا در دریافت اطلاعات رتبهبندی افراد"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching people ranking:", error);
|
console.error("Error fetching people ranking:", error);
|
||||||
|
|
@ -521,6 +594,10 @@ export function ManageIdeasTechPage() {
|
||||||
ProcessName: "idea",
|
ProcessName: "idea",
|
||||||
OutputFields: ["idea_status", "count(idea_status)"],
|
OutputFields: ["idea_status", "count(idea_status)"],
|
||||||
GroupBy: ["idea_status"],
|
GroupBy: ["idea_status"],
|
||||||
|
Conditions: [
|
||||||
|
["idea_registration_date", ">=", date?.start || null, "and"],
|
||||||
|
["idea_registration_date", "<=", date?.end || null],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -551,7 +628,10 @@ export function ManageIdeasTechPage() {
|
||||||
setLoadingStats(true);
|
setLoadingStats(true);
|
||||||
|
|
||||||
const response = await apiService.call({
|
const response = await apiService.call({
|
||||||
idea_page_function: {}
|
idea_page_function: {
|
||||||
|
start_date: date?.start || null,
|
||||||
|
end_date: date?.end || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.state === 0) {
|
if (response.state === 0) {
|
||||||
|
|
@ -621,11 +701,14 @@ export function ManageIdeasTechPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chart configuration for shadcn/ui
|
// Chart configuration for shadcn/ui
|
||||||
const chartConfig: ChartConfig = useMemo(() => ({
|
const chartConfig: ChartConfig = useMemo(
|
||||||
|
() => ({
|
||||||
count: {
|
count: {
|
||||||
label: "تعداد",
|
label: "تعداد",
|
||||||
},
|
},
|
||||||
}), []);
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// Color palette for idea status
|
// Color palette for idea status
|
||||||
// Specific colors for idea statuses
|
// Specific colors for idea statuses
|
||||||
|
|
@ -644,7 +727,16 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"];
|
const statusColorPalette = [
|
||||||
|
"#3AEA83",
|
||||||
|
"#69C8EA",
|
||||||
|
"#F76276",
|
||||||
|
"#FFD700",
|
||||||
|
"#A757FF",
|
||||||
|
"#E884CE",
|
||||||
|
"#C3BF8B",
|
||||||
|
"#FB7185",
|
||||||
|
];
|
||||||
|
|
||||||
// Build a mapping of status value -> color based on loaded ideas
|
// Build a mapping of status value -> color based on loaded ideas
|
||||||
const statusColorMap = useMemo(() => {
|
const statusColorMap = useMemo(() => {
|
||||||
|
|
@ -681,9 +773,7 @@ export function ManageIdeasTechPage() {
|
||||||
|
|
||||||
switch (column.key) {
|
switch (column.key) {
|
||||||
case "idea_title":
|
case "idea_title":
|
||||||
return (
|
return <span className="text-sm text-white">{String(value)}</span>;
|
||||||
<span className="text-sm text-white">{String(value)}</span>
|
|
||||||
);
|
|
||||||
case "idea_registration_date":
|
case "idea_registration_date":
|
||||||
return (
|
return (
|
||||||
<span className="text-white text-sm">
|
<span className="text-white text-sm">
|
||||||
|
|
@ -708,7 +798,7 @@ export function ManageIdeasTechPage() {
|
||||||
case "increased_revenue":
|
case "increased_revenue":
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-white w-full">
|
<span className="text-sm text-white w-full">
|
||||||
{formatCurrency(String(value || "0")).replace("ریال" , "")}
|
{formatCurrency(String(value || "0")).replace("ریال", "")}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case "details":
|
case "details":
|
||||||
|
|
@ -720,7 +810,8 @@ export function ManageIdeasTechPage() {
|
||||||
className="underline text-pr-green underline-offset-4 text-sm"
|
className="underline text-pr-green underline-offset-4 text-sm"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
</Button> );
|
</Button>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<span className="text-white text-sm">
|
<span className="text-white text-sm">
|
||||||
|
|
@ -730,12 +821,9 @@ export function ManageIdeasTechPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
|
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
|
||||||
<div className="space-y-6 h-full">
|
<div className="space-y-6 h-full">
|
||||||
|
|
||||||
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
|
<div className="grid grid-cols-1 grid-rows-2 lg:grid-cols-3 gap-4 h-full">
|
||||||
{/* People Ranking Table */}
|
{/* People Ranking Table */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
|
|
@ -762,7 +850,10 @@ export function ManageIdeasTechPage() {
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loadingPeople ? (
|
{loadingPeople ? (
|
||||||
Array.from({ length: 10 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
<TableRow key={`skeleton-${index}`} className="text-sm leading-tight h-12">
|
<TableRow
|
||||||
|
key={`skeleton-${index}`}
|
||||||
|
className="text-sm leading-tight h-12"
|
||||||
|
>
|
||||||
<TableCell className="text-center py-2 px-2">
|
<TableCell className="text-center py-2 px-2">
|
||||||
<div className="w-6 h-6 bg-muted rounded-full animate-pulse mx-auto" />
|
<div className="w-6 h-6 bg-muted rounded-full animate-pulse mx-auto" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -771,9 +862,14 @@ export function ManageIdeasTechPage() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center py-2 px-2">
|
<TableCell className="text-center py-2 px-2">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
{Array.from({ length: 5 }).map((_, starIndex) => (
|
{Array.from({ length: 5 }).map(
|
||||||
<div key={starIndex} className="w-3 h-3 bg-muted rounded animate-pulse" />
|
(_, starIndex) => (
|
||||||
))}
|
<div
|
||||||
|
key={starIndex}
|
||||||
|
className="w-3 h-3 bg-muted rounded animate-pulse"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -788,7 +884,10 @@ export function ManageIdeasTechPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
peopleRanking.map((person) => (
|
peopleRanking.map((person) => (
|
||||||
<TableRow key={person.full_name} className="text-sm leading-tight h-10 not-last:border-b-pr-gray border-border">
|
<TableRow
|
||||||
|
key={person.full_name}
|
||||||
|
className="text-sm leading-tight h-10 not-last:border-b-pr-gray border-border"
|
||||||
|
>
|
||||||
<TableCell className="text-center py-2 px-2">
|
<TableCell className="text-center py-2 px-2">
|
||||||
<div className="flex items-center justify-center text-white text-sm mx-auto">
|
<div className="flex items-center justify-center text-white text-sm mx-auto">
|
||||||
{toPersianDigits(person.ranking)}
|
{toPersianDigits(person.ranking)}
|
||||||
|
|
@ -801,7 +900,8 @@ export function ManageIdeasTechPage() {
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center py-4 px-2">
|
<TableCell className="text-center py-4 px-2">
|
||||||
<div className="flex mx-4 flex-row-reverse items-center justify-center gap-1">
|
<div className="flex mx-4 flex-row-reverse items-center justify-center gap-1">
|
||||||
{Array.from({ length: 5 }).map((_, starIndex) => (
|
{Array.from({ length: 5 }).map(
|
||||||
|
(_, starIndex) => (
|
||||||
<Star
|
<Star
|
||||||
key={starIndex}
|
key={starIndex}
|
||||||
className={`w-5 h-5 ${
|
className={`w-5 h-5 ${
|
||||||
|
|
@ -810,7 +910,8 @@ export function ManageIdeasTechPage() {
|
||||||
: "text-pr-gray"
|
: "text-pr-gray"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
@ -856,7 +957,10 @@ export function ManageIdeasTechPage() {
|
||||||
>
|
>
|
||||||
<span>{column.label}</span>
|
<span>{column.label}</span>
|
||||||
{column.key === "increased_revenue" && (
|
{column.key === "increased_revenue" && (
|
||||||
<span className="text-[#ACACAC] text-right font-light text-[8px]">میلیون <br/>ریال</span>
|
<span className="text-[#ACACAC] text-right font-light text-[8px]">
|
||||||
|
میلیون <br />
|
||||||
|
ریال
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{sortConfig.field === column.key ? (
|
{sortConfig.field === column.key ? (
|
||||||
sortConfig.direction === "asc" ? (
|
sortConfig.direction === "asc" ? (
|
||||||
|
|
@ -891,7 +995,9 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="w-3 h-3 bg-muted rounded-full animate-pulse" />
|
<div className="w-3 h-3 bg-muted rounded-full animate-pulse" />
|
||||||
<div
|
<div
|
||||||
className="h-3 bg-muted rounded animate-pulse"
|
className="h-3 bg-muted rounded animate-pulse"
|
||||||
style={{ width: `${Math.random() * 60 + 40}%` }}
|
style={{
|
||||||
|
width: `${Math.random() * 60 + 40}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -931,9 +1037,7 @@ export function ManageIdeasTechPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Infinite scroll trigger */}
|
{/* Infinite scroll trigger */}
|
||||||
<div ref={observerRef} className="h-auto">
|
<div ref={observerRef} className="h-auto"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|
@ -947,14 +1051,17 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="flex items-center justify-center py-2">
|
<div className="flex items-center justify-center py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RefreshCw className="w-4 h-4 animate-spin text-success" />
|
<RefreshCw className="w-4 h-4 animate-spin text-success" />
|
||||||
<span className="font-persian text-muted-foreground text-sm">
|
<span className="font-persian text-muted-foreground text-sm"></span>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Chart Section */}
|
{/* Chart Section */}
|
||||||
<BaseCard icon={TrendingUp} className="col-span-1 mt-12 row-start-2 col-start-3 row-span-1" title="نمودار ایدهها">
|
<BaseCard
|
||||||
|
icon={TrendingUp}
|
||||||
|
className="col-span-1 mt-12 row-start-2 col-start-3 row-span-1"
|
||||||
|
title="نمودار ایدهها"
|
||||||
|
>
|
||||||
<MemoizedVerticalBarChart
|
<MemoizedVerticalBarChart
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
loadingChart={loadingChart}
|
loadingChart={loadingChart}
|
||||||
|
|
@ -994,18 +1101,19 @@ export function ManageIdeasTechPage() {
|
||||||
browser: "ideas",
|
browser: "ideas",
|
||||||
visitors:
|
visitors:
|
||||||
parseFloat(
|
parseFloat(
|
||||||
statsData?.registered_innovation_technology_idea || "0"
|
statsData?.registered_innovation_technology_idea ||
|
||||||
|
"0"
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
statsData?.registered_innovation_technology_idea || "0",
|
statsData?.registered_innovation_technology_idea ||
|
||||||
|
"0"
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea ||
|
"1"
|
||||||
"1",
|
|
||||||
)) *
|
)) *
|
||||||
100,
|
100
|
||||||
)
|
)
|
||||||
: 0,
|
: 0,
|
||||||
fill: "var(--color-green)",
|
fill: "var(--color-green)",
|
||||||
|
|
@ -1015,20 +1123,19 @@ export function ManageIdeasTechPage() {
|
||||||
endAngle={
|
endAngle={
|
||||||
90 +
|
90 +
|
||||||
((parseFloat(
|
((parseFloat(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea || "0",
|
"0"
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
statsData
|
statsData?.ongoing_innovation_technology_ideas ||
|
||||||
?.ongoing_innovation_technology_ideas || "0",
|
"0"
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea ||
|
"1"
|
||||||
"1",
|
|
||||||
)) *
|
)) *
|
||||||
100,
|
100
|
||||||
)
|
)
|
||||||
: 0) /
|
: 0) /
|
||||||
100) *
|
100) *
|
||||||
|
|
@ -1072,24 +1179,21 @@ export function ManageIdeasTechPage() {
|
||||||
%
|
%
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
parseFloat(
|
parseFloat(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea ||
|
"0"
|
||||||
"0",
|
|
||||||
) > 0
|
) > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
(parseFloat(
|
(parseFloat(
|
||||||
statsData
|
statsData?.ongoing_innovation_technology_ideas ||
|
||||||
?.ongoing_innovation_technology_ideas ||
|
"0"
|
||||||
"0",
|
|
||||||
) /
|
) /
|
||||||
parseFloat(
|
parseFloat(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea ||
|
"1"
|
||||||
"1",
|
|
||||||
)) *
|
)) *
|
||||||
100,
|
100
|
||||||
)
|
)
|
||||||
: 0,
|
: 0
|
||||||
)}
|
)}
|
||||||
</tspan>
|
</tspan>
|
||||||
</text>
|
</text>
|
||||||
|
|
@ -1105,15 +1209,14 @@ export function ManageIdeasTechPage() {
|
||||||
<span className="flex font-bold items-center gap-1 text-base">
|
<span className="flex font-bold items-center gap-1 text-base">
|
||||||
<div className="font-light text-sm">ثبت شده :</div>
|
<div className="font-light text-sm">ثبت شده :</div>
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
statsData
|
statsData?.registered_innovation_technology_idea ||
|
||||||
?.registered_innovation_technology_idea || "0",
|
"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(
|
||||||
statsData
|
statsData?.ongoing_innovation_technology_ideas || "0"
|
||||||
?.ongoing_innovation_technology_ideas || "0",
|
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1139,7 +1242,12 @@ export function ManageIdeasTechPage() {
|
||||||
) : (
|
) : (
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="درآمد افزایش یافته"
|
title="درآمد افزایش یافته"
|
||||||
value={statsData?.increased_revenue_from_ideas?.replaceAll("," , "") || "0"}
|
value={
|
||||||
|
statsData?.increased_revenue_from_ideas?.replaceAll(
|
||||||
|
",",
|
||||||
|
""
|
||||||
|
) || "0"
|
||||||
|
}
|
||||||
percentValue={statsData?.increased_revenue_from_ideas_percent}
|
percentValue={statsData?.increased_revenue_from_ideas_percent}
|
||||||
percentLabel="درصد به کل درآمد"
|
percentLabel="درصد به کل درآمد"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1156,7 +1264,8 @@ export function ManageIdeasTechPage() {
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{selectedIdea && <div className="flex w-full justify-center gap-4">
|
{selectedIdea && (
|
||||||
|
<div className="flex w-full justify-center gap-4">
|
||||||
<div className="flex gap-4 flex-col text-right font-persian w-full border-l-2 border-l-pr-gray px-4 pb-4">
|
<div className="flex gap-4 flex-col text-right font-persian w-full border-l-2 border-l-pr-gray px-4 pb-4">
|
||||||
{/* مشخصات ایده پردازان Section */}
|
{/* مشخصات ایده پردازان Section */}
|
||||||
<div className="">
|
<div className="">
|
||||||
|
|
@ -1166,39 +1275,59 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="flex flex-col gap-4 mr-5">
|
<div className="flex flex-col gap-4 mr-5">
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]"/>
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">نام ایده پرداز:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
نام ایده پرداز:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.full_name || "-"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.full_name || "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">شماره پرسنلی:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
شماره پرسنلی:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{toPersianDigits(selectedIdea.personnel_number) ||
|
||||||
|
"۱۳۰۶۵۸۰۶"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">مدیریت:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
مدیریت:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.management || "مدیریت توسعه"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.management || "مدیریت توسعه"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">معاونت:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
معاونت:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.deputy || "توسعه"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.deputy || "توسعه"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2 col-span-2">
|
<div className="grid grid-cols-3 items-center gap-2 col-span-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">اعضای تیم:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
اعضای تیم:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
{selectedIdea.innovator_team_members || "رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
|
{selectedIdea.innovator_team_members ||
|
||||||
|
"رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1213,30 +1342,47 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">تاریخ ثبت ایده:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
تاریخ ثبت ایده:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{formatDate(selectedIdea.idea_registration_date) || "-"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{formatDate(selectedIdea.idea_registration_date) ||
|
||||||
|
"-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">نوع نوآوری:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
نوع نوآوری:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.innovation_type || "-"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.innovation_type || "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">اصالت ایده:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
اصالت ایده:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_originality || "-"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.idea_originality || "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light min-w-max">محور ایده:</span>
|
<span className="text-white text-sm text-light min-w-max">
|
||||||
|
محور ایده:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_axis || "-"}</span>
|
<span className="text-white font-normal text-sm mr-10">
|
||||||
|
{selectedIdea.idea_axis || "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1249,33 +1395,38 @@ export function ManageIdeasTechPage() {
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">درآمد حاصل:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
درآمد حاصل:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white text-sm font-normal mr-10">{formatNumber(selectedIdea.increased_revenue) || "-"}
|
<span className="text-white text-sm font-normal mr-10">
|
||||||
|
{formatNumber(selectedIdea.increased_revenue) || "-"}
|
||||||
<span className="text-[11px] mr-2 font-light">
|
<span className="text-[11px] mr-2 font-light">
|
||||||
میلیون ریال
|
میلیون ریال
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">مقاله چاپ شده:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
مقاله چاپ شده:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white font-normal cursor-pointer text-sm flex items-center gap-2 mr-10">
|
<span className="text-white font-normal cursor-pointer text-sm flex items-center gap-2 mr-10">
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-4 w-4" />
|
||||||
|
|
||||||
دانلود
|
دانلود
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 items-center gap-2">
|
<div className="grid grid-cols-3 items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
<Hexagon className="stroke-pr-green h-5 w-5 stroke-[1px]" />
|
||||||
<span className="text-white text-sm text-light">پتنت ثبت شده:</span>
|
<span className="text-white text-sm text-light">
|
||||||
|
پتنت ثبت شده:
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white cursor-pointer font-normal text-sm flex items-center gap-2 mr-10">
|
<span className="text-white cursor-pointer font-normal text-sm flex items-center gap-2 mr-10">
|
||||||
<Download className="h-4 w-4"/>
|
<Download className="h-4 w-4" />
|
||||||
دانلود
|
دانلود
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1283,7 +1434,6 @@ export function ManageIdeasTechPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col gap-8">
|
<div className="w-full flex flex-col gap-8">
|
||||||
|
|
||||||
{/* شرح ایده Section */}
|
{/* شرح ایده Section */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-bold text-white mb-4">
|
<h3 className="text-base font-bold text-white mb-4">
|
||||||
|
|
@ -1291,9 +1441,7 @@ export function ManageIdeasTechPage() {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="">
|
<div className="">
|
||||||
<p className="text-white text-sm">
|
<p className="text-white text-sm">
|
||||||
{selectedIdea.idea_description ||
|
{selectedIdea.idea_description || "-"}
|
||||||
"-"
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1305,9 +1453,7 @@ export function ManageIdeasTechPage() {
|
||||||
</h3>
|
</h3>
|
||||||
<div className="">
|
<div className="">
|
||||||
<p className="text-white leading-relaxed text-sm">
|
<p className="text-white leading-relaxed text-sm">
|
||||||
{selectedIdea.idea_current_status_description ||
|
{selectedIdea.idea_current_status_description || "-"}
|
||||||
"-"
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1319,9 +1465,7 @@ export function ManageIdeasTechPage() {
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white leading-relaxed text-sm">
|
<p className="text-white leading-relaxed text-sm">
|
||||||
{selectedIdea.idea_execution_benefits ||
|
{selectedIdea.idea_execution_benefits || "-"}
|
||||||
"-"
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1333,15 +1477,13 @@ export function ManageIdeasTechPage() {
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white leading-relaxed text-sm">
|
<p className="text-white leading-relaxed text-sm">
|
||||||
{selectedIdea.process_improvements ||
|
{selectedIdea.process_improvements || "-"}
|
||||||
"-"
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,38 @@
|
||||||
import {
|
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
|
||||||
ArrowDownCircle,
|
|
||||||
ArrowUpCircle,
|
|
||||||
Building2,
|
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
CirclePause,
|
|
||||||
DollarSign,
|
|
||||||
Funnel,
|
|
||||||
Loader2,
|
|
||||||
PickaxeIcon,
|
|
||||||
RefreshCw,
|
|
||||||
TrendingUp,
|
|
||||||
UserIcon,
|
|
||||||
UsersIcon,
|
|
||||||
Wrench,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { Bar, BarChart, LabelList } from "recharts";
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
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 } from "~/components/ui/card";
|
||||||
import { Checkbox } from "~/components/ui/checkbox";
|
import { Checkbox } from "~/components/ui/checkbox";
|
||||||
import { Bar, BarChart, LabelList } from "recharts"
|
import { MetricCard } from "~/components/ui/metric-card";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverTrigger,
|
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
} from "~/components/ui/popover"
|
PopoverTrigger,
|
||||||
|
} from "~/components/ui/popover";
|
||||||
|
|
||||||
import { FunnelChart } from "~/components/ui/funnel-chart";
|
import jalaali from "jalaali-js";
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
|
import {
|
||||||
|
CartesianGrid,
|
||||||
|
Legend,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import { Label } from "~/components/ui/label";
|
import { FunnelChart } from "~/components/ui/funnel-chart";
|
||||||
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -49,12 +41,11 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
|
import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip";
|
||||||
import apiService from "~/lib/api";
|
import apiService from "~/lib/api";
|
||||||
import { formatNumber, handleDataValue } from "~/lib/utils";
|
import { EventBus, formatNumber, handleDataValue } from "~/lib/utils";
|
||||||
|
import type { CalendarDate } from "~/types/util.type";
|
||||||
import { DashboardLayout } from "../layout";
|
import { DashboardLayout } from "../layout";
|
||||||
import { Skeleton } from "~/components/ui/skeleton";
|
|
||||||
import { Tooltip as TooltipSh, TooltipTrigger, TooltipContent } from "~/components/ui/tooltip";
|
|
||||||
|
|
||||||
|
|
||||||
interface ProjectData {
|
interface ProjectData {
|
||||||
project_no: string;
|
project_no: string;
|
||||||
|
|
@ -139,15 +130,16 @@ const columns = [
|
||||||
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default function Timeline(valueTimeLine: string) {
|
||||||
export default function Timeline( valueTimeLine : string) {
|
|
||||||
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
|
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
|
||||||
const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine)
|
const currentStage = stages
|
||||||
|
?.toReversed()
|
||||||
|
?.findIndex((x: string) => x == valueTimeLine);
|
||||||
const per = () => {
|
const per = () => {
|
||||||
const main = stages?.findIndex((x) => x == "ثبت ایده")
|
const main = stages?.findIndex((x) => x == "ثبت ایده");
|
||||||
console.log( 'yay ' , 25 * main + 12.5);
|
console.log("yay ", 25 * main + 12.5);
|
||||||
return 25 * main + 12.5
|
return 25 * main + 12.5;
|
||||||
}
|
};
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-4">
|
<div className="w-full p-4">
|
||||||
{/* Year labels */}
|
{/* Year labels */}
|
||||||
|
|
@ -160,12 +152,17 @@ export default function Timeline( valueTimeLine : string) {
|
||||||
{/* Timeline bar */}
|
{/* Timeline bar */}
|
||||||
<div className="relative rounded-lg flex mb-4 items-center">
|
<div className="relative rounded-lg flex mb-4 items-center">
|
||||||
{stages.map((stage, index) => (
|
{stages.map((stage, index) => (
|
||||||
<div key={stage} className="flex-1 flex flex-col items-center relative">
|
<div
|
||||||
|
key={stage}
|
||||||
|
className="flex-1 flex flex-col items-center relative"
|
||||||
|
>
|
||||||
<TooltipSh>
|
<TooltipSh>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={`w-full py-2 text-center transition-colors duration-300 ${
|
className={`w-full py-2 text-center transition-colors duration-300 ${
|
||||||
index <= currentStage ? "bg-[#3D7968] text-white" : "bg-[#3AEA83] text-slate-600"
|
index <= currentStage
|
||||||
|
? "bg-[#3D7968] text-white"
|
||||||
|
: "bg-[#3AEA83] text-slate-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mt-1 text-sm">{stage}</span>
|
<span className="mt-1 text-sm">{stage}</span>
|
||||||
|
|
@ -176,25 +173,33 @@ export default function Timeline( valueTimeLine : string) {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Vertical line showing current position */}
|
{/* Vertical line showing current position */}
|
||||||
{ valueTimeLine?.length > 0 && ( <> <div
|
{valueTimeLine?.length > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<div
|
||||||
className={`absolute top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full`}
|
className={`absolute top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full`}
|
||||||
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
|
style={{
|
||||||
|
left: `${(currentStage + 0.5) * (100 / stages.length)}%`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute top-15 h-[max-content] translate-x-[-50%] text-xs text-gray-300 border-gray-400 rounded-md border px-2 bottom-0"
|
className="absolute top-15 h-[max-content] translate-x-[-50%] text-xs text-gray-300 border-gray-400 rounded-md border px-2 bottom-0"
|
||||||
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
|
style={{
|
||||||
>وضعیت فعلی</div>
|
left: `${(currentStage + 0.5) * (100 / stages.length)}%`,
|
||||||
</> ) }
|
}}
|
||||||
|
>
|
||||||
|
وضعیت فعلی
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function ProductInnovationPage() {
|
export function ProductInnovationPage() {
|
||||||
const [showPopup, setShowPopup] = useState(false);
|
// const [showPopup, setShowPopup] = useState(false);
|
||||||
|
const { jy } = jalaali.toJalaali(new Date());
|
||||||
const [projects, setProjects] = useState<ProductInnovationData[]>([]);
|
const [projects, setProjects] = useState<ProductInnovationData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
|
@ -241,7 +246,7 @@ export function ProductInnovationPage() {
|
||||||
description: "میلیون ریال",
|
description: "میلیون ریال",
|
||||||
descriptionPercent: "درصد به کل درآمد",
|
descriptionPercent: "درصد به کل درآمد",
|
||||||
color: "text-[#3AEA83]",
|
color: "text-[#3AEA83]",
|
||||||
percent :0
|
percent: 0,
|
||||||
},
|
},
|
||||||
newProductExports: {
|
newProductExports: {
|
||||||
id: "newProductExports",
|
id: "newProductExports",
|
||||||
|
|
@ -259,45 +264,53 @@ export function ProductInnovationPage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [date, setDate] = useState<CalendarDate>({
|
||||||
|
start: `${jy}/01/01`,
|
||||||
|
end: `${jy}/12/30`,
|
||||||
|
});
|
||||||
const observerRef = useRef<HTMLDivElement>(null);
|
const observerRef = useRef<HTMLDivElement>(null);
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleProjectDetails = async (project: ProductInnovationData) => {
|
const handleProjectDetails = async (project: ProductInnovationData) => {
|
||||||
setSelectedProjectDetails(project);
|
setSelectedProjectDetails(project);
|
||||||
console.log(project)
|
|
||||||
setDetailsDialogOpen(true);
|
setDetailsDialogOpen(true);
|
||||||
await fetchPopupData(project);
|
await fetchPopupData(project, date?.start, date?.end);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPopupData = async (project: ProductInnovationData) => {
|
const fetchPopupData = async (
|
||||||
|
project: ProductInnovationData,
|
||||||
|
startDate?: string,
|
||||||
|
endDate?: string
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
setPopupLoading(true);
|
setPopupLoading(true);
|
||||||
|
|
||||||
// Fetch popup stats
|
// Fetch popup stats
|
||||||
const statsResponse = await apiService.call({
|
const statsResponse = await apiService.call({
|
||||||
innovation_product_popup_function1: {
|
innovation_product_popup_function1: {
|
||||||
project_id: project.project_id
|
project_id: project.project_id,
|
||||||
}
|
start_date: startDate || null,
|
||||||
|
end_date: endDate || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statsResponse.state === 0) {
|
if (statsResponse.state === 0) {
|
||||||
const statsData = JSON.parse(statsResponse.data);
|
const statsData = JSON.parse(statsResponse.data);
|
||||||
if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) {
|
if (
|
||||||
setPopupStats(JSON.parse(statsData.innovation_product_popup_function1)[0]);
|
statsData.innovation_product_popup_function1 &&
|
||||||
|
statsData.innovation_product_popup_function1[0]
|
||||||
|
) {
|
||||||
|
setPopupStats(
|
||||||
|
JSON.parse(statsData.innovation_product_popup_function1)[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch export chart data
|
// Fetch export chart data
|
||||||
const chartResponse = await apiService.select({
|
const chartResponse = await apiService.select({
|
||||||
ProcessName: "export_product_innovation",
|
ProcessName: "export_product_innovation",
|
||||||
OutputFields: [
|
OutputFields: ["product_title", "full_season", "sum(export_revenue)"],
|
||||||
"product_title",
|
GroupBy: ["product_title", "full_season"],
|
||||||
"full_season",
|
|
||||||
"sum(export_revenue)"
|
|
||||||
],
|
|
||||||
GroupBy: ["product_title", "full_season"]
|
|
||||||
});
|
});
|
||||||
if (chartResponse.state === 0) {
|
if (chartResponse.state === 0) {
|
||||||
const chartData = JSON.parse(chartResponse.data);
|
const chartData = JSON.parse(chartResponse.data);
|
||||||
|
|
@ -305,14 +318,13 @@ export function ProductInnovationPage() {
|
||||||
// Set all data for line chart
|
// Set all data for line chart
|
||||||
|
|
||||||
// Filter data for the selected project (bar chart)
|
// Filter data for the selected project (bar chart)
|
||||||
const filteredData = chartData.filter(item =>
|
const filteredData = chartData.filter(
|
||||||
item.product_title === project?.title
|
(item) => item.product_title === project?.title
|
||||||
);
|
);
|
||||||
setAllExportData(chartData);
|
setAllExportData(chartData);
|
||||||
setExportChartData(filteredData);
|
setExportChartData(filteredData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching popup data:", error);
|
console.error("Error fetching popup data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -358,10 +370,14 @@ export function ProductInnovationPage() {
|
||||||
"knowledge_based_certificate_obtained",
|
"knowledge_based_certificate_obtained",
|
||||||
"knowledge_based_certificate_number",
|
"knowledge_based_certificate_number",
|
||||||
"certificate_obtain_date",
|
"certificate_obtain_date",
|
||||||
"issuing_authority"
|
"issuing_authority",
|
||||||
],
|
],
|
||||||
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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -427,8 +443,12 @@ export function ProductInnovationPage() {
|
||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
setStatsLoading(true);
|
setStatsLoading(true);
|
||||||
|
|
||||||
const raw = await apiService.call<any>({
|
const raw = await apiService.call<any>({
|
||||||
innovation_product_function: {},
|
innovation_product_function: {
|
||||||
|
start_date: date?.start || null,
|
||||||
|
end_date: date?.end || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let payload: any = JSON.parse(raw?.data);
|
let payload: any = JSON.parse(raw?.data);
|
||||||
|
|
@ -444,21 +464,25 @@ export function ProductInnovationPage() {
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const data: Array<any> = JSON.parse(
|
const data: Array<any> = JSON.parse(payload?.innovation_product_function);
|
||||||
payload?.innovation_product_function
|
|
||||||
);
|
|
||||||
const stats = data[0];
|
const stats = data[0];
|
||||||
const normalized: ProductInnovationStats = {
|
const normalized: ProductInnovationStats = {
|
||||||
new_products_revenue_share: parseNum(stats?.new_products_revenue_share),
|
new_products_revenue_share: parseNum(stats?.new_products_revenue_share),
|
||||||
new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent),
|
new_products_revenue_share_percent: parseNum(
|
||||||
|
stats?.new_products_revenue_share_percent
|
||||||
|
),
|
||||||
import_impact: parseNum(stats?.import_impact),
|
import_impact: parseNum(stats?.import_impact),
|
||||||
new_products_export: parseNum(stats?.new_products_export),
|
new_products_export: parseNum(stats?.new_products_export),
|
||||||
all_funnel: parseNum(stats?.all_funnel),
|
all_funnel: parseNum(stats?.all_funnel),
|
||||||
successful_sample_funnel: parseNum(stats?.successful_sample_funnel),
|
successful_sample_funnel: parseNum(stats?.successful_sample_funnel),
|
||||||
successful_products_funnel: parseNum(stats?.successful_products_funnel),
|
successful_products_funnel: parseNum(stats?.successful_products_funnel),
|
||||||
successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel),
|
successful_improvement_or_change_funnel: parseNum(
|
||||||
|
stats?.successful_improvement_or_change_funnel
|
||||||
|
),
|
||||||
new_product_funnel: parseNum(stats?.new_product_funnel),
|
new_product_funnel: parseNum(stats?.new_product_funnel),
|
||||||
count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects),
|
count_innovation_construction_inside_projects: parseNum(
|
||||||
|
stats?.count_innovation_construction_inside_projects
|
||||||
|
),
|
||||||
average_project_score: parseNum(stats?.average_project_score),
|
average_project_score: parseNum(stats?.average_project_score),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -487,13 +511,21 @@ export function ProductInnovationPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
|
if (date) {
|
||||||
|
setDate(date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProjects(true);
|
fetchProjects(true);
|
||||||
}, [sortConfig]);
|
}, [sortConfig, date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats();
|
fetchStats();
|
||||||
}, []);
|
}, [date]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPage > 1) {
|
if (currentPage > 1) {
|
||||||
|
|
@ -538,39 +570,42 @@ export function ProductInnovationPage() {
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// const formatCurrency = (amount: string | number) => {
|
||||||
const formatCurrency = (amount: string | number) => {
|
// if (!amount) return "0 ریال";
|
||||||
if (!amount) return "0 ریال";
|
// const numericAmount =
|
||||||
const numericAmount =
|
// typeof amount === "string"
|
||||||
typeof amount === "string"
|
// ? parseFloat(amount.replace(/,/g, ""))
|
||||||
? parseFloat(amount.replace(/,/g, ""))
|
// : amount;
|
||||||
: amount;
|
// if (isNaN(numericAmount)) return "0 ریال";
|
||||||
if (isNaN(numericAmount)) return "0 ریال";
|
// return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
||||||
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
|
// };
|
||||||
};
|
|
||||||
|
|
||||||
// Transform data for line chart
|
// Transform data for line chart
|
||||||
const transformDataForLineChart = (data: any[]) => {
|
const transformDataForLineChart = (data: any[]) => {
|
||||||
const seasons = [...new Set(data.map(item => item.full_season))];
|
const seasons = [...new Set(data.map((item) => item.full_season))];
|
||||||
const products = [...new Set(data.map(item => item.product_title))];
|
const products = [...new Set(data.map((item) => item.product_title))];
|
||||||
return seasons.map(season => {
|
return seasons.map((season) => {
|
||||||
const seasonData: any = { season };
|
const seasonData: any = { season };
|
||||||
products.forEach(product => {
|
products.forEach((product) => {
|
||||||
const productData = data.find(item =>
|
const productData = data.find(
|
||||||
|
(item) =>
|
||||||
item.product_title === product && item.full_season === season
|
item.product_title === product && item.full_season === season
|
||||||
);
|
);
|
||||||
seasonData[product] = productData?.export_revenue_sum > 0 && productData ? Math.round(productData?.export_revenue_sum) : 0;
|
seasonData[product] =
|
||||||
|
productData?.export_revenue_sum > 0 && productData
|
||||||
|
? Math.round(productData?.export_revenue_sum)
|
||||||
|
: 0;
|
||||||
});
|
});
|
||||||
return seasonData;
|
return seasonData;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRatingColor = (rating: string | number) => {
|
// const getRatingColor = (rating: string | number) => {
|
||||||
const numRating = typeof rating === "string" ? parseInt(rating) : rating;
|
// const numRating = typeof rating === "string" ? parseInt(rating) : rating;
|
||||||
if (numRating >= 150) return "text-emerald-400";
|
// if (numRating >= 150) return "text-emerald-400";
|
||||||
if (numRating >= 100) return "text-blue-400";
|
// if (numRating >= 100) return "text-blue-400";
|
||||||
return "text-red-400";
|
// return "text-red-400";
|
||||||
};
|
// };
|
||||||
|
|
||||||
const statusColor = (status: projectStatus): any => {
|
const statusColor = (status: projectStatus): any => {
|
||||||
let el = null;
|
let el = null;
|
||||||
|
|
@ -615,7 +650,8 @@ export function ProductInnovationPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleProjectDetails(item)}}
|
handleProjectDetails(item);
|
||||||
|
}}
|
||||||
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm p-2 h-auto"
|
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm p-2 h-auto"
|
||||||
>
|
>
|
||||||
جزئیات بیشتر
|
جزئیات بیشتر
|
||||||
|
|
@ -628,7 +664,9 @@ export function ProductInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
case "title":
|
case "title":
|
||||||
return <span className="font-light text-sm text-white">{String(value)}</span>;
|
return (
|
||||||
|
<span className="font-light text-sm text-white">{String(value)}</span>
|
||||||
|
);
|
||||||
case "project_status":
|
case "project_status":
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center text-sm font-light gap-1">
|
<div className="flex items-center text-sm font-light gap-1">
|
||||||
|
|
@ -652,7 +690,11 @@ export function ProductInnovationPage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
|
return (
|
||||||
|
<span className="text-white text-sm font-light">
|
||||||
|
{String(value) || "-"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -667,7 +709,8 @@ export function ProductInnovationPage() {
|
||||||
})
|
})
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
label: item.full_season,
|
label: item.full_season,
|
||||||
value: item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum) ,
|
value:
|
||||||
|
item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -719,18 +762,27 @@ export function ProductInnovationPage() {
|
||||||
value={stateCard.revenueNewProducts.value}
|
value={stateCard.revenueNewProducts.value}
|
||||||
percentValue={stateCard.revenueNewProducts.percent}
|
percentValue={stateCard.revenueNewProducts.percent}
|
||||||
valueLabel={stateCard.revenueNewProducts.description}
|
valueLabel={stateCard.revenueNewProducts.description}
|
||||||
percentLabel={stateCard.revenueNewProducts.descriptionPercent}
|
percentLabel={
|
||||||
|
stateCard.revenueNewProducts.descriptionPercent
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second card */}
|
{/* Second card */}
|
||||||
<div>
|
<div>
|
||||||
<BaseCard title={stateCard.newProductExports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
|
<BaseCard
|
||||||
|
title={stateCard.newProductExports.title}
|
||||||
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-center flex-col">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl font-bold mb-1 text-pr-green">{stateCard.newProductExports.value}</p>
|
<p className="text-3xl font-bold mb-1 text-pr-green">
|
||||||
<div className="text-xs text-gray-400 font-persian">{stateCard.newProductExports.description}</div>
|
{stateCard.newProductExports.value}
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
|
{stateCard.newProductExports.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -739,12 +791,19 @@ export function ProductInnovationPage() {
|
||||||
|
|
||||||
{/* Third card - basic BaseCard */}
|
{/* Third card - basic BaseCard */}
|
||||||
<div>
|
<div>
|
||||||
<BaseCard title={stateCard.impactOnImports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
|
<BaseCard
|
||||||
|
title={stateCard.impactOnImports.title}
|
||||||
|
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
|
||||||
|
>
|
||||||
<div className="flex items-center justify-center flex-col">
|
<div className="flex items-center justify-center flex-col">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-3xl font-bold mb-1 text-pr-red">{stateCard.impactOnImports.value}</p>
|
<p className="text-3xl font-bold mb-1 text-pr-red">
|
||||||
<div className="text-xs text-gray-400 font-persian">{stateCard.impactOnImports.description}</div>
|
{stateCard.impactOnImports.value}
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-400 font-persian">
|
||||||
|
{stateCard.impactOnImports.description}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -902,7 +961,10 @@ export function ProductInnovationPage() {
|
||||||
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
|
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
|
||||||
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
|
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
|
||||||
<div className="text-sm font-semibold text-white">
|
<div className="text-sm font-semibold text-white">
|
||||||
کل پروژه ها :{formatNumber(stats?.count_innovation_construction_inside_projects)}
|
کل پروژه ها :
|
||||||
|
{formatNumber(
|
||||||
|
stats?.count_innovation_construction_inside_projects
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -917,7 +979,9 @@ export function ProductInnovationPage() {
|
||||||
<div className="text-bold text-sm text-white">میانگین :</div>
|
<div className="text-bold text-sm text-white">میانگین :</div>
|
||||||
<div className="font-bold text-sm text-white">
|
<div className="font-bold text-sm text-white">
|
||||||
{formatNumber(
|
{formatNumber(
|
||||||
((stats.average_project_score ?? 0) as number).toFixed?.(1) ?? 0
|
((stats.average_project_score ?? 0) as number).toFixed?.(
|
||||||
|
1
|
||||||
|
) ?? 0
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -941,29 +1005,47 @@ export function ProductInnovationPage() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="font-bold text-base">{selectedProjectDetails?.title}</h3>
|
<h3 className="font-bold text-base">
|
||||||
<p className="py-2">{selectedProjectDetails?.project_description}</p>
|
{selectedProjectDetails?.title}
|
||||||
|
</h3>
|
||||||
|
<p className="py-2">
|
||||||
|
{selectedProjectDetails?.project_description}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Timeline valueTimeLine={selectedProjectDetails?.current_status} />
|
<Timeline
|
||||||
|
valueTimeLine={selectedProjectDetails?.current_status}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Technical Knowledge */}
|
{/* Technical Knowledge */}
|
||||||
<div className=" rounded-lg py-2 mb-0">
|
<div className=" rounded-lg py-2 mb-0">
|
||||||
<h3 className="text-sm text-white font-semibold mb-2">دانش فنی محصول جدید</h3>
|
<h3 className="text-sm text-white font-semibold mb-2">
|
||||||
|
دانش فنی محصول جدید
|
||||||
|
</h3>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-white font-light">توسعه درونزا</span>
|
<span className="text-sm text-white font-light">
|
||||||
|
توسعه درونزا
|
||||||
|
</span>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedProjectDetails?.developed_technology_type === "توسعه درونزا"}
|
checked={
|
||||||
|
selectedProjectDetails?.developed_technology_type ===
|
||||||
|
"توسعه درونزا"
|
||||||
|
}
|
||||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-white font-light">همکاری فناورانه</span>
|
<span className="text-sm text-white font-light">
|
||||||
|
همکاری فناورانه
|
||||||
|
</span>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedProjectDetails?.developed_technology_type === "همکاری فناوری"}
|
checked={
|
||||||
|
selectedProjectDetails?.developed_technology_type ===
|
||||||
|
"همکاری فناوری"
|
||||||
|
}
|
||||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -971,11 +1053,14 @@ export function ProductInnovationPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-white font-light">سایر</span>
|
<span className="text-sm text-white font-light">سایر</span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedProjectDetails?.developed_technology_type === "سایر"}
|
checked={
|
||||||
|
selectedProjectDetails?.developed_technology_type ===
|
||||||
|
"سایر"
|
||||||
|
}
|
||||||
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Standards */}
|
{/* Standards */}
|
||||||
|
|
@ -984,15 +1069,20 @@ export function ProductInnovationPage() {
|
||||||
استانداردهای ملی و بینالمللی اخذ شده
|
استانداردهای ملی و بینالمللی اخذ شده
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{selectedProjectDetails?.obtained_standard_title && selectedProjectDetails?.obtained_standard_title.length > 0 ? (
|
{selectedProjectDetails?.obtained_standard_title &&
|
||||||
|
selectedProjectDetails?.obtained_standard_title.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(Array.isArray(selectedProjectDetails?.obtained_standard_title)
|
{(Array.isArray(
|
||||||
|
selectedProjectDetails?.obtained_standard_title
|
||||||
|
)
|
||||||
? selectedProjectDetails?.obtained_standard_title
|
? selectedProjectDetails?.obtained_standard_title
|
||||||
: [selectedProjectDetails?.obtained_standard_title]
|
: [selectedProjectDetails?.obtained_standard_title]
|
||||||
).map((standard, index) => (
|
).map((standard, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
||||||
<span className="text-sm text-white font-light">{standard}</span>
|
<span className="text-sm text-white font-light">
|
||||||
|
{standard}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1001,11 +1091,12 @@ export function ProductInnovationPage() {
|
||||||
هیچ استانداردی ثبت نشده است.
|
هیچ استانداردی ثبت نشده است.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Knowledge-based Certificate Button */}
|
{/* Knowledge-based Certificate Button */}
|
||||||
<div className="justify-self-centerr grid py-1 mx-auto">
|
<div className="justify-self-centerr grid py-1 mx-auto">
|
||||||
{selectedProjectDetails?.knowledge_based_certificate_obtained === "خیر" ? (
|
{selectedProjectDetails?.knowledge_based_certificate_obtained ===
|
||||||
|
"خیر" ? (
|
||||||
<div className=" border border-pr-red mx-auto rounded-lg p-2 text-center">
|
<div className=" border border-pr-red mx-auto rounded-lg p-2 text-center">
|
||||||
<button className="text-pr-red font-bold text-sm">
|
<button className="text-pr-red font-bold text-sm">
|
||||||
گواهی دانشبنیان ندارد
|
گواهی دانشبنیان ندارد
|
||||||
|
|
@ -1036,10 +1127,14 @@ export function ProductInnovationPage() {
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white">
|
<p className="text-sm text-white">
|
||||||
<span className="font-bold">تاریخ اخذ: </span>
|
<span className="font-bold">تاریخ اخذ: </span>
|
||||||
{handleDataValue(selectedProjectDetails?.certificate_obtain_date) || "—"}
|
{handleDataValue(
|
||||||
|
selectedProjectDetails?.certificate_obtain_date
|
||||||
|
) || "—"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-white">
|
<p className="text-sm text-white">
|
||||||
<span className="font-bold">مرجع صادرکننده: </span>
|
<span className="font-bold">
|
||||||
|
مرجع صادرکننده:{" "}
|
||||||
|
</span>
|
||||||
{selectedProjectDetails?.issuing_authority || "—"}
|
{selectedProjectDetails?.issuing_authority || "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1081,16 +1176,32 @@ export function ProductInnovationPage() {
|
||||||
<div className="rounded-lg pt-4 grid grid-cols-2 gap-4 w-full">
|
<div className="rounded-lg pt-4 grid grid-cols-2 gap-4 w-full">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="میزان صادارت محصول جدید"
|
title="میزان صادارت محصول جدید"
|
||||||
value={Math.round(popupStats?.new_products_export > 0 ? popupStats?.new_products_export : 0)}
|
value={Math.round(
|
||||||
percentValue={Math.round(popupStats?.new_products_export_percent > 0 ? popupStats?.new_products_export_percent : 0)}
|
popupStats?.new_products_export > 0
|
||||||
|
? popupStats?.new_products_export
|
||||||
|
: 0
|
||||||
|
)}
|
||||||
|
percentValue={Math.round(
|
||||||
|
popupStats?.new_products_export_percent > 0
|
||||||
|
? popupStats?.new_products_export_percent
|
||||||
|
: 0
|
||||||
|
)}
|
||||||
valueLabel="میلیون ریال"
|
valueLabel="میلیون ریال"
|
||||||
percentLabel="درصد به کل صادرات"
|
percentLabel="درصد به کل صادرات"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="تاثیر در واردات"
|
title="تاثیر در واردات"
|
||||||
value={Math.round(popupStats?.import_impact > 0 ? popupStats?.import_impact : 0)}
|
value={Math.round(
|
||||||
percentValue={Math.round(popupStats?.import_impact_percent > 0 ? popupStats?.import_impact_percent : 0)}
|
popupStats?.import_impact > 0
|
||||||
|
? popupStats?.import_impact
|
||||||
|
: 0
|
||||||
|
)}
|
||||||
|
percentValue={Math.round(
|
||||||
|
popupStats?.import_impact_percent > 0
|
||||||
|
? popupStats?.import_impact_percent
|
||||||
|
: 0
|
||||||
|
)}
|
||||||
valueLabel="میلیون ریال"
|
valueLabel="میلیون ریال"
|
||||||
percentLabel="درصد صرفه جویی"
|
percentLabel="درصد صرفه جویی"
|
||||||
/>
|
/>
|
||||||
|
|
@ -1098,7 +1209,9 @@ export function ProductInnovationPage() {
|
||||||
|
|
||||||
{/* Export Revenue Bar Chart */}
|
{/* Export Revenue Bar Chart */}
|
||||||
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
||||||
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
|
<h3 className="text-sm font-semibold text-white">
|
||||||
|
ظرفیت صادر شده
|
||||||
|
</h3>
|
||||||
<div className="h-60">
|
<div className="h-60">
|
||||||
{exportChartData.length > 0 ? (
|
{exportChartData.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
|
@ -1116,52 +1229,149 @@ export function ProductInnovationPage() {
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
stroke="#C3C3C3"
|
stroke="#C3C3C3"
|
||||||
tickMargin={8}
|
tickMargin={8}
|
||||||
tickFormatter={(value: string) => `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll('٬','')}`}
|
tickFormatter={(value: string) =>
|
||||||
|
`${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll("٬", "")}`
|
||||||
|
}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
/>
|
/>
|
||||||
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value: number) => `${formatNumber(value)} میلیون`} />
|
<YAxis
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
stroke="#9CA3AF"
|
||||||
|
fontSize={11}
|
||||||
|
tick={{ dx: -50 }}
|
||||||
|
tickFormatter={(value: number) =>
|
||||||
|
`${formatNumber(value)} میلیون`
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Bar dataKey="value" fill="#10B981" radius={10}>
|
<Bar dataKey="value" fill="#10B981" radius={10}>
|
||||||
<LabelList formatter={(value: number) => `${formatNumber(value)}`} position="top" offset={15} fill="F9FAFB" className="fill-foreground" fontSize={16} />
|
<LabelList
|
||||||
|
formatter={(value: number) =>
|
||||||
|
`${formatNumber(value)}`
|
||||||
|
}
|
||||||
|
position="top"
|
||||||
|
offset={15}
|
||||||
|
fill="F9FAFB"
|
||||||
|
className="fill-foreground"
|
||||||
|
fontSize={16}
|
||||||
|
/>
|
||||||
</Bar>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
دادهای برای نمایش وجود ندارد
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Revenue Line Chart */}
|
{/* Export Revenue Line Chart */}
|
||||||
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
|
||||||
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
|
<h3 className="text-sm font-semibold text-white">
|
||||||
|
ظرفیت صادر شده
|
||||||
|
</h3>
|
||||||
<div className="h-60">
|
<div className="h-60">
|
||||||
{allExportData.length > 0 ? (
|
{allExportData.length > 0 ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart className="aspect-auto w-full" data={transformDataForLineChart(allExportData)} margin={{ top: 20, right: 30, left: 10, bottom: 50 }}>
|
<LineChart
|
||||||
|
className="aspect-auto w-full"
|
||||||
|
data={transformDataForLineChart(allExportData)}
|
||||||
|
margin={{ top: 20, right: 30, left: 10, bottom: 50 }}
|
||||||
|
>
|
||||||
<CartesianGrid vertical={false} stroke="#374151" />
|
<CartesianGrid vertical={false} stroke="#374151" />
|
||||||
<XAxis dataKey="season" stroke="#9CA3AF" fontSize={11} tick={({ x, y, payload }) => (
|
<XAxis
|
||||||
|
dataKey="season"
|
||||||
|
stroke="#9CA3AF"
|
||||||
|
fontSize={11}
|
||||||
|
tick={({ x, y, payload }) => (
|
||||||
<g transform={`translate(${x},${y + 10})`}>
|
<g transform={`translate(${x},${y + 10})`}>
|
||||||
<text x={-40} y={15} dy={0} textAnchor="end" fill="#9CA3AF" fontSize={11} transform="rotate(-45)">{(payload as any).value}</text>
|
<text
|
||||||
|
x={-40}
|
||||||
|
y={15}
|
||||||
|
dy={0}
|
||||||
|
textAnchor="end"
|
||||||
|
fill="#9CA3AF"
|
||||||
|
fontSize={11}
|
||||||
|
transform="rotate(-45)"
|
||||||
|
>
|
||||||
|
{(payload as any).value}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
)} />
|
)}
|
||||||
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value) => `${formatNumber(value)} میلیون`} />
|
/>
|
||||||
<Tooltip formatter={(value: number) => `${formatNumber(value)} میلیون`} contentStyle={{ backgroundColor: "#1F2937", border: "1px solid #374151", borderRadius: "6px", padding: "6px 10px", fontSize: "11px", color: "#F9FAFB" }} />
|
<YAxis
|
||||||
<Legend layout="vertical" verticalAlign="middle" align="right" iconType={"plainline"} className="!flex" wrapperStyle={{ fontSize: 11, paddingLeft: 12, gap: 10 }} />
|
tickLine={false}
|
||||||
{[...new Set(allExportData.map((item) => item.product_title))].slice(0, 5).map((product, index) => {
|
axisLine={false}
|
||||||
const colors = ["#10B981", "#EF4444", "#3B82F6", "#F59E0B", "#8B5CF6"];
|
stroke="#9CA3AF"
|
||||||
return <Line key={product} type="linear" dot={false} activeDot={{ r: 5 }} dataKey={product} stroke={colors[index % colors.length]} strokeWidth={2} />;
|
fontSize={11}
|
||||||
|
tick={{ dx: -50 }}
|
||||||
|
tickFormatter={(value) =>
|
||||||
|
`${formatNumber(value)} میلیون`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value: number) =>
|
||||||
|
`${formatNumber(value)} میلیون`
|
||||||
|
}
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "#1F2937",
|
||||||
|
border: "1px solid #374151",
|
||||||
|
borderRadius: "6px",
|
||||||
|
padding: "6px 10px",
|
||||||
|
fontSize: "11px",
|
||||||
|
color: "#F9FAFB",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
layout="vertical"
|
||||||
|
verticalAlign="middle"
|
||||||
|
align="right"
|
||||||
|
iconType={"plainline"}
|
||||||
|
className="!flex"
|
||||||
|
wrapperStyle={{
|
||||||
|
fontSize: 11,
|
||||||
|
paddingLeft: 12,
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{[
|
||||||
|
...new Set(
|
||||||
|
allExportData.map((item) => item.product_title)
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((product, index) => {
|
||||||
|
const colors = [
|
||||||
|
"#10B981",
|
||||||
|
"#EF4444",
|
||||||
|
"#3B82F6",
|
||||||
|
"#F59E0B",
|
||||||
|
"#8B5CF6",
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={product}
|
||||||
|
type="linear"
|
||||||
|
dot={false}
|
||||||
|
activeDot={{ r: 5 }}
|
||||||
|
dataKey={product}
|
||||||
|
stroke={colors[index % colors.length]}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</LineChart>
|
</LineChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
|
دادهای برای نمایش وجود ندارد
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -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]">
|
||||||
|
|
|
||||||
|
|
@ -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 || []);
|
||||||
|
|
|
||||||
|
|
@ -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,27 +63,44 @@ 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(() => {
|
||||||
|
fetchCounts();
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
const fetchCounts = async () => {
|
const fetchCounts = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [countsRes, processRes] = await Promise.all([
|
const [countsRes, processRes] = await Promise.all([
|
||||||
apiService.call<EcosystemCounts>({
|
apiService.call<EcosystemCounts>({
|
||||||
ecosystem_count_function: {},
|
ecosystem_count_function: {
|
||||||
|
start_date: date?.start || null,
|
||||||
|
end_date: date?.end || null,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
apiService.call<ProcessActorsResponse[]>({
|
apiService.call<ProcessActorsResponse[]>({
|
||||||
process_creating_actors_function: {},
|
process_creating_actors_function: {
|
||||||
|
start_date: date?.start || null,
|
||||||
|
end_date: date?.end || null,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setCounts(
|
setCounts(
|
||||||
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
|
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process the years data and fill missing years
|
// Process the years data and fill missing years
|
||||||
const processedData = processYearsData(
|
const processedData = processYearsData(
|
||||||
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
|
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors)
|
||||||
);
|
);
|
||||||
setProcessData(processedData);
|
setProcessData(processedData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -91,8 +109,6 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchCounts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 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++) {
|
||||||
|
|
@ -462,7 +478,13 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
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
|
||||||
|
id="fillDesktop"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2="1"
|
||||||
|
>
|
||||||
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
|
||||||
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
|
@ -501,7 +523,14 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
activeDot={({ cx, cy, payload }) => (
|
activeDot={({ cx, cy, payload }) => (
|
||||||
<g>
|
<g>
|
||||||
{/* Small circle */}
|
{/* Small circle */}
|
||||||
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
|
<circle
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={5}
|
||||||
|
fill="#3AEA83"
|
||||||
|
stroke="#fff"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
{/* Year label above point */}
|
{/* Year label above point */}
|
||||||
<text
|
<text
|
||||||
x={cx}
|
x={cx}
|
||||||
|
|
@ -517,8 +546,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ResponsiveContainer>
|
</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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
async (stage_id: number) => {
|
||||||
return await apiService.call<any>({
|
return await apiService.call<any>({
|
||||||
get_values_workflow_function: {
|
get_values_workflow_function: {
|
||||||
stage_id: stage_id,
|
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,12 +270,12 @@ 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;
|
||||||
|
|
@ -270,22 +298,20 @@ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps
|
||||||
// نودهای نهایی **هیچ fx/fy نداشته باشند**
|
// نودهای نهایی **هیچ fx/fy نداشته باشند**
|
||||||
// فقط forceLink آنها را به دستهها متصل نگه میدارد
|
// فقط 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);
|
||||||
// categoryNodes.forEach((catNode) => {
|
// const childCount = childNodes.length;
|
||||||
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
|
// const radius = 100; // فاصله از دسته
|
||||||
// const childCount = childNodes.length;
|
// const angleStep = (2 * Math.PI) / childCount;
|
||||||
// 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);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
// 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,7 +331,7 @@ 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>()
|
||||||
|
|
@ -437,7 +463,6 @@ 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();
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
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 { 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 آرایه از مقادیر دادهها
|
||||||
|
|
@ -117,7 +116,7 @@ function calculateNiceNumber(value: number, round: boolean): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
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
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