feat: completed designed
This commit is contained in:
parent
584450550b
commit
173176bbb5
|
|
@ -1,38 +1,6 @@
|
||||||
import { useState, useEffect } from "react";
|
import { Book, CheckCircle } from "lucide-react";
|
||||||
import { DashboardLayout } from "./layout";
|
import { useEffect, useState } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import { Progress } from "~/components/ui/progress";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
BarChart,
|
|
||||||
Bar,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
LineChart,
|
|
||||||
Line,
|
|
||||||
} from "recharts";
|
|
||||||
import apiService from "~/lib/api";
|
|
||||||
import toast from "react-hot-toast";
|
import 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,10 +8,20 @@ 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 [dashboardData, setDashboardData] = useState<any | null>(null);
|
const [dashboardData, setDashboardData] = useState<any | null>(null);
|
||||||
|
|
@ -51,17 +29,30 @@ export function DashboardHome() {
|
||||||
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 [totalIncreasedCapacity, setTotalIncreasedCapacity] =
|
||||||
|
// useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDashboardData = async () => {
|
useEffect(() => {
|
||||||
|
EventBus.on("dateSelected", (date: CalendarDate) => {
|
||||||
|
if (date) fetchDashboardData(date.start, date.end);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchDashboardData = async (startDate?: string, endDate?: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -74,12 +65,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: startDate || null,
|
||||||
|
end_date: endDate || 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: startDate || null,
|
||||||
|
end_date: endDate || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
|
const topCardsResponseData = JSON.parse(topCardsResponse?.data);
|
||||||
|
|
@ -130,12 +127,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;
|
||||||
|
|
||||||
|
|
@ -147,14 +162,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 =
|
||||||
|
|
@ -172,20 +187,19 @@ export function DashboardHome() {
|
||||||
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: {
|
||||||
|
|
@ -329,20 +343,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)",
|
||||||
|
|
@ -353,19 +366,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) *
|
||||||
|
|
@ -381,11 +393,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}
|
||||||
|
|
@ -411,22 +419,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>
|
||||||
|
|
@ -443,14 +451,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>
|
||||||
|
|
@ -460,130 +468,144 @@ export function DashboardHome() {
|
||||||
{/* Revenue Card */}
|
{/* Revenue Card */}
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
title="افزایش درآمد مبتنی بر فناوری و نوآوری"
|
||||||
value={dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll("," , "") || "0"}
|
value={
|
||||||
percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
|
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll(
|
||||||
|
",",
|
||||||
|
""
|
||||||
|
) || "0"
|
||||||
|
}
|
||||||
|
percentValue={
|
||||||
|
dashboardData.topData
|
||||||
|
?.technology_innovation_based_revenue_growth_percent
|
||||||
|
}
|
||||||
percentLabel="درصد به کل درآمد"
|
percentLabel="درصد به کل درآمد"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Cost Reduction Card */}
|
{/* Cost Reduction Card */}
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
title="کاهش هزینه ها مبتنی بر فناوری و نوآوری"
|
||||||
value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0"))}
|
value={Math.round(
|
||||||
percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
|
parseFloat(
|
||||||
|
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
|
||||||
|
/,/g,
|
||||||
|
""
|
||||||
|
) || "0"
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
percentValue={
|
||||||
|
dashboardData.topData
|
||||||
|
?.technology_innovation_based_cost_reduction_percent || "0"
|
||||||
|
}
|
||||||
percentLabel="درصد به کل هزینه"
|
percentLabel="درصد به کل هزینه"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Budget Ratio Card */}
|
{/* Budget Ratio Card */}
|
||||||
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
|
<BaseCard title="نسبت تحقق بودجه فناوی و نوآوری">
|
||||||
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
<div className="flex items-center gap-2 justify-center flex-row-reverse">
|
||||||
<ChartContainer
|
<ChartContainer
|
||||||
config={chartConfig}
|
config={chartConfig}
|
||||||
className="aspect-square w-[6rem] h-auto"
|
className="aspect-square w-[6rem] h-auto"
|
||||||
|
>
|
||||||
|
<RadialBarChart
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
browser: "budget",
|
||||||
|
visitors: parseFloat(
|
||||||
|
dashboardData.topData
|
||||||
|
?.innovation_budget_achievement_percent || "0"
|
||||||
|
),
|
||||||
|
fill: "var(--color-green)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
startAngle={90}
|
||||||
|
endAngle={
|
||||||
|
90 +
|
||||||
|
(dashboardData.topData
|
||||||
|
?.innovation_budget_achievement_percent /
|
||||||
|
100) *
|
||||||
|
360
|
||||||
|
}
|
||||||
|
innerRadius={35}
|
||||||
|
outerRadius={55}
|
||||||
|
>
|
||||||
|
<PolarGrid
|
||||||
|
gridType="circle"
|
||||||
|
radialLines={false}
|
||||||
|
stroke="none"
|
||||||
|
className="first:fill-pr-red last:fill-[#24273A]"
|
||||||
|
polarRadius={[38, 31]}
|
||||||
|
/>
|
||||||
|
<RadialBar dataKey="visitors" background cornerRadius={5} />
|
||||||
|
<PolarRadiusAxis
|
||||||
|
tick={false}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
>
|
>
|
||||||
<RadialBarChart
|
<Label
|
||||||
data={[
|
content={({ viewBox }) => {
|
||||||
{
|
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||||
browser: "budget",
|
return (
|
||||||
visitors: parseFloat(
|
<text
|
||||||
dashboardData.topData
|
x={viewBox.cx}
|
||||||
?.innovation_budget_achievement_percent || "0",
|
y={viewBox.cy}
|
||||||
),
|
textAnchor="middle"
|
||||||
fill: "var(--color-green)",
|
dominantBaseline="middle"
|
||||||
},
|
>
|
||||||
]}
|
<tspan
|
||||||
startAngle={90}
|
x={viewBox.cx}
|
||||||
endAngle={
|
y={viewBox.cy}
|
||||||
90 +
|
className="fill-foreground text-lg font-bold"
|
||||||
(dashboardData.topData
|
>
|
||||||
?.innovation_budget_achievement_percent /
|
%
|
||||||
100) *
|
{formatNumber(
|
||||||
360
|
Math.round(
|
||||||
}
|
dashboardData.topData
|
||||||
innerRadius={35}
|
?.innovation_budget_achievement_percent ||
|
||||||
outerRadius={55}
|
0
|
||||||
>
|
)
|
||||||
<PolarGrid
|
)}
|
||||||
gridType="circle"
|
</tspan>
|
||||||
radialLines={false}
|
</text>
|
||||||
stroke="none"
|
);
|
||||||
className="first:fill-pr-red last:fill-[#24273A]"
|
}
|
||||||
polarRadius={[38, 31]}
|
}}
|
||||||
/>
|
/>
|
||||||
<RadialBar
|
</PolarRadiusAxis>
|
||||||
dataKey="visitors"
|
</RadialBarChart>
|
||||||
background
|
</ChartContainer>
|
||||||
cornerRadius={5}
|
<div className="font-bold font-persian text-center">
|
||||||
/>
|
<div className="flex flex-col justify-between items-center gap-2">
|
||||||
<PolarRadiusAxis
|
<span className="flex font-bold items-center text-base gap-1 mr-auto">
|
||||||
tick={false}
|
<div className="font-light text-sm">مصوب :</div>
|
||||||
tickLine={false}
|
{formatNumber(
|
||||||
axisLine={false}
|
Math.round(
|
||||||
>
|
parseFloat(
|
||||||
<Label
|
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
||||||
content={({ viewBox }) => {
|
/,/g,
|
||||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
""
|
||||||
return (
|
) || "0"
|
||||||
<text
|
)
|
||||||
x={viewBox.cx}
|
)
|
||||||
y={viewBox.cy}
|
)}
|
||||||
textAnchor="middle"
|
</span>
|
||||||
dominantBaseline="middle"
|
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
||||||
>
|
<div className="font-light text-sm">جذب شده :</div>
|
||||||
<tspan
|
{formatNumber(
|
||||||
x={viewBox.cx}
|
Math.round(
|
||||||
y={viewBox.cy}
|
parseFloat(
|
||||||
className="fill-foreground text-lg font-bold"
|
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
||||||
>
|
/,/g,
|
||||||
%
|
""
|
||||||
{formatNumber(
|
) || "0"
|
||||||
Math.round(
|
)
|
||||||
dashboardData.topData
|
)
|
||||||
?.innovation_budget_achievement_percent ||
|
)}
|
||||||
0,
|
</span>
|
||||||
),
|
|
||||||
)}
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PolarRadiusAxis>
|
|
||||||
</RadialBarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
<div className="font-bold font-persian text-center">
|
|
||||||
<div className="flex flex-col justify-between items-center gap-2">
|
|
||||||
<span className="flex font-bold items-center text-base gap-1 mr-auto">
|
|
||||||
<div className="font-light text-sm">مصوب :</div>
|
|
||||||
{formatNumber(
|
|
||||||
Math.round(
|
|
||||||
parseFloat(
|
|
||||||
dashboardData.topData?.approved_innovation_budget_achievement_ratio?.replace(
|
|
||||||
/,/g,
|
|
||||||
"",
|
|
||||||
) || "0",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1 text-base font-bold mr-auto">
|
|
||||||
<div className="font-light text-sm">جذب شده :</div>
|
|
||||||
{formatNumber(
|
|
||||||
Math.round(
|
|
||||||
parseFloat(
|
|
||||||
dashboardData.topData?.allocated_innovation_budget_achievement_ratio?.replace(
|
|
||||||
/,/g,
|
|
||||||
"",
|
|
||||||
) || "0",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</BaseCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main Content with Tabs */}
|
{/* Main Content with Tabs */}
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
@ -595,10 +617,13 @@ export function DashboardHome() {
|
||||||
تحقق ارزش ها
|
تحقق ارزش ها
|
||||||
</p>
|
</p>
|
||||||
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
|
<TabsList className="bg-transparent py-2 m-6 border-[1px] border-[#5F6284]">
|
||||||
<TabsTrigger value="canvas" className="cursor-pointer">
|
<TabsTrigger value="canvas" className="cursor-pointer">
|
||||||
شماتیک
|
شماتیک
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="charts" className=" text-white cursor-pointer font-light ">
|
<TabsTrigger
|
||||||
|
value="charts"
|
||||||
|
className=" text-white cursor-pointer font-light "
|
||||||
|
>
|
||||||
مقایسه ای
|
مقایسه ای
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
@ -611,27 +636,25 @@ export function DashboardHome() {
|
||||||
<TabsContent value="canvas" className="w-ful h-full">
|
<TabsContent value="canvas" className="w-ful h-full">
|
||||||
<div className="p-4 h-full w-full">
|
<div className="p-4 h-full w-full">
|
||||||
<D3ImageInfo
|
<D3ImageInfo
|
||||||
companies={
|
companies={companyChartData.map((item) => {
|
||||||
companyChartData.map((item) => {
|
const imageMap: Record<string, string> = {
|
||||||
const imageMap: Record<string, string> = {
|
بسپاران: "/besparan.png",
|
||||||
"بسپاران": "/besparan.png",
|
خوارزمی: "/khwarazmi.png",
|
||||||
"خوارزمی": "/khwarazmi.png",
|
"فراورش 1": "/faravash1.png",
|
||||||
"فراورش 1": "/faravash1.png",
|
"فراورش 2": "/faravash2.png",
|
||||||
"فراورش 2": "/faravash2.png",
|
کیمیا: "/kimia.png",
|
||||||
"کیمیا": "/kimia.png",
|
"آب نیرو": "/abniro.png",
|
||||||
"آب نیرو": "/abniro.png",
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.category,
|
id: item.category,
|
||||||
name: item.category,
|
name: item.category,
|
||||||
imageUrl: imageMap[item.category] || "/placeholder.png",
|
imageUrl: imageMap[item.category] || "/placeholder.png",
|
||||||
cost: item?.costI || 0,
|
cost: item?.costI || 0,
|
||||||
capacity: item?.capacityI || 0,
|
capacity: item?.capacityI || 0,
|
||||||
revenue: item?.revenueI || 0,
|
revenue: item?.revenueI || 0,
|
||||||
};
|
};
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
@ -649,7 +672,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"
|
||||||
/>
|
/>
|
||||||
|
|
@ -667,21 +690,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",
|
||||||
},
|
},
|
||||||
|
|
@ -706,7 +729,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>
|
||||||
|
|
@ -717,7 +740,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>
|
||||||
|
|
@ -728,7 +751,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>
|
||||||
|
|
@ -739,7 +762,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>
|
||||||
|
|
@ -763,7 +786,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>
|
||||||
|
|
@ -774,7 +797,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>
|
||||||
|
|
@ -785,7 +808,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>
|
||||||
|
|
@ -796,7 +819,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>
|
||||||
|
|
@ -804,9 +827,8 @@ export function DashboardHome() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</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,111 @@ 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 [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
|
||||||
|
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
|
||||||
|
const [selectedDate, setSelectedDate] = useState<CurrentDay>();
|
||||||
|
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
|
||||||
|
const calendarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [currentYear, setCurrentYear] = useState<SelectedDate>({
|
||||||
|
since: jy,
|
||||||
|
until: jy,
|
||||||
|
});
|
||||||
|
|
||||||
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)) {
|
||||||
|
setCurrentYear((prev) => ({
|
||||||
|
...prev,
|
||||||
|
since: currentYear?.since! + 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevFromYearHandler = () => {
|
||||||
|
setCurrentYear((prev) => ({
|
||||||
|
...prev,
|
||||||
|
since: currentYear?.since! - 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFromDateHandler = (val: MonthItem) => {
|
||||||
|
const data = {
|
||||||
|
start: `${currentYear.since}/${val.start}`,
|
||||||
|
sinceMonth: val.label,
|
||||||
|
};
|
||||||
|
setSelectedDate((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...data,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextUntilYearHandler = () => {
|
||||||
|
setCurrentYear((prev) => ({
|
||||||
|
...prev,
|
||||||
|
until: currentYear?.until! + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevUntilYearHandler = () => {
|
||||||
|
if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) {
|
||||||
|
setCurrentYear((prev) => ({
|
||||||
|
...prev,
|
||||||
|
until: currentYear?.until! - 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectUntilDateHandler = (val: MonthItem) => {
|
||||||
|
const data = {
|
||||||
|
end: `${currentYear.until}/${val.end}`,
|
||||||
|
fromMonth: val.label,
|
||||||
|
};
|
||||||
|
setSelectedDate((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...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);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
EventBus.emit("dateSelected", selectedDate);
|
||||||
|
}, [currentYear, selectedDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
"backdrop-blur-sm border-b border-gray-400/30 h-16 flex items-center justify-between px-4 lg:px-6 shadow-sm relative z-30",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Left Section */}
|
{/* Left Section */}
|
||||||
|
|
@ -69,24 +200,74 @@ export function Header({
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
<h1 className="text-xl flex items-center justify-center gap-4 font-bold text-white font-persian">
|
||||||
{/* Right-side icon for current page */}
|
{/* Right-side icon for current page */}
|
||||||
{titleIcon ? (
|
{titleIcon ? (
|
||||||
<div className="flex items-center gap-2 mr-4">
|
<div className="flex items-center gap-2 mr-4">
|
||||||
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
{React.createElement(titleIcon, { className: "w-5 h-5 " })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
|
)}
|
||||||
|
{title.includes("-") ? (
|
||||||
|
<div className="flex row items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{title.split("-")[0]}
|
||||||
|
<ChevronLeft className="inline-block w-4 h-4" />
|
||||||
|
{title.split("-")[1]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
)}
|
)}
|
||||||
{title.includes("-") ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{title.split("-")[0]}
|
|
||||||
<ChevronLeft className="inline-block w-4 h-4" />
|
|
||||||
{title.split("-")[1]}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
title
|
|
||||||
)}
|
|
||||||
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div ref={calendarRef} className="flex flex-col gap-3 relative">
|
||||||
|
<div
|
||||||
|
onClick={toggleCalendar}
|
||||||
|
className="flex flex-row gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-72 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<Calendar size={20} />
|
||||||
|
{selectedDate ? (
|
||||||
|
<div className="flex flex-row justify-between w-full gap-2.5 min-w-64 font-bold">
|
||||||
|
<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-1 absolute top-14 w-full 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-52 border border-[#5F6284] block mt-3"></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 +275,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 +291,6 @@ export function Header({
|
||||||
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
|
||||||
className="flex items-center gap-2 text-gray-300"
|
className="flex items-center gap-2 text-gray-300"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="hidden sm:block text-right">
|
<div className="hidden sm:block text-right">
|
||||||
<div className="text-sm font-medium font-persian">
|
<div className="text-sm font-medium font-persian">
|
||||||
{user?.name} {user?.family}
|
{user?.name} {user?.family}
|
||||||
|
|
@ -118,7 +299,7 @@ export function Header({
|
||||||
{user?.username}
|
{user?.username}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-gradient-to-r from-emerald-500/20 to-teal-500/20 text-emerald-400 rounded-lg flex items-center justify-center">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -686,6 +686,12 @@ export function GreenInnovationPage() {
|
||||||
{ name: recycleParams.food.label, pv: 30, amt: 50 },
|
{ name: recycleParams.food.label, pv: 30, amt: 50 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// EventBus.on("dateSelected", (date) => {
|
||||||
|
// debugger;
|
||||||
|
// });
|
||||||
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="نوآوری سبز">
|
<DashboardLayout title="نوآوری سبز">
|
||||||
<div className="space-y-4 h-[23.5rem]">
|
<div className="space-y-4 h-[23.5rem]">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
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 p-3 w-full">
|
||||||
|
<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 آرایه از مقادیر دادهها
|
||||||
|
|
@ -46,7 +45,7 @@ export function calculateNiceRange(
|
||||||
|
|
||||||
// پیدا کردن حداکثر مقدار در دادهها
|
// پیدا کردن حداکثر مقدار در دادهها
|
||||||
const dataMax = Math.max(...values);
|
const dataMax = Math.max(...values);
|
||||||
|
|
||||||
// اگر همه مقادیر صفر یا منفی هستند
|
// اگر همه مقادیر صفر یا منفی هستند
|
||||||
if (dataMax <= 0) {
|
if (dataMax <= 0) {
|
||||||
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
|
return { niceMax: 100, tickInterval: 20, ticks: [0, 20, 40, 60, 80, 100] };
|
||||||
|
|
@ -57,19 +56,19 @@ export function calculateNiceRange(
|
||||||
|
|
||||||
// محاسبه nice upper limit
|
// محاسبه nice upper limit
|
||||||
const niceMax = calculateNiceNumber(maxWithMargin, true);
|
const niceMax = calculateNiceNumber(maxWithMargin, true);
|
||||||
|
|
||||||
// محاسبه فاصله مناسب tick ها بر اساس niceMax
|
// محاسبه فاصله مناسب tick ها بر اساس niceMax
|
||||||
const range = niceMax - minValue;
|
const range = niceMax - minValue;
|
||||||
const targetTicks = 5; // هدف: 5 tick
|
const targetTicks = 5; // هدف: 5 tick
|
||||||
const roughTickInterval = range / (targetTicks - 1);
|
const roughTickInterval = range / (targetTicks - 1);
|
||||||
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
|
const niceTickInterval = calculateNiceNumber(roughTickInterval, false);
|
||||||
|
|
||||||
// ایجاد آرایه tick ها
|
// ایجاد آرایه tick ها
|
||||||
const ticks: number[] = [];
|
const ticks: number[] = [];
|
||||||
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
|
for (let i = minValue; i <= niceMax; i += niceTickInterval) {
|
||||||
ticks.push(Math.round(i));
|
ticks.push(Math.round(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
// اطمینان از اینکه niceMax در آرایه tick ها باشد
|
// اطمینان از اینکه niceMax در آرایه tick ها باشد
|
||||||
if (ticks[ticks.length - 1] !== niceMax) {
|
if (ticks[ticks.length - 1] !== niceMax) {
|
||||||
ticks.push(niceMax);
|
ticks.push(niceMax);
|
||||||
|
|
@ -90,13 +89,13 @@ export function calculateNiceRange(
|
||||||
*/
|
*/
|
||||||
function calculateNiceNumber(value: number, round: boolean): number {
|
function calculateNiceNumber(value: number, round: boolean): number {
|
||||||
if (value <= 0) return 0;
|
if (value <= 0) return 0;
|
||||||
|
|
||||||
// پیدا کردن قدرت 10
|
// پیدا کردن قدرت 10
|
||||||
const exponent = Math.floor(Math.log10(value));
|
const exponent = Math.floor(Math.log10(value));
|
||||||
const fraction = value / Math.pow(10, exponent);
|
const fraction = value / Math.pow(10, exponent);
|
||||||
|
|
||||||
let niceFraction: number;
|
let niceFraction: number;
|
||||||
|
|
||||||
if (round) {
|
if (round) {
|
||||||
// برای حداکثر: به سمت بالا گرد میکنیم با دقت بیشتر
|
// برای حداکثر: به سمت بالا گرد میکنیم با دقت بیشتر
|
||||||
if (fraction <= 1.0) niceFraction = 1;
|
if (fraction <= 1.0) niceFraction = 1;
|
||||||
|
|
@ -112,12 +111,12 @@ function calculateNiceNumber(value: number, round: boolean): number {
|
||||||
else if (fraction <= 5.0) niceFraction = 5;
|
else if (fraction <= 5.0) niceFraction = 5;
|
||||||
else niceFraction = 10;
|
else niceFraction = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
return niceFraction * Math.pow(10, exponent);
|
return niceFraction * Math.pow(10, exponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleDataValue = (val: any): any => {
|
export const handleDataValue = (val: any): any => {
|
||||||
moment.loadPersian({ usePersianDigits: true });
|
moment.loadPersian({ usePersianDigits: true });
|
||||||
if (val == null) return val;
|
if (val == null) return val;
|
||||||
if (
|
if (
|
||||||
typeof val === "string" &&
|
typeof val === "string" &&
|
||||||
|
|
@ -132,4 +131,6 @@ moment.loadPersian({ usePersianDigits: true });
|
||||||
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||||||
}
|
}
|
||||||
return val;
|
return val;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const EventBus = new EventEmitter();
|
||||||
|
|
|
||||||
6
app/types/util.type.ts
Normal file
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