fix: change date picker logic to another pages

This commit is contained in:
MehrdadAdabi 2025-10-12 21:30:13 +03:30
parent bda2e62411
commit efa46a02c2
14 changed files with 1383 additions and 999 deletions

View File

@ -1,3 +1,4 @@
import jalaali from "jalaali-js";
import { Book, CheckCircle } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
@ -24,6 +25,7 @@ import { InteractiveBarChart } from "./interactive-bar-chart";
import { DashboardLayout } from "./layout";
export function DashboardHome() {
const { jy } = jalaali.toJalaali(new Date());
const [dashboardData, setDashboardData] = useState<any | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -39,20 +41,23 @@ export function DashboardHome() {
revenueI: number;
}[]
>([]);
// const [totalIncreasedCapacity, setTotalIncreasedCapacity] =
// useState<number>(0);
useEffect(() => {
fetchDashboardData();
}, []);
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) fetchDashboardData(date.start, date.end);
if (date) setDate(date);
});
}, []);
const fetchDashboardData = async (startDate?: string, endDate?: string) => {
useEffect(() => {
fetchDashboardData();
}, [date]);
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
@ -66,16 +71,16 @@ export function DashboardHome() {
// Fetch top cards data
const topCardsResponse = await apiService.call({
main_page_first_function: {
start_date: startDate || null,
end_date: endDate || null,
start_date: date.start || null,
end_date: date.end || null,
},
});
// Fetch left section data
const leftCardsResponse = await apiService.call({
main_page_second_function: {
start_date: startDate || null,
end_date: endDate || null,
start_date: date.start || null,
end_date: date.end || null,
},
});
@ -109,7 +114,10 @@ export function DashboardHome() {
"sum(pre_project_income)",
"sum(increased_income_after_innovation)",
],
Conditions: [["start_date", ">=", startDate || null, "and"],["start_date", "<=", endDate || null]],
Conditions: [
["start_date", ">=", date.start || null, "and"],
["start_date", "<=", date.end || null],
],
GroupBy: ["related_company"],
};
@ -183,22 +191,22 @@ export function DashboardHome() {
};
// RadialBarChart data for ideas visualization
const getIdeasChartData = () => {
if (!dashboardData?.topData)
return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
// const getIdeasChartData = () => {
// if (!dashboardData?.topData)
// return [{ browser: "safari", visitors: 0, fill: "var(--color-safari)" }];
const registered = parseFloat(
dashboardData.topData.registered_innovation_technology_idea || "0"
);
const ongoing = parseFloat(
dashboardData.topData.ongoing_innovation_technology_ideas || "0"
);
const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
// const registered = parseFloat(
// dashboardData.topData.registered_innovation_technology_idea || "0"
// );
// const ongoing = parseFloat(
// dashboardData.topData.ongoing_innovation_technology_ideas || "0"
// );
// const percentage = registered > 0 ? (ongoing / registered) * 100 : 0;
return [
{ browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
];
};
// return [
// { browser: "safari", visitors: percentage, fill: "var(--color-safari)" },
// ];
// };
// const chartData = getIdeasChartData();

View File

@ -78,16 +78,22 @@ export function Header({
const { user } = useAuth();
const { jy } = jalaali.toJalaali(new Date());
const calendarRef = useRef<HTMLDivElement>(null);
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState<boolean>(false);
const [isNotificationOpen, setIsNotificationOpen] = useState<boolean>(false);
const [selectedDate, setSelectedDate] = useState<CurrentDay>();
const [openCalendar, setOpenCalendar] = useState<boolean>(false);
const calendarRef = useRef<HTMLDivElement>(null);
const [currentYear, setCurrentYear] = useState<SelectedDate>({
since: jy,
until: jy,
});
const [selectedDate, setSelectedDate] = useState<CurrentDay>({
sinceMonth: "بهار",
fromMonth: "زمستان",
start: `${currentYear.since}/01/01`,
end: `${currentYear.until}/12/30`,
});
const redirectHandler = async () => {
try {
const getData = await apiService.post("/GenerateSsoCode");
@ -100,56 +106,74 @@ export function Header({
const nextFromYearHandler = () => {
if (currentYear && (currentYear.since ?? 0) < (currentYear.until ?? 0)) {
setCurrentYear((prev) => ({
...prev,
since: currentYear?.since! + 1,
}));
const data = {
...currentYear,
since: currentYear.since! + 1,
};
setCurrentYear(data);
EventBus.emit("dateSelected", {
...selectedDate,
start: `${data.since}/${selectedDate.start?.split("/").slice(1).join("/")}`,
});
}
};
const prevFromYearHandler = () => {
setCurrentYear((prev) => ({
...prev,
since: currentYear?.since! - 1,
}));
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((prev) => ({
...prev,
...data,
}));
setSelectedDate(data);
EventBus.emit("dateSelected", data);
};
const nextUntilYearHandler = () => {
setCurrentYear((prev) => ({
...prev,
until: currentYear?.until! + 1,
}));
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)) {
setCurrentYear((prev) => ({
...prev,
until: currentYear?.until! - 1,
}));
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((prev) => ({
...prev,
...data,
}));
setSelectedDate(data);
EventBus.emit("dateSelected", data);
toggleCalendar();
};
@ -172,10 +196,6 @@ export function Header({
};
}, []);
useEffect(() => {
EventBus.emit("dateSelected", selectedDate);
}, [currentYear, selectedDate]);
return (
<header
className={cn(
@ -223,11 +243,11 @@ export function Header({
<div ref={calendarRef} className="flex flex-col gap-3 relative">
<div
onClick={toggleCalendar}
className="flex flex-row gap-2 items-center border border-pr-gray p-1.5 rounded-md px-2.5 min-w-72 cursor-pointer hover:bg-pr-gray/50 transition-all duration-300"
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 gap-2.5 min-w-64 font-bold">
<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>
@ -245,7 +265,7 @@ export function Header({
</div>
{openCalendar && (
<div className="flex flex-row gap-1 absolute top-14 w-full rounded-3xl overflow-hidden bg-pr-gray border-2 border-[#5F6284]">
<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}
@ -255,7 +275,7 @@ export function Header({
selectedDate={selectedDate?.sinceMonth}
selectDateHandler={selectFromDateHandler}
/>
<span className="w-0.5 h-52 border border-[#5F6284] block mt-3"></span>
<span className="w-0.5 h-[12.5rem] border border-[#5F6284] block "></span>
<CustomCalendar
title="تا"
nextYearHandler={nextUntilYearHandler}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import {
PopoverTrigger,
} from "~/components/ui/popover";
import jalaali from "jalaali-js";
import {
CartesianGrid,
Legend,
@ -42,7 +43,8 @@ import {
} from "~/components/ui/table";
import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip";
import apiService from "~/lib/api";
import { formatNumber, handleDataValue } from "~/lib/utils";
import { EventBus, formatNumber, handleDataValue } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout";
interface ProjectData {
@ -196,7 +198,8 @@ export default function Timeline(valueTimeLine: string) {
}
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 [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
@ -261,17 +264,24 @@ export function ProductInnovationPage() {
},
});
const [date, setDate] = useState<CalendarDate>({
start: `${jy}/01/01`,
end: `${jy}/12/30`,
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const handleProjectDetails = async (project: ProductInnovationData) => {
setSelectedProjectDetails(project);
console.log(project);
setDetailsDialogOpen(true);
await fetchPopupData(project);
await fetchPopupData(project, date?.start, date?.end);
};
const fetchPopupData = async (project: ProductInnovationData) => {
const fetchPopupData = async (
project: ProductInnovationData,
startDate?: string,
endDate?: string
) => {
try {
setPopupLoading(true);
@ -279,6 +289,8 @@ export function ProductInnovationPage() {
const statsResponse = await apiService.call({
innovation_product_popup_function1: {
project_id: project.project_id,
start_date: startDate || null,
end_date: endDate || null,
},
});
@ -361,7 +373,11 @@ export function ProductInnovationPage() {
"issuing_authority",
],
Sorts: [["start_date", "asc"]],
Conditions: [["type_of_innovation", "=", "نوآوری در محصول"]],
Conditions: [
["type_of_innovation", "=", "نوآوری در محصول", "and"],
["start_date", ">=", date?.start || null, "and"],
["start_date", "<=", date?.end || null],
],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
@ -424,14 +440,14 @@ export function ProductInnovationPage() {
}
};
const fetchStats = async (startDate?: string, endDate?: string) => {
const fetchStats = async () => {
try {
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_product_function: {
start_date: startDate,
end_date: endDate,
start_date: date?.start || null,
end_date: date?.end || null,
},
});
@ -495,13 +511,21 @@ export function ProductInnovationPage() {
}
};
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => {
fetchProjects(true);
}, [sortConfig]);
}, [sortConfig, date]);
useEffect(() => {
fetchStats();
}, []);
}, [date]);
useEffect(() => {
if (currentPage > 1) {
@ -546,15 +570,15 @@ export function ProductInnovationPage() {
setHasMore(true);
};
const formatCurrency = (amount: string | number) => {
if (!amount) return "0 ریال";
const numericAmount =
typeof amount === "string"
? parseFloat(amount.replace(/,/g, ""))
: amount;
if (isNaN(numericAmount)) return "0 ریال";
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
};
// const formatCurrency = (amount: string | number) => {
// if (!amount) return "0 ریال";
// const numericAmount =
// typeof amount === "string"
// ? parseFloat(amount.replace(/,/g, ""))
// : amount;
// if (isNaN(numericAmount)) return "0 ریال";
// return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
// };
// Transform data for line chart
const transformDataForLineChart = (data: any[]) => {
@ -576,12 +600,12 @@ export function ProductInnovationPage() {
});
};
const getRatingColor = (rating: string | number) => {
const numRating = typeof rating === "string" ? parseInt(rating) : rating;
if (numRating >= 150) return "text-emerald-400";
if (numRating >= 100) return "text-blue-400";
return "text-red-400";
};
// const getRatingColor = (rating: string | number) => {
// const numRating = typeof rating === "string" ? parseInt(rating) : rating;
// if (numRating >= 150) return "text-emerald-400";
// if (numRating >= 100) return "text-blue-400";
// return "text-red-400";
// };
const statusColor = (status: projectStatus): any => {
let el = null;

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export const Calendar: React.FC<CalendarProps> = ({
selectDateHandler,
}) => {
return (
<div className="filter-box bg-pr-gray p-3 w-full">
<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">

View File

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