Compare commits

..

No commits in common. "0dd1fe2ec2798a843425b3a9e2bdcd912513b153" and "8749cebe7c06d4e70e74c01dabd264642effcb78" have entirely different histories.

16 changed files with 1479 additions and 2339 deletions

View File

@ -1,7 +1,38 @@
import jalaali from "jalaali-js"; 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,
@ -9,53 +40,26 @@ import {
RadialBar, RadialBar,
RadialBarChart, RadialBarChart,
} from "recharts"; } from "recharts";
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 { ChartContainer } from "~/components/ui/chart";
import { formatNumber } from "~/lib/utils";
import { MetricCard } from "~/components/ui/metric-card"; import { MetricCard } from "~/components/ui/metric-card";
import { Progress } from "~/components/ui/progress"; import { BaseCard } from "~/components/ui/base-card";
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,
category: string; capacityI : number,
capacity: number; revenueI : 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 {
@ -64,18 +68,12 @@ 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);
@ -108,10 +106,6 @@ 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"],
}; };
@ -130,30 +124,12 @@ 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 = const preFee = Number(r?.pre_innovation_fee_sum ?? 0) >= 0 ? r?.pre_innovation_fee_sum : 0;
Number(r?.pre_innovation_fee_sum ?? 0) >= 0 const costRed = Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 ? r?.innovation_cost_reduction_sum : 0;
? r?.pre_innovation_fee_sum const preCap = Number(r?.pre_project_production_capacity_sum ?? 0) >= 0 ? r?.pre_project_production_capacity_sum : 0;
: 0; const incCap = Number(r?.increased_capacity_after_innovation_sum ?? 0) >= 0 ? r?.increased_capacity_after_innovation_sum : 0;
const costRed = const preInc = Number(r?.pre_project_income_sum ?? 0) >= 0 ? r?.pre_project_income_sum : 0;
Number(r?.innovation_cost_reduction_sum ?? 0) >= 0 const incInc = Number(r?.increased_income_after_innovation_sum ?? 0) >= 0 ? r?.increased_income_after_innovation_sum : 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;
@ -165,14 +141,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 =
@ -185,24 +161,25 @@ 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 = registered > 0 ? (ongoing / registered) * 100 : 0; const percentage =
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: {
@ -346,19 +323,20 @@ 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 || "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,
fill: "var(--color-green)", fill: "var(--color-green)",
@ -369,18 +347,19 @@ 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 || "1" ?.registered_innovation_technology_idea ||
"1",
)) * )) *
100 100,
) )
: 0) / : 0) /
100) * 100) *
@ -396,7 +375,11 @@ 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 dataKey="visitors" background cornerRadius={5} /> <RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis <PolarRadiusAxis
tick={false} tick={false}
tickLine={false} tickLine={false}
@ -422,22 +405,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>
@ -454,14 +437,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>
@ -471,34 +454,16 @@ export function DashboardHome() {
{/* Revenue Card */} {/* Revenue Card */}
<MetricCard <MetricCard
title="افزایش درآمد مبتنی بر فناوری و نوآوری" title="افزایش درآمد مبتنی بر فناوری و نوآوری"
value={ value={dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll("," , "") || "0"}
dashboardData.topData?.technology_innovation_based_revenue_growth?.replaceAll( percentValue={dashboardData.topData?.technology_innovation_based_revenue_growth_percent}
",",
""
) || "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( value={Math.round(parseFloat(dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(/,/g, "") || "0"))}
parseFloat( percentValue={dashboardData.topData?.technology_innovation_based_cost_reduction_percent || "0"}
dashboardData.topData?.technology_innovation_based_cost_reduction?.replace(
/,/g,
""
) || "0"
)
)}
percentValue={
dashboardData.topData
?.technology_innovation_based_cost_reduction_percent || "0"
}
percentLabel="درصد به کل هزینه" percentLabel="درصد به کل هزینه"
/> />
@ -515,7 +480,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)",
}, },
@ -538,7 +503,11 @@ 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 dataKey="visitors" background cornerRadius={5} /> <RadialBar
dataKey="visitors"
background
cornerRadius={5}
/>
<PolarRadiusAxis <PolarRadiusAxis
tick={false} tick={false}
tickLine={false} tickLine={false}
@ -564,8 +533,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>
@ -585,10 +554,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">
@ -598,10 +567,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>
@ -623,10 +592,7 @@ export function DashboardHome() {
<TabsTrigger value="canvas" className="cursor-pointer"> <TabsTrigger value="canvas" className="cursor-pointer">
شماتیک شماتیک
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger value="charts" className=" text-white cursor-pointer font-light ">
value="charts"
className=" text-white cursor-pointer font-light "
>
مقایسه ای مقایسه ای
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -639,13 +605,14 @@ 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={companyChartData.map((item) => { companies={
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",
}; };
@ -657,7 +624,8 @@ export function DashboardHome() {
capacity: item?.capacityI || 0, capacity: item?.capacityI || 0,
revenue: item?.revenueI || 0, revenue: item?.revenueI || 0,
}; };
})} })
}
/> />
</div> </div>
</TabsContent> </TabsContent>
@ -675,7 +643,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"
/> />
@ -693,21 +661,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",
}, },
@ -732,7 +700,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>
@ -743,7 +711,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>
@ -754,7 +722,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>
@ -765,7 +733,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>
@ -789,7 +757,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>
@ -800,7 +768,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>
@ -811,7 +779,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>
@ -822,7 +790,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>
@ -832,6 +800,7 @@ export function DashboardHome() {
</div> </div>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@ -1,20 +1,21 @@
import jalaali from "jalaali-js"; import React, { useEffect, useState } from "react";
import { useAuth } from "~/contexts/auth-context";
import { Link } from "react-router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui/button";
import { 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;
@ -23,52 +24,6 @@ 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,
@ -76,131 +31,25 @@ export function Header({
titleIcon, titleIcon,
}: HeaderProps) { }: HeaderProps) {
const { user } = useAuth(); const { user } = useAuth();
const { jy } = jalaali.toJalaali(new Date()); const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
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 */}
@ -228,66 +77,16 @@ export function Header({
<PanelLeft /> <PanelLeft />
)} )}
{title.includes("-") ? ( {title.includes("-") ? (
<div className="flex row items-center gap-4"> <span className="flex items-center gap-1">
<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]}
</div> </span>
</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 */}
@ -295,15 +94,14 @@ 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"
@ -311,6 +109,7 @@ 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}

View File

@ -1,8 +1,9 @@
import { useState } from "react"; import { useState } from "react";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { Header } from "./header";
import { Sidebar } from "./sidebar"; import { Sidebar } from "./sidebar";
import { Header } from "./header";
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;
@ -17,14 +18,9 @@ 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] = const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
useState(false); const [currentTitle, setCurrentTitle] = useState<string | undefined>(title ?? "صفحه اول");
const [currentTitle, setCurrentTitle] = useState<string | undefined>( const [currentTitleIcon, setCurrentTitleIcon] = useState<React.ComponentType<{ className?: string }> | null | undefined>(undefined);
title ?? "صفحه اول"
);
const [currentTitleIcon, setCurrentTitleIcon] = useState<
React.ComponentType<{ className?: string }> | null | undefined
>(undefined);
const toggleSidebarCollapse = () => { const toggleSidebarCollapse = () => {
setIsSidebarCollapsed(!isSidebarCollapsed); setIsSidebarCollapsed(!isSidebarCollapsed);
@ -34,6 +30,8 @@ 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"
@ -57,20 +55,19 @@ 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={() => onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
setIsStrategicAlignmentPopupOpen(true)
}
onTitleChange={(info) => { onTitleChange={(info) => {
setCurrentTitle(info.title); setCurrentTitle(info.title);
setCurrentTitleIcon(info.icon ?? null); setCurrentTitleIcon(info.icon ?? null);
}} }}
/> />
</div> </div>
@ -88,7 +85,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">
@ -96,10 +93,7 @@ export function DashboardLayout({
</div> </div>
</main> </main>
</div> </div>
<StrategicAlignmentPopup <StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
open={isStrategicAlignmentPopupOpen}
onOpenChange={setIsStrategicAlignmentPopupOpen}
/>
</div> </div>
); );
} }

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { import {
BrainCircuit, BrainCircuit,
ChevronDown, ChevronDown,
@ -13,7 +12,7 @@ import {
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import moment from "moment-jalaali"; import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } 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 { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
@ -35,8 +34,7 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { 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 });
@ -148,18 +146,13 @@ 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[]>([]);
@ -288,11 +281,7 @@ export function DigitalInnovationPage() {
"reduce_costs_percent", "reduce_costs_percent",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [ Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
["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 },
}); });
@ -305,16 +294,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);
} }
@ -322,14 +311,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);
} }
@ -337,7 +326,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);
} }
@ -346,7 +335,7 @@ export function DigitalInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها"); toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
// setTotalCount(0); setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} finally { } finally {
@ -367,15 +356,7 @@ export function DigitalInnovationPage() {
fetchTable(true); fetchTable(true);
fetchTotalCount(); fetchTotalCount();
fetchStats(); fetchStats();
}, [sortConfig, date]); }, [sortConfig]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -431,23 +412,19 @@ export function DigitalInnovationPage() {
direction: direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc", prev.field === field && prev.direction === "asc" ? "desc" : "asc",
})); }));
fetchTotalCount(date?.start, date?.end); fetchTotalCount();
fetchStats(date?.start, date?.end); fetchStats();
setCurrentPage(1); setCurrentPage(1);
setProjects([]); setProjects([]);
setHasMore(true); setHasMore(true);
}; };
const fetchTotalCount = async (startDate?: string, endDate?: string) => { const fetchTotalCount = async () => {
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: [ Conditions: [["type_of_innovation", "=", "نوآوری دیجیتال"]],
["type_of_innovation", "=", "نوآوری دیجیتال", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -474,10 +451,7 @@ 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;
@ -555,33 +529,33 @@ export function DigitalInnovationPage() {
// fetchStats(); // fetchStats();
// }; // };
// const renderProgress = useMemo(() => { const renderProgress = useMemo(() => {
// const total = 10; const total = 10;
// for (let i = 0; i < rating.length; i++) { for (let i = 0; i < rating.length; i++) {
// const currentElm = rating[i]; const currentElm = rating[i];
// currentElm.house = []; currentElm.house = [];
// const greenBoxes = Math.floor((total * currentElm.development) / 100); const greenBoxes = Math.floor((total * currentElm.development) / 100);
// const partialPercent = const partialPercent =
// (total * currentElm.development) / 100 - greenBoxes; (total * currentElm.development) / 100 - greenBoxes;
// for (let j = 0; j < greenBoxes; j++) { for (let j = 0; j < greenBoxes; j++) {
// currentElm.house.push({ currentElm.house.push({
// index: j, index: j,
// color: "!bg-emerald-400", color: "!bg-emerald-400",
// }); });
// } }
// if (partialPercent != 0 && greenBoxes != 10) if (partialPercent != 0 && greenBoxes != 10)
// currentElm.house.push({ currentElm.house.push({
// index: greenBoxes + 1, index: greenBoxes + 1,
// style: `linear-gradient( style: `linear-gradient(
// to right, to right,
// oklch(76.5% 0.177 163.223) 0%, oklch(76.5% 0.177 163.223) 0%,
// oklch(76.5% 0.177 163.223) ${partialPercent * 100}%, oklch(76.5% 0.177 163.223) ${partialPercent * 100}%,
// oklch(55.1% 0.027 264.364) ${partialPercent * 100}%, oklch(55.1% 0.027 264.364) ${partialPercent * 100}%,
// oklch(55.1% 0.027 264.364) 100% oklch(55.1% 0.027 264.364) 100%
// )`, )`,
// }); });
// } }
// }, [rating]); }, [rating]);
const statusColor = (status: projectStatus): any => { const statusColor = (status: projectStatus): any => {
let el = null; let el = null;

View File

@ -26,9 +26,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { EventBus, formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -47,7 +46,6 @@ 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 });
@ -159,7 +157,6 @@ 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);
@ -169,10 +166,6 @@ 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",
@ -295,11 +288,7 @@ export function GreenInnovationPage() {
"observer", "observer",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [ Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
["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) {
@ -361,14 +350,6 @@ 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);
@ -378,11 +359,11 @@ export function GreenInnovationPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects, date]); }, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -435,11 +416,7 @@ 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: [ Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
["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;
@ -471,8 +448,6 @@ export function GreenInnovationPage() {
selectedProjects.size > 0 selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ") ? Array.from(selectedProjects).join(" , ")
: "", : "",
start_date: date?.start || null,
end_date: date?.end || null,
}, },
}); });
let payload: any = raw?.data; let payload: any = raw?.data;

View File

@ -19,7 +19,6 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import jalaali from "jalaali-js";
import { import {
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
@ -41,8 +40,7 @@ import {
XAxis, XAxis,
} from "recharts"; } from "recharts";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
interface innovationBuiltInDate { interface innovationBuiltInDate {
@ -179,7 +177,6 @@ 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);
@ -194,10 +191,6 @@ 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>>();
@ -317,11 +310,7 @@ export function InnovationBuiltInsidePage() {
"technology_maturity_level", "technology_maturity_level",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [ Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
["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) {
@ -427,21 +416,13 @@ export function InnovationBuiltInsidePage() {
} }
}, [hasMore, loading]); }, [hasMore, loading]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
}, [sortConfig, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects, date]); }, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -499,8 +480,6 @@ 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;
@ -645,8 +624,7 @@ export function InnovationBuiltInsidePage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleProjectDetails(item)} onClick={() => handleProjectDetails(item)}
className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto" className="text-pr-green hover:text-pr-green underline-offset-4 underline font-normal hover:bg-emerald-500/20 p-2 h-auto">
>
جزئیات بیشتر جزئیات بیشتر
</Button> </Button>
); );

View File

@ -1,40 +1,15 @@
import jalaali from "jalaali-js"; import { ChevronDown, ChevronUp, RefreshCw, Eye, Star, TrendingUp, Hexagon, Download } from "lucide-react";
import { import { useCallback, useEffect, useRef, useState, useMemo, memo } from "react";
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 { import { Badge } from "~/components/ui/badge";
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,
@ -44,9 +19,20 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { 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;
@ -100,19 +86,9 @@ 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" },
]; ];
@ -124,15 +100,7 @@ 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">
@ -144,10 +112,7 @@ 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 <div key={i} className="h-3 bg-gray-600 rounded animate-pulse w-6"></div>
key={i}
className="h-3 bg-gray-600 rounded animate-pulse w-6"
></div>
))} ))}
</div> </div>
@ -173,24 +138,18 @@ 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 className="text-lg font-persian font-semibold text-white mb-4">وضعیت ایده ها</h3>
وضعیت ایده ها
</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( const rechartData = useMemo(() => chartData.map((item) => ({
() =>
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%">
@ -208,9 +167,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}
@ -222,9 +181,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={{
@ -238,7 +197,10 @@ const VerticalBarChart = memo<{
style: { textAnchor: "middle" }, style: { textAnchor: "middle" },
}} }}
/> />
<Bar dataKey="count" radius={[4, 4, 0, 0]}> <Bar
dataKey="count"
radius={[4, 4, 0, 0]}
>
<LabelList <LabelList
dataKey="count" dataKey="count"
position="top" position="top"
@ -255,13 +217,11 @@ 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);
@ -276,10 +236,6 @@ 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[]>([]);
@ -337,10 +293,7 @@ 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) {
@ -408,21 +361,13 @@ 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, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -435,8 +380,7 @@ export function ManageIdeasTechPage() {
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
const handleScroll = () => { const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
return;
if (scrollTimeoutRef.current) { if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current); clearTimeout(scrollTimeoutRef.current);
@ -453,9 +397,7 @@ export function ManageIdeasTechPage() {
}; };
if (scrollContainer) { if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, { scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
passive: true,
});
} }
return () => { return () => {
@ -485,10 +427,7 @@ 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) {
@ -517,10 +456,6 @@ 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) {
@ -530,14 +465,12 @@ 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( const sortedData = parsedData.sort((a, b) => b.full_name_count - a.full_name_count);
(a, b) => b.full_name_count - a.full_name_count
);
const rankedPeople = []; const rankedPeople = [];
let currentRank = 1; let currentRank = 1;
@ -547,15 +480,11 @@ 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 ( if (i > 0 && sortedData[i - 1].full_name_count !== item.full_name_count) {
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 = const normalizedScore = maxCount === minCount
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));
@ -574,9 +503,7 @@ export function ManageIdeasTechPage() {
} }
} }
} else { } else {
toast.error( toast.error(response.message || "خطا در دریافت اطلاعات رتبه‌بندی افراد");
response.message || "خطا در دریافت اطلاعات رتبه‌بندی افراد"
);
} }
} catch (error) { } catch (error) {
console.error("Error fetching people ranking:", error); console.error("Error fetching people ranking:", error);
@ -594,10 +521,6 @@ 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) {
@ -628,10 +551,7 @@ 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) {
@ -701,14 +621,11 @@ 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
@ -727,16 +644,7 @@ export function ManageIdeasTechPage() {
} }
}, []); }, []);
const statusColorPalette = [ const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"];
"#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(() => {
@ -773,7 +681,9 @@ export function ManageIdeasTechPage() {
switch (column.key) { switch (column.key) {
case "idea_title": case "idea_title":
return <span className="text-sm text-white">{String(value)}</span>; return (
<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">
@ -798,7 +708,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":
@ -810,8 +720,7 @@ 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">
@ -821,9 +730,12 @@ 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">
@ -850,10 +762,7 @@ export function ManageIdeasTechPage() {
<TableBody> <TableBody>
{loadingPeople ? ( {loadingPeople ? (
Array.from({ length: 10 }).map((_, index) => ( Array.from({ length: 10 }).map((_, index) => (
<TableRow <TableRow key={`skeleton-${index}`} className="text-sm leading-tight h-12">
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>
@ -862,14 +771,9 @@ 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( {Array.from({ length: 5 }).map((_, starIndex) => (
(_, starIndex) => ( <div key={starIndex} className="w-3 h-3 bg-muted rounded animate-pulse" />
<div ))}
key={starIndex}
className="w-3 h-3 bg-muted rounded animate-pulse"
/>
)
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -884,10 +788,7 @@ export function ManageIdeasTechPage() {
</TableRow> </TableRow>
) : ( ) : (
peopleRanking.map((person) => ( peopleRanking.map((person) => (
<TableRow <TableRow key={person.full_name} className="text-sm leading-tight h-10 not-last:border-b-pr-gray border-border">
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)}
@ -900,8 +801,7 @@ 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( {Array.from({ length: 5 }).map((_, starIndex) => (
(_, starIndex) => (
<Star <Star
key={starIndex} key={starIndex}
className={`w-5 h-5 ${ className={`w-5 h-5 ${
@ -910,8 +810,7 @@ export function ManageIdeasTechPage() {
: "text-pr-gray" : "text-pr-gray"
}`} }`}
/> />
) ))}
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -957,10 +856,7 @@ 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]"> <span className="text-[#ACACAC] text-right font-light text-[8px]">میلیون <br/>ریال</span>
میلیون <br />
ریال
</span>
)} )}
{sortConfig.field === column.key ? ( {sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? ( sortConfig.direction === "asc" ? (
@ -995,9 +891,7 @@ 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={{ style={{ width: `${Math.random() * 60 + 40}%` }}
width: `${Math.random() * 60 + 40}%`,
}}
/> />
</div> </div>
</TableCell> </TableCell>
@ -1037,7 +931,9 @@ export function ManageIdeasTechPage() {
</div> </div>
{/* Infinite scroll trigger */} {/* Infinite scroll trigger */}
<div ref={observerRef} className="h-auto"></div> <div ref={observerRef} className="h-auto">
</div>
</CardContent> </CardContent>
{/* Footer */} {/* Footer */}
@ -1051,17 +947,14 @@ 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> <span className="font-persian text-muted-foreground text-sm">
</span>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Chart Section */} {/* Chart Section */}
<BaseCard <BaseCard icon={TrendingUp} className="col-span-1 mt-12 row-start-2 col-start-3 row-span-1" title="نمودار ایده‌ها">
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}
@ -1101,19 +994,18 @@ export function ManageIdeasTechPage() {
browser: "ideas", browser: "ideas",
visitors: visitors:
parseFloat( parseFloat(
statsData?.registered_innovation_technology_idea || statsData?.registered_innovation_technology_idea || "0"
"0"
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
statsData?.registered_innovation_technology_idea || statsData?.registered_innovation_technology_idea || "0",
"0"
) / ) /
parseFloat( parseFloat(
statsData?.registered_innovation_technology_idea || statsData
"1" ?.registered_innovation_technology_idea ||
"1",
)) * )) *
100 100,
) )
: 0, : 0,
fill: "var(--color-green)", fill: "var(--color-green)",
@ -1123,19 +1015,20 @@ export function ManageIdeasTechPage() {
endAngle={ endAngle={
90 + 90 +
((parseFloat( ((parseFloat(
statsData?.registered_innovation_technology_idea || statsData
"0" ?.registered_innovation_technology_idea || "0",
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
statsData?.ongoing_innovation_technology_ideas || statsData
"0" ?.ongoing_innovation_technology_ideas || "0",
) / ) /
parseFloat( parseFloat(
statsData?.registered_innovation_technology_idea || statsData
"1" ?.registered_innovation_technology_idea ||
"1",
)) * )) *
100 100,
) )
: 0) / : 0) /
100) * 100) *
@ -1179,21 +1072,24 @@ export function ManageIdeasTechPage() {
% %
{formatNumber( {formatNumber(
parseFloat( parseFloat(
statsData?.registered_innovation_technology_idea || statsData
"0" ?.registered_innovation_technology_idea ||
"0",
) > 0 ) > 0
? Math.round( ? Math.round(
(parseFloat( (parseFloat(
statsData?.ongoing_innovation_technology_ideas || statsData
"0" ?.ongoing_innovation_technology_ideas ||
"0",
) / ) /
parseFloat( parseFloat(
statsData?.registered_innovation_technology_idea || statsData
"1" ?.registered_innovation_technology_idea ||
"1",
)) * )) *
100 100,
) )
: 0 : 0,
)} )}
</tspan> </tspan>
</text> </text>
@ -1209,14 +1105,15 @@ 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?.registered_innovation_technology_idea || statsData
"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(
statsData?.ongoing_innovation_technology_ideas || "0" statsData
?.ongoing_innovation_technology_ideas || "0",
)} )}
</span> </span>
</div> </div>
@ -1242,12 +1139,7 @@ export function ManageIdeasTechPage() {
) : ( ) : (
<MetricCard <MetricCard
title="درآمد افزایش یافته" title="درآمد افزایش یافته"
value={ value={statsData?.increased_revenue_from_ideas?.replaceAll("," , "") || "0"}
statsData?.increased_revenue_from_ideas?.replaceAll(
",",
""
) || "0"
}
percentValue={statsData?.increased_revenue_from_ideas_percent} percentValue={statsData?.increased_revenue_from_ideas_percent}
percentLabel="درصد به کل درآمد" percentLabel="درصد به کل درآمد"
/> />
@ -1264,8 +1156,7 @@ export function ManageIdeasTechPage() {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
{selectedIdea && ( {selectedIdea && <div className="flex w-full justify-center gap-4">
<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="">
@ -1275,59 +1166,39 @@ 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 className="text-white text-sm text-light">نام ایده پرداز:</span>
نام ایده پرداز:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.full_name || "-"}</span>
{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 className="text-white text-sm text-light">شماره پرسنلی:</span>
شماره پرسنلی:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}</span>
{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 className="text-white text-sm text-light">مدیریت:</span>
مدیریت:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.management || "مدیریت توسعه"}</span>
{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 className="text-white text-sm text-light">معاونت:</span>
معاونت:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.deputy || "توسعه"}</span>
{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 className="text-white text-sm text-light">اعضای تیم:</span>
اعضای تیم:
</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>
@ -1342,47 +1213,30 @@ 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 className="text-white text-sm text-light">تاریخ ثبت ایده:</span>
تاریخ ثبت ایده:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{formatDate(selectedIdea.idea_registration_date) || "-"}</span>
{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 className="text-white text-sm text-light">نوع نوآوری:</span>
نوع نوآوری:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.innovation_type || "-"}</span>
{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 className="text-white text-sm text-light">اصالت ایده:</span>
اصالت ایده:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_originality || "-"}</span>
{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 className="text-white text-sm text-light min-w-max">محور ایده:</span>
محور ایده:
</span>
</div> </div>
<span className="text-white font-normal text-sm mr-10"> <span className="text-white font-normal text-sm mr-10">{selectedIdea.idea_axis || "-"}</span>
{selectedIdea.idea_axis || "-"}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -1395,38 +1249,33 @@ 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 className="text-white text-sm text-light">درآمد حاصل:</span>
درآمد حاصل:
</span>
</div> </div>
<span className="text-white text-sm font-normal mr-10"> <span className="text-white text-sm font-normal mr-10">{formatNumber(selectedIdea.increased_revenue) || "-"}
{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 className="text-white text-sm text-light">مقاله چاپ شده:</span>
مقاله چاپ شده:
</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 className="text-white text-sm text-light">پتنت ثبت شده:</span>
پتنت ثبت شده:
</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>
@ -1434,6 +1283,7 @@ 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">
@ -1441,7 +1291,9 @@ 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>
@ -1453,7 +1305,9 @@ 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>
@ -1465,7 +1319,9 @@ 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>
@ -1477,13 +1333,15 @@ 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>

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { import {
Building2, Building2,
ChevronDown, ChevronDown,
@ -36,8 +35,7 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatNumber } from "~/lib/utils"; import { 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 });
@ -119,18 +117,13 @@ 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>({
@ -203,13 +196,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);
@ -263,11 +256,7 @@ export function ProcessInnovationPage() {
"observer", "observer",
], ],
Sorts: [["start_date", "asc"]], Sorts: [["start_date", "asc"]],
Conditions: [ Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
["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 },
}); });
@ -279,16 +268,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);
} }
@ -296,14 +285,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);
} }
@ -311,7 +300,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);
} }
@ -320,7 +309,7 @@ export function ProcessInnovationPage() {
toast.error("خطا در دریافت اطلاعات پروژه‌ها"); toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) { if (reset) {
setProjects([]); setProjects([]);
// setTotalCount(0); setTotalCount(0);
} }
setHasMore(false); setHasMore(false);
} finally { } finally {
@ -336,22 +325,14 @@ 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, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects, date]); }, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -401,11 +382,7 @@ 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: [ Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
["type_of_innovation", "=", "نوآوری در فرآیند", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
}); });
if (response.state === 0) { if (response.state === 0) {
@ -439,8 +416,6 @@ export function ProcessInnovationPage() {
selectedProjects.size > 0 selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ") ? Array.from(selectedProjects).join(" , ")
: "", : "",
start_date: date?.start || null,
end_date: date?.end || null,
}, },
}); });

View File

@ -1,38 +1,46 @@
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import {
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 { 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, CardHeader, CardTitle } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
import { MetricCard } from "~/components/ui/metric-card"; import { MetricCard } from "~/components/ui/metric-card";
import { BaseCard } from "~/components/ui/base-card";
import { Checkbox } from "~/components/ui/checkbox";
import { Bar, BarChart, LabelList } from "recharts"
import { import {
Popover, Popover,
PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "~/components/ui/popover"; PopoverContent,
} from "~/components/ui/popover"
import jalaali from "jalaali-js"; import { FunnelChart } from "~/components/ui/funnel-chart";
import { import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
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 { FunnelChart } from "~/components/ui/funnel-chart"; import { Label } from "~/components/ui/label";
import { Skeleton } from "~/components/ui/skeleton";
import { import {
Table, Table,
TableBody, TableBody,
@ -41,11 +49,12 @@ 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 { EventBus, formatNumber, handleDataValue } from "~/lib/utils"; import { 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;
@ -130,16 +139,15 @@ 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 const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine)
?.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 */}
@ -152,17 +160,12 @@ 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 <div key={stage} className="flex-1 flex flex-col items-center relative">
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 index <= currentStage ? "bg-[#3D7968] text-white" : "bg-[#3AEA83] text-slate-600"
? "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>
@ -173,33 +176,25 @@ export default function Timeline(valueTimeLine: string) {
))} ))}
{/* Vertical line showing current position */} {/* Vertical line showing current position */}
{valueTimeLine?.length > 0 && ( { valueTimeLine?.length > 0 && ( <> <div
<>
{" "}
<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={{ style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
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={{ style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
left: `${(currentStage + 0.5) * (100 / stages.length)}%`, >وضعیت فعلی</div>
}} </> ) }
>
وضعیت فعلی
</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);
@ -246,7 +241,7 @@ export function ProductInnovationPage() {
description: "میلیون ریال", description: "میلیون ریال",
descriptionPercent: "درصد به کل درآمد", descriptionPercent: "درصد به کل درآمد",
color: "text-[#3AEA83]", color: "text-[#3AEA83]",
percent: 0, percent :0
}, },
newProductExports: { newProductExports: {
id: "newProductExports", id: "newProductExports",
@ -264,53 +259,45 @@ 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, date?.start, date?.end); await fetchPopupData(project);
}; };
const fetchPopupData = async ( const fetchPopupData = async (project: ProductInnovationData) => {
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 ( if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) {
statsData.innovation_product_popup_function1 && setPopupStats(JSON.parse(statsData.innovation_product_popup_function1)[0]);
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: ["product_title", "full_season", "sum(export_revenue)"], OutputFields: [
GroupBy: ["product_title", "full_season"], "product_title",
"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);
@ -318,13 +305,14 @@ 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( const filteredData = chartData.filter(item =>
(item) => item.product_title === project?.title 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 {
@ -370,14 +358,10 @@ 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: [ Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]],
["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 },
}); });
@ -443,12 +427,8 @@ 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);
@ -464,25 +444,21 @@ export function ProductInnovationPage() {
return 0; return 0;
}; };
const data: Array<any> = JSON.parse(payload?.innovation_product_function); const data: Array<any> = JSON.parse(
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( new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent),
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( successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel),
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( count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects),
stats?.count_innovation_construction_inside_projects
),
average_project_score: parseNum(stats?.average_project_score), average_project_score: parseNum(stats?.average_project_score),
}; };
@ -511,21 +487,13 @@ export function ProductInnovationPage() {
} }
}; };
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
}, [sortConfig, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [date]); }, []);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -570,42 +538,39 @@ export function ProductInnovationPage() {
setHasMore(true); setHasMore(true);
}; };
// const formatCurrency = (amount: string | number) => {
// if (!amount) return "0 ریال"; const formatCurrency = (amount: string | number) => {
// const numericAmount = if (!amount) return "0 ریال";
// typeof amount === "string" const numericAmount =
// ? parseFloat(amount.replace(/,/g, "")) typeof amount === "string"
// : amount; ? parseFloat(amount.replace(/,/g, ""))
// if (isNaN(numericAmount)) return "0 ریال"; : amount;
// return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; if (isNaN(numericAmount)) return "0 ریال";
// }; 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( const productData = data.find(item =>
(item) =>
item.product_title === product && item.full_season === season item.product_title === product && item.full_season === season
); );
seasonData[product] = seasonData[product] = productData?.export_revenue_sum > 0 && productData ? Math.round(productData?.export_revenue_sum) : 0;
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;
@ -650,8 +615,7 @@ 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"
> >
جزئیات بیشتر جزئیات بیشتر
@ -664,9 +628,7 @@ export function ProductInnovationPage() {
</Badge> </Badge>
); );
case "title": case "title":
return ( return <span className="font-light text-sm text-white">{String(value)}</span>;
<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">
@ -690,11 +652,7 @@ export function ProductInnovationPage() {
</Badge> </Badge>
); );
default: default:
return ( return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
<span className="text-white text-sm font-light">
{String(value) || "-"}
</span>
);
} }
}; };
@ -709,8 +667,7 @@ export function ProductInnovationPage() {
}) })
.map((item) => ({ .map((item) => ({
label: item.full_season, label: item.full_season,
value: value: item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum) ,
item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum),
})); }));
return ( return (
@ -762,27 +719,18 @@ 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={ percentLabel={stateCard.revenueNewProducts.descriptionPercent}
stateCard.revenueNewProducts.descriptionPercent
}
/> />
</div> </div>
{/* Second card */} {/* Second card */}
<div> <div>
<BaseCard <BaseCard title={stateCard.newProductExports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
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"> <p className="text-3xl font-bold mb-1 text-pr-green">{stateCard.newProductExports.value}</p>
{stateCard.newProductExports.value} <div className="text-xs text-gray-400 font-persian">{stateCard.newProductExports.description}</div>
</p>
<div className="text-xs text-gray-400 font-persian">
{stateCard.newProductExports.description}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -791,19 +739,12 @@ export function ProductInnovationPage() {
{/* Third card - basic BaseCard */} {/* Third card - basic BaseCard */}
<div> <div>
<BaseCard <BaseCard title={stateCard.impactOnImports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
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"> <p className="text-3xl font-bold mb-1 text-pr-red">{stateCard.impactOnImports.value}</p>
{stateCard.impactOnImports.value} <div className="text-xs text-gray-400 font-persian">{stateCard.impactOnImports.description}</div>
</p>
<div className="text-xs text-gray-400 font-persian">
{stateCard.impactOnImports.description}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -961,10 +902,7 @@ 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>
@ -979,9 +917,7 @@ 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?.( ((stats.average_project_score ?? 0) as number).toFixed?.(1) ?? 0
1
) ?? 0
)} )}
</div> </div>
</div> </div>
@ -1005,47 +941,29 @@ 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"> <h3 className="font-bold text-base">{selectedProjectDetails?.title}</h3>
{selectedProjectDetails?.title} <p className="py-2">{selectedProjectDetails?.project_description}</p>
</h3>
<p className="py-2">
{selectedProjectDetails?.project_description}
</p>
</div> </div>
<Timeline <Timeline valueTimeLine={selectedProjectDetails?.current_status} />
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 className="text-sm text-white font-semibold mb-2">دانش فنی محصول جدید</h3>
دانش فنی محصول جدید
</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 className="text-sm text-white font-light">توسعه درونزا</span>
توسعه درونزا
</span>
<Checkbox <Checkbox
checked={ checked={selectedProjectDetails?.developed_technology_type === "توسعه درونزا"}
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 className="text-sm text-white font-light">همکاری فناورانه</span>
همکاری فناورانه
</span>
<Checkbox <Checkbox
checked={ checked={selectedProjectDetails?.developed_technology_type === "همکاری فناوری"}
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>
@ -1053,14 +971,11 @@ 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={ checked={selectedProjectDetails?.developed_technology_type === "سایر"}
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 */}
@ -1069,20 +984,15 @@ export function ProductInnovationPage() {
استانداردهای ملی و بینالمللی اخذ شده استانداردهای ملی و بینالمللی اخذ شده
</h3> </h3>
{selectedProjectDetails?.obtained_standard_title && {selectedProjectDetails?.obtained_standard_title && selectedProjectDetails?.obtained_standard_title.length > 0 ? (
selectedProjectDetails?.obtained_standard_title.length > 0 ? (
<div className="space-y-2"> <div className="space-y-2">
{(Array.isArray( {(Array.isArray(selectedProjectDetails?.obtained_standard_title)
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"> <span className="text-sm text-white font-light">{standard}</span>
{standard}
</span>
</div> </div>
))} ))}
</div> </div>
@ -1091,12 +1001,11 @@ 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">
گواهی دانشبنیان ندارد گواهی دانشبنیان ندارد
@ -1127,14 +1036,10 @@ 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( {handleDataValue(selectedProjectDetails?.certificate_obtain_date) || "—"}
selectedProjectDetails?.certificate_obtain_date
) || "—"}
</p> </p>
<p className="text-sm text-white"> <p className="text-sm text-white">
<span className="font-bold"> <span className="font-bold">مرجع صادرکننده: </span>
مرجع صادرکننده:{" "}
</span>
{selectedProjectDetails?.issuing_authority || "—"} {selectedProjectDetails?.issuing_authority || "—"}
</p> </p>
</div> </div>
@ -1176,32 +1081,16 @@ 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( value={Math.round(popupStats?.new_products_export > 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)}
? 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( value={Math.round(popupStats?.import_impact > 0 ? popupStats?.import_impact : 0)}
popupStats?.import_impact > 0 percentValue={Math.round(popupStats?.import_impact_percent > 0 ? popupStats?.import_impact_percent : 0)}
? popupStats?.import_impact
: 0
)}
percentValue={Math.round(
popupStats?.import_impact_percent > 0
? popupStats?.import_impact_percent
: 0
)}
valueLabel="میلیون ریال" valueLabel="میلیون ریال"
percentLabel="درصد صرفه جویی" percentLabel="درصد صرفه جویی"
/> />
@ -1209,9 +1098,7 @@ 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 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
ظرفیت صادر شده
</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%">
@ -1229,149 +1116,52 @@ export function ProductInnovationPage() {
axisLine={false} axisLine={false}
stroke="#C3C3C3" stroke="#C3C3C3"
tickMargin={8} tickMargin={8}
tickFormatter={(value: string) => tickFormatter={(value: string) => `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll('٬','')}`}
`${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll("٬", "")}`
}
fontSize={11} fontSize={11}
/> />
<YAxis <YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value: number) => `${formatNumber(value)} میلیون`} />
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 <LabelList formatter={(value: number) => `${formatNumber(value)}`} position="top" offset={15} fill="F9FAFB" className="fill-foreground" fontSize={16} />
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 className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
دادهای برای نمایش وجود ندارد
</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 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
ظرفیت صادر شده
</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 <LineChart className="aspect-auto w-full" data={transformDataForLineChart(allExportData)} margin={{ top: 20, right: 30, left: 10, bottom: 50 }}>
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 <XAxis dataKey="season" stroke="#9CA3AF" fontSize={11} tick={({ x, y, payload }) => (
dataKey="season"
stroke="#9CA3AF"
fontSize={11}
tick={({ x, y, payload }) => (
<g transform={`translate(${x},${y + 10})`}> <g transform={`translate(${x},${y + 10})`}>
<text <text x={-40} y={15} dy={0} textAnchor="end" fill="#9CA3AF" fontSize={11} transform="rotate(-45)">{(payload as any).value}</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)} میلیون`} />
<YAxis <Tooltip formatter={(value: number) => `${formatNumber(value)} میلیون`} contentStyle={{ backgroundColor: "#1F2937", border: "1px solid #374151", borderRadius: "6px", padding: "6px 10px", fontSize: "11px", color: "#F9FAFB" }} />
tickLine={false} <Legend layout="vertical" verticalAlign="middle" align="right" iconType={"plainline"} className="!flex" wrapperStyle={{ fontSize: 11, paddingLeft: 12, gap: 10 }} />
axisLine={false} {[...new Set(allExportData.map((item) => item.product_title))].slice(0, 5).map((product, index) => {
stroke="#9CA3AF" const colors = ["#10B981", "#EF4444", "#3B82F6", "#F59E0B", "#8B5CF6"];
fontSize={11} return <Line key={product} type="linear" dot={false} activeDot={{ r: 5 }} dataKey={product} stroke={colors[index % colors.length]} strokeWidth={2} />;
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 className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
دادهای برای نمایش وجود ندارد
</div>
)} )}
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,6 +1,5 @@
import jalaali from "jalaali-js";
import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState, useMemo } 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";
@ -14,8 +13,8 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import apiService from "~/lib/api"; import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type"; import { formatNumber } from "~/lib/utils";
import { DashboardLayout } from "../layout"; import { DashboardLayout } from "../layout";
interface ProjectData { interface ProjectData {
@ -154,7 +153,6 @@ 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);
@ -171,10 +169,6 @@ 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
@ -206,10 +200,7 @@ 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) {
@ -274,13 +265,6 @@ 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);
@ -290,7 +274,7 @@ export function ProjectManagementPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig, date]); }, [sortConfig]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -303,8 +287,7 @@ export function ProjectManagementPage() {
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
const handleScroll = () => { const handleScroll = () => {
if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return;
return;
// Clear previous timeout // Clear previous timeout
if (scrollTimeoutRef.current) { if (scrollTimeoutRef.current) {
@ -324,9 +307,7 @@ export function ProjectManagementPage() {
}; };
if (scrollContainer) { if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll, { scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
passive: true,
});
} }
return () => { return () => {
@ -356,10 +337,7 @@ 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) {
@ -380,14 +358,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...
@ -652,7 +630,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;
@ -666,7 +644,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;
}) })
@ -792,10 +770,7 @@ 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 <div ref={scrollContainerRef} className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]">
ref={scrollContainerRef}
className="relative overflow-auto custom-scrollbar max-h-[calc(100vh-120px)]"
>
<Table className="table-fixed"> <Table className="table-fixed">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]"> <TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]"> <TableRow className="bg-[#3F415A]">

View File

@ -1,4 +1,3 @@
import jalaali from "jalaali-js";
import { useEffect, useReducer, useRef, useState } from "react"; import { useEffect, useReducer, useRef, useState } from "react";
import { import {
Bar, Bar,
@ -13,8 +12,7 @@ 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 { EventBus, formatNumber } from "~/lib/utils"; import { 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,
@ -118,7 +116,6 @@ 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);
@ -128,35 +125,22 @@ 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: ["strategic_theme", "count(operational_fee)"], OutputFields: [
GroupBy: ["strategic_theme"], "strategic_theme",
Conditions: [ "count(operational_fee)",
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
], ],
GroupBy: ["strategic_theme"],
}); });
const responseData = const responseData =
@ -186,11 +170,7 @@ export function StrategicAlignmentPopup({
"value_technology_and_innovation", "value_technology_and_innovation",
"count(operational_fee)", "count(operational_fee)",
], ],
Conditions: [ Conditions: [["strategic_theme", "=", item]],
["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"],
}); });
@ -267,9 +247,7 @@ export function StrategicAlignmentPopup({
(item: StrategicAlignmentData) => ({ (item: StrategicAlignmentData) => ({
...item, ...item,
percentage: percentage:
total > 0 total > 0 ? Math.round((item.operational_fee_count / total) * 100) : 0,
? Math.round((item.operational_fee_count / total) * 100)
: 0,
}) })
); );
setData(dataWithPercentage || []); setData(dataWithPercentage || []);

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { import {
Area, Area,
AreaChart, AreaChart,
@ -10,11 +11,9 @@ 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 { EventBus, formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
export interface CompanyDetails { export interface CompanyDetails {
id: string; id: string;
@ -63,44 +62,27 @@ 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) {
@ -109,6 +91,8 @@ 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 => {
@ -119,7 +103,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 [];
@ -137,7 +121,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++) {
@ -478,13 +462,7 @@ 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 <linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
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>
@ -523,14 +501,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
activeDot={({ cx, cy, payload }) => ( activeDot={({ cx, cy, payload }) => (
<g> <g>
{/* Small circle */} {/* Small circle */}
<circle <circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
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}
@ -546,7 +517,8 @@ 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">
دادهای برای نمایش وجود ندارد دادهای برای نمایش وجود ندارد
@ -554,6 +526,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );

View File

@ -1,9 +1,7 @@
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as d3 from "d3"; import * as d3 from "d3";
import { useCallback, useEffect, useRef, useState } from "react";
import { EventBus } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { useAuth } from "../../contexts/auth-context";
import apiService from "../../lib/api"; import apiService from "../../lib/api";
import { useAuth } from "../../contexts/auth-context";
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";
@ -61,10 +59,7 @@ function isBrowser(): boolean {
return typeof window !== "undefined"; return typeof window !== "undefined";
} }
export function NetworkGraph({ export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps) {
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[]>([]);
@ -73,15 +68,6 @@ export function NetworkGraph({
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);
@ -94,21 +80,16 @@ export function NetworkGraph({
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( const callAPI = useCallback(async (stage_id: number) => {
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;
@ -127,7 +108,7 @@ export function NetworkGraph({
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] || {}),
); );
// نود مرکزی // نود مرکزی
@ -140,9 +121,7 @@ export function NetworkGraph({
}; };
// دسته‌بندی‌ها // دسته‌بندی‌ها
const categories = Array.from( const categories = Array.from(new Set(data.map((item: any) => item.category)));
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}`,
@ -191,8 +170,7 @@ export function NetworkGraph({
}, [isMounted, token, getImageUrl]); }, [isMounted, token, getImageUrl]);
useEffect(() => { useEffect(() => {
if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) if (!isMounted || !svgRef.current || isLoading || nodes.length === 0) return;
return;
const svg = d3.select(svgRef.current); const svg = d3.select(svgRef.current);
const width = svgRef.current.clientWidth; const width = svgRef.current.clientWidth;
@ -247,18 +225,12 @@ export function NetworkGraph({
.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( .force("radial", d3.forceRadial(d => d.isCenter ? 0 : 300, width/2, height/2))
"radial", .force("collision", d3.forceCollide().radius((d) => (d.isCenter ? 50 : 35)));
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;
@ -270,12 +242,12 @@ export function NetworkGraph({
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;
@ -298,20 +270,22 @@ export function NetworkGraph({
// نودهای نهایی **هیچ fx/fy نداشته باشند** // نودهای نهایی **هیچ fx/fy نداشته باشند**
// فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد // فقط forceLink آن‌ها را به دسته‌ها متصل نگه می‌دارد
// const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// categoryNodes.forEach((catNode) => { // const finalNodes = nodes.filter(n => !n.isCenter && n.stageid !== -1);
// const childNodes = finalNodes.filter(n => n.category === catNode.category);
// const childCount = childNodes.length; // categoryNodes.forEach((catNode) => {
// const radius = 100; // فاصله از دسته // const childNodes = finalNodes.filter(n => n.category === catNode.category);
// const angleStep = (2 * Math.PI) / childCount; // const childCount = childNodes.length;
// const radius = 100; // فاصله از دسته
// const angleStep = (2 * Math.PI) / childCount;
// childNodes.forEach((node, i) => {
// const angle = i * angleStep;
// node.fx = catNode.fx! + radius * Math.cos(angle);
// node.fy = catNode.fy! + radius * Math.sin(angle);
// });
// });
// 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
@ -331,7 +305,7 @@ export function NetworkGraph({
.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>()
@ -463,6 +437,7 @@ export function NetworkGraph({
.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();
@ -492,15 +467,15 @@ export function NetworkGraph({
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 = {
@ -617,4 +592,5 @@ export function NetworkGraph({
); );
} }
export default NetworkGraph; export default NetworkGraph;

View File

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

View File

@ -1,7 +1,6 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import EventEmitter from "events";
import moment from "moment-jalaali";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import moment from "moment-jalaali";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -23,6 +22,8 @@ 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 آرایه از مقادیر دادهها
@ -116,7 +117,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" &&
@ -131,6 +132,4 @@ export const handleDataValue = (val: any): any => {
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();

View File

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