inogen/app/components/dashboard/project-management/innovation-built-inside-page.tsx
2025-10-14 16:39:29 +03:30

1380 lines
51 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ...existing code...
import { useCallback, useEffect, useRef, useState } from "react";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import {
ChevronDown,
ChevronUp,
CodeXml,
FilterIcon,
Handshake,
RefreshCw,
SquareUser,
User,
Users,
} from "lucide-react";
import toast from "react-hot-toast";
import {
Customized,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
XAxis,
} from "recharts";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatCurrency, formatNumber } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import DashboardLayout from "../layout";
interface innovationBuiltInDate {
WorkflowID: number;
approved_budget: string;
done_date: string | null;
observer: string;
project_description: string;
project_id: number | string;
project_no: string;
project_rating: string;
project_status: string;
start_date: string;
title: string;
}
interface DialogInfo {
WorkflowID: number;
collaboration_model: string;
complexity_level: string;
developer_team_role: string;
number_employees_involved: number | null;
participants_full_name: string;
project_description: string;
project_id: string;
project_no: string;
project_rating: string;
project_status: string;
role_company_staff: string | null;
technology_maturity_level: string;
title: string;
technology_params?: Array<TechnologyParameter>;
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
}
interface StateItem {
id: string;
title: string;
percent: {
value: number;
description: string;
};
total: {
value: number;
description: string;
};
}
interface BottleNeckItem {
resolveBottleNeck: {
label: string;
value: string;
description?: string;
};
increaseCapacity: {
label: string;
value: string;
description?: string;
unit?: string;
increasePercent: number;
};
increaseIncome: {
label: string;
value: string;
description?: string;
increasePercent: number;
unit?: string;
};
}
interface StatsCard {
currencySaving: StateItem;
investmentAmount: StateItem;
}
interface InnovationStats {
average_project_score: number | null;
count_innovation_construction_inside_projects: number;
foreign_currency_saving: number;
foreign_currency_saving_percent: number;
high_level_technology_count: number;
increased_capacity_after_innovation: number;
increased_capacity_after_innovation_percent: number;
increased_income_after_innovation: number;
increased_income_after_innovation_percent: number;
investment_amount: number;
investment_amount_percent: number;
resolved_bottleneck_count: number;
}
interface TechnologyParameter {
WorkflowID: number;
domestic_technology_parameter_value: string;
foreign_technology_parameter_value: string;
technology_parameter_title: string;
}
enum projectStatus {
propozal = "پروپوزال",
contract = "پیشنویس قرارداد",
inprogress = "در حال انجام",
stop = "متوقف شده",
mafasa = "مرحله مفاصا",
finish = "پایان یافته",
}
const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "120px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "300px" },
{
key: "project_status",
label: "وضعیت پروژه",
sortable: true,
width: "140px",
},
{
key: "project_rating",
label: "امتیاز پروژه",
sortable: true,
width: "120px",
},
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
];
const dialogChartData = [
{ name: "مرحه پیدایش", value: 10 },
{ name: "مرحله رشد", value: 14 },
{ name: "مرحله بلوغ", value: 25 },
{ name: "مرحله افول", value: 15 },
];
export function InnovationBuiltInsidePage() {
const [projects, setProjects] = useState<innovationBuiltInDate[]>([]);
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 [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
// const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
direction: "asc",
});
const [date, setDate] = useStoredDate();
const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] =
useState<Set<string | number>>();
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<DialogInfo>();
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
currencySaving: {
id: "reduce-pollution",
title: "صرفه جویی ارزی",
total: {
value: 10.45,
description: "میلیون ریال کاهش یافته",
},
percent: {
value: 10,
description: "درصد نسبت به کل",
},
},
investmentAmount: {
id: "reduce-junkfull",
title: "میزان سرمایه گذاری ",
total: {
value: 10,
description: "میلیون ریال",
},
percent: {
value: 10,
description: "درصد به کل",
},
},
});
const [bottleNeck, setBottleNeck] = useState<BottleNeckItem>({
resolveBottleNeck: {
label: "تعدادگلوگاه رفع شده",
value: "0",
description: "",
},
increaseCapacity: {
label: "ظرفیت تولید اضافه شده",
value: "0",
description: "درصد افزایش ظرفیت تولید",
increasePercent: 0,
unit: "تن",
},
increaseIncome: {
label: "میزان افزایش درآمد",
value: "0",
description: "درصد افزایش درآمد",
increasePercent: 0,
unit: "میلیون ریال",
},
});
const [showDialogItems, setShowDialogItems] = useState<boolean>(false);
const [countOfHighTech, setCountOfHighTech] = useState(0);
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const handleSelectProject = (projectNo: string | number) => {
const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) {
newSelected.delete(projectNo);
} else {
newSelected.add(projectNo);
}
setSelectedProjects(newSelected);
};
const handleProjectDetails = async (project: DialogInfo) => {
setShowDialogItems(true);
setDetailsDialogOpen(true);
setSelectedProjectDetails(project);
await fetchDialogTbl(project.WorkflowID);
setTimeout(() => {
setShowDialogItems(false);
calculateProgressBar(+project.project_rating);
}, 500);
};
// ...existing code...
const fetchProjects = async (reset = false) => {
if (fetchingRef.current) {
return;
}
try {
fetchingRef.current = true;
if (reset) {
setCurrentPage(1);
} else {
setLoadingMore(true);
}
const pageToFetch = reset ? 1 : currentPage;
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"project_id",
"project_no",
"title",
"project_status",
"project_rating",
"project_description",
"complexity_level",
"collaboration_model",
"developer_team_role",
"role_company_staff",
"number_employees_involved",
"participants_full_name",
"technology_maturity_level",
],
Sorts: [[sortConfig.field, sortConfig.direction]],
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) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData)) {
if (reset) {
setProjects(parsedData);
setTotalCount(parsedData.length);
} else {
setProjects((prev) => [...prev, ...parsedData]);
setTotalCount((prev) => prev + parsedData.length);
}
setHasMore(parsedData.length === pageSize);
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (parseError) {
console.error("Error parsing project data:", parseError);
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} else {
toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
}
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
if (reset) {
setProjects([]);
setTotalCount(0);
}
setHasMore(false);
} finally {
setLoading(false);
setLoadingMore(false);
fetchingRef.current = false;
}
};
const fetchDialogTbl = async (tblId: number) => {
try {
const response = await apiService.select({
ProcessName: "technology_parameter",
OutputFields: [
"technology_parameter_title",
"domestic_technology_parameter_value",
"foreign_technology_parameter_value",
],
Conditions: [["project_id", "=", tblId]],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
const parsedData = JSON.parse(dataString);
setSelectedProjectDetails((prev: any) => ({
...prev,
technology_params: parsedData,
}));
}
}
} catch (error) {
console.error("Error fetching projects:", error);
toast.error("خطا در دریافت اطلاعات پروژه‌ها");
}
};
const calculateProgressBar = (val: number) => {
const pointer = document.getElementsByClassName(
"progressBarPointer"
)[0] as HTMLElement;
if (!pointer) return;
const leftValue = val !== 0 ? `calc(${val}% - 100px)` : `calc(0% - 40px)`;
pointer.style.left = leftValue;
};
const loadMore = useCallback(() => {
if (hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
useEffect(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => {
if (date.start && date.end) fetchProjects(true);
}, [sortConfig, date]);
useEffect(() => {
if (date.end && date.start) fetchStats();
}, [selectedProjects, date]);
useEffect(() => {
if (currentPage > 1) {
fetchProjects(false);
}
}, [currentPage]);
useEffect(() => {
const scrollContainer = document.querySelector(".overflow-auto");
const handleScroll = () => {
if (!scrollContainer || !hasMore) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
if (scrollPercentage == 1) {
loadMore();
}
};
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
}
return () => {
if (scrollContainer) {
scrollContainer.removeEventListener("scroll", handleScroll);
}
};
}, [loadMore, hasMore, loadingMore]);
useEffect(() => {
setLoading(true);
}, []);
const handleSort = (field: string) => {
fetchingRef.current = false;
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
setCurrentPage(1);
setProjects([]);
setHasMore(true);
};
const fetchStats = async () => {
try {
detailsDialogOpen;
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_construction_inside_function: {
project_ids:
selectedProjects && selectedProjects?.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
start_date: date?.start || null,
end_date: date?.end || null,
},
});
let payload: any = raw?.data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch {}
}
const parseNum = (v: unknown): any => {
if (v == null) return 0;
if (typeof v === "number") return v;
if (typeof v === "string") {
const cleaned = v.replace(/,/g, "").trim();
const n = parseFloat(cleaned);
return isNaN(n) ? 0 : n;
}
return 0;
};
const data: Array<InnovationStats> = JSON.parse(
payload?.innovation_construction_inside_function
);
const stats = data[0];
const normalized: any = {
currencySaving: {
value: formatNumber(parseNum(stats?.foreign_currency_saving)),
percent: formatNumber(
parseNum(stats?.foreign_currency_saving_percent)
),
},
investmentAmount: {
value: formatNumber(parseNum(stats?.investment_amount)),
percent: formatNumber(parseNum(stats?.investment_amount_percent)),
},
technology: {
value: formatNumber(parseNum(stats?.high_level_technology_count)),
},
income: {
value: formatNumber(
parseNum(stats.increased_income_after_innovation)
),
percent: formatNumber(
parseNum(stats.increased_income_after_innovation_percent)
),
},
capacity: {
value: formatNumber(
parseNum(stats.increased_capacity_after_innovation)
),
percent: formatNumber(
parseNum(stats.increased_capacity_after_innovation_percent)
),
},
resolveBottleNeck: {
value: formatNumber(parseNum(stats.resolved_bottleneck_count)),
},
countOfHighTech: formatNumber(stats.high_level_technology_count),
avarage: stats.average_project_score,
countInnovationGreenProjects:
stats.count_innovation_construction_inside_projects,
};
setActualTotalCount(normalized.countInnovationGreenProjects);
setTblAvarage(normalized.avarage);
setPageData(normalized);
} catch (error) {
console.error("Error fetching stats:", error);
} finally {
setStatsLoading(false);
}
};
const setPageData = (normalized: any) => {
setSustainabilityStats((prev) => ({
...prev,
currencySaving: {
...prev.currencySaving,
total: {
...prev.currencySaving.total,
value: normalized.currencySaving.value,
},
percent: {
...prev.currencySaving.percent,
value: normalized.currencySaving.percent,
},
},
investmentAmount: {
...prev.investmentAmount,
total: {
...prev.investmentAmount.total,
value: normalized.investmentAmount.value,
},
percent: {
...prev.investmentAmount.percent,
value: normalized.investmentAmount.percent,
},
},
}));
setBottleNeck((prev) => ({
...prev,
increaseIncome: {
...prev.increaseIncome,
value: normalized.income.value,
increasePercent: normalized.income.percent,
},
increaseCapacity: {
...prev.increaseCapacity,
value: normalized.capacity.value,
increasePercent: normalized.capacity.percent,
},
resolveBottleNeck: {
...prev.resolveBottleNeck,
value: normalized.resolveBottleNeck.value,
},
// average: normalized.avarage,
// countInnovationGreenProjects: normalized.countInnovationGreenProjects,
}));
setCountOfHighTech(normalized.countOfHighTech);
};
const renderCellContent = (item: innovationBuiltInDate, column: any) => {
const value = item[column.key as keyof innovationBuiltInDate];
switch (column.key) {
case "select":
return (
<Checkbox
checked={selectedProjects?.has(item?.project_id!)}
onCheckedChange={() => handleSelectProject(item?.project_id)}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer"
/>
);
case "details":
return (
<Button
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"
>
جزئیات بیشتر
</Button>
);
case "amount_currency_reduction":
return (
<span className="font-medium text-pr-green">
{formatCurrency(String(value))}
</span>
);
case "project_no":
return (
<Badge variant="outline" className="font-mono">
{String(value)}
</Badge>
);
case "title":
return (
<span className="font-light text-sm text-white">{String(value)}</span>
);
case "project_status":
return (
<div className="flex items-center gap-1">
<Badge
variant={statusColor(value)}
className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
style={{
border: "none",
}}
></Badge>
{String(value)}
</div>
);
case "project_rating":
return (
<Badge variant="outline" className="text-lg text-center border-none">
{formatNumber(String(value))}
</Badge>
);
case "reduce_prevention_production_stops":
case "throat_removal":
case "Reduce_rate_failure":
return (
<span className="font-medium text-blue-400">
{formatNumber(String(value))}
</span>
);
default:
return <span className="text-gray-300">{String(value) || "-"}</span>;
}
};
const statusColor = (status: projectStatus): any => {
let el = null;
switch (status) {
case projectStatus.contract:
el = "teal";
break;
case projectStatus.finish:
el = "info";
break;
case projectStatus.stop:
el = "warning";
break;
case projectStatus.inprogress:
el = "teal";
break;
case projectStatus.mafasa:
el = "destructive";
break;
case projectStatus.propozal:
el = "info";
}
return el;
};
return (
<DashboardLayout title="نوآوری ساخت داخل">
<div className="space-y-4 justify-between gap-8 grid pl-6 sm:grid-cols-1 xl:grid-cols-[35%_65%]">
{/* Stats Cards */}
<div className="flex w-full mb-0">
<div className="flex flex-col w-full justify-between gap-2">
{statsLoading
? // Loading skeleton for stats cards - matching new design
Array.from({ length: 2 }).map((_, index) => (
<Card
key={`skeleton-${index}`}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden"
>
<CardContent className="p-0 h-[11.5rem]">
<div className="flex flex-col gap-2 h-full">
<div className="border-b-2 border-gray-500/20 p-2.5">
<div
className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }}
/>
</div>
<div className="flex items-center justify-center flex-col p-2.5 mt-4">
<div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div>
</CardContent>
</Card>
))
: Object.entries(sustainabilityStats).map(([key, value]) => (
<Card
key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
>
<CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
<h3 className="text-lg font-semibold text-white p-4">
{value.title}
</h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-pr-green mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
{statsLoading ? (
<div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse">
<div className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5">
<div className="h-6 w-32 bg-gray-600/40 rounded-md"></div>
<div className="h-6 w-6 bg-gray-600/40 rounded-md"></div>
</div>
<div className="flex items-center justify-between p-6 px-14 flex-col gap-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="flex flex-row-reverse w-full justify-between"
>
<div className="flex flex-col text-left text-gray-400">
<div className="h-8 w-16 bg-gray-600/40 mb-1 rounded-md"></div>
<div className="h-4 w-12 bg-gray-600/40 rounded-md"></div>
</div>
<div className="text-sm flex flex-col gap-1">
<div className="h-5 w-20 bg-gray-600/40 rounded-md"></div>
<div className="h-4 w-16 bg-gray-600/40 rounded-md"></div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50">
<CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5">
<h3 className="text-lg font-bold text-white font-persian">
بر طرف کردن گلوگاه
</h3>
<FilterIcon />
</div>
<div className="flex items-center justify-between p-6 px-14 flex-col gap-4">
{Object.entries(bottleNeck).map(([key, value]) => {
return (
<div
key={`bottle-neck-${key}`}
className="flex flex-row-reverse w-full justify-between"
>
<div className="flex flex-col text-left text-gray-400">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
{value.value}
</span>
{value.unit && (
<span className="text-sm font-persian">
{value.unit}
</span>
)}
</div>
<div className="text-sm font-persian flex flex-col gap-1">
<span className="text-lg">{value.label}</span>
<div className="text-emerald-500 flex flex-row-reverse gap-1">
<span>
{value.description && value.description}
</span>
<span>{value.increasePercent}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</CardContent>
</Card>
)}
{statsLoading ? (
<div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse mt-4">
<div className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto">
<div className="h-6 w-32 bg-gray-600/40 rounded-md"></div>
<div className="h-8 w-16 bg-gray-600/40 rounded-md"></div>
</div>
</div>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border-gray-700/50">
<CardContent className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto">
<span className="text-lg">تعداد فناوری سطح بالا</span>
<span className="text-emerald-500 text-3xl font-bold font-persian">
{countOfHighTech}
</span>
</CardContent>
</Card>
)}
</div>
</div>
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden w-full h-max">
<CardContent className="p-0">
<div className="relative ">
<Table containerClassName="overflow-auto custom-scrollbar h-[calc(100vh-160px)]">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium sticky top-0 z-20 bg-[#3F415A] "
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2 cursor-pointer"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 40 }).map((_, index) => (
<TableRow
key={`skeleton-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div
className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }}
/>
</div>
</TableCell>
))}
</TableRow>
))
) : projects.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<span className="text-gray-400 font-persian">
هیچ پروژهای یافت نشد
</span>
</TableCell>
</TableRow>
) : (
projects.map((project, index) => (
<TableRow
key={`${project.project_no}-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className={`text-right whitespace-nowrap border-emerald-500/20 py-1 px-2 ${column.key === "select" ? "flex justify-center items-center" : ""}`}
>
{renderCellContent(project, column)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Infinite scroll trigger */}
<div ref={observerRef} className="h-auto">
{loadingMore && (
<div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-500" />
<span className="font-persian text-gray-300 text-xs"></span>
</div>
</div>
)}
</div>
</CardContent>
<div className="p-2 px-4 bg-gray-700/50">
<div className="grid grid-cols-[minmax(100px,1fr)_minmax(80px,1fr)_minmax(100px,2fr)_minmax(10px,1fr)_minmax(100px,1fr)] text-sm text-gray-300 font-persian items-center ">
<div></div>
<div className="text-right text-gray-400 mr-6">
کل پروژهها:{" "}
<span className="font-bold">
{formatNumber(actualTotalCount)}
</span>
</div>
<div className="flex flex-row-reverse">
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
<span className="block w-7 h-2.5 bg-purple-500 "></span>
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
</div>
<div className="text-center text-gray-400 py-2">
میانگین:{" "}
<span className="font-bold">
{formatNumber(
((tblAvarage ?? 0) as number).toFixed?.(1) ?? 0
)}
</span>
</div>
<div></div>
</div>
</div>
{/* <div className="p-2 px-4 bg-gray-700/50">
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
<div className="text-center items-center xl:w-full pr-6 sm:w-full">
<div className="text-base text-gray-401 mb-1">
کل پروژه ها :{formatNumber(actualTotalCount)}
</div>
</div>
<div className="flex items-center flex-row gap-5 status justify-start xl:w-full sm:w-full ">
<div className="flex flex-row-reverse">
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
<span className="block w-7 h-2.5 bg-purple-500 "></span>
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
</div>
<div className="flex justify-center items-center gap-2">
<div className="text-base text-gray-400 mb-1">میانگین :</div>
<div className="font-bold">
{formatNumber(
((tblAvarage ?? 0) as number).toFixed?.(1) ?? 0
)}
</div>
</div>
</div>
</div>
</div> */}
</Card>
</div>
{/* Project Details Dialog */}
<Dialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<DialogContent className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] max-w-5xl overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-4 font-persian text-right">
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="flex justify-between text-right px-4">
<div className="flex-[4] border-l-2 border-gray-600 pl-8">
{showDialogItems ? (
<div className="animate-pulse flex flex-col gap-2">
<div className="flex flex-col gap-2 mt-2">
<div className="h-4 w-full bg-gray-600 rounded"></div>
<div className="h-4 w-5/6 bg-gray-600 rounded"></div>
<div className="h-4 w-11/12 bg-gray-600 rounded"></div>
</div>
</div>
) : (
<div>
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian mt-2 text-justify">
{selectedProjectDetails?.project_description || "-"}
</p>
</div>
)}
<h2 className="font-bold my-6">دانش فنی محصول جدید</h2>
{showDialogItems ? (
<div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full animate-pulse">
<div className="w-max relative">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-gray-500 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
<div className="bg-gray-400 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
<div className="bg-gray-300 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
</div>
<div className="progressBarPointer absolute z-[200] top-0.5 left-0.5">
<div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-gray-400"></span>
<span className="text-gray-400 border border-gray-400 p-1 px-2 text-xs rounded-lg">
سطح تکنولوژی
</span>
</div>
</div>
</div>
</div>
) : (
<div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full ">
<div className="w-max relative transition-all duration-300">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-emerald-700 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح پایین</span>
</div>
<div className="bg-emerald-600 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح متوسط</span>
</div>
<div className="bg-emerald-400 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
<span className="text-gray-700">سطح بالا</span>
</div>
</div>
<div className="progressBarPointer absolute z-[200] top-0.5 transition-all duration-300">
<div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-white"></span>
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
سطح تکنولوژی
</span>
</div>
</div>
</div>
</div>
)}
<div className="flex flex-col gap-5 mt-20">
<h2 className="font-bold">مشارکت در پروژه</h2>
{showDialogItems ? (
<div className="space-y-3">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-row gap-2 items-center">
<div className="h-5 w-5 bg-gray-500 rounded-full"></div>
<div className="flex flex-row justify-between w-full gap-2">
<div className="h-4 w-24 bg-gray-600 rounded"></div>
<div className="h-4 w-20 bg-gray-500 rounded"></div>
</div>
</div>
))}
</div>
) : (
<div className="flex flex-col gap-5">
<div className="content flex flex-col gap-3 w-5/6">
<div className="flex flex-row gap-2 w-full">
<Handshake className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>مدل همکاری:</span>
<span>
{selectedProjectDetails?.collaboration_model}
</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<CodeXml className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>نقش تیم توسعه دهنده:</span>
<span>
{selectedProjectDetails?.developer_team_role}
</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<SquareUser className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>نقش کارکنان شرکت: </span>
<span>
{selectedProjectDetails?.role_company_staff ?? "-"}
</span>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<Users className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full">
<span>تعداد کارکنان درگیر: </span>
<span className="persian-font">
{selectedProjectDetails?.number_employees_involved ??
0}
</span>
</div>
</div>
</div>
<div className="flex flex-row gap-2 w-full">
<User className="text-emerald-500 w-5" />
<div className="flex flex-col justify-between w-full gap-1">
<span>لیست کارکنان : </span>
<span className="pr-1">
{selectedProjectDetails?.participants_full_name}
</span>
</div>
</div>
</div>
)}
</div>
</div>
{/* Project Details */}
<div className="px-4 pl-0 flex flex-col gap-4 w-1/2">
{showDialogItems ? (
<div className="flex flex-col gap-2 animate-pulse">
{[...Array(5)].map((_, rowIndex) => (
<div key={`skeleton-${rowIndex}`}>
<div className="h-6 bg-gray-700 rounded"></div>
</div>
))}
</div>
) : (
<div className="tbl rounded-xl overflow-hidden">
<div className="grid grid-cols-3 bg-[#3F415A] text-white p-3 items-center">
<span className="text-md text-center">
شاخص مقایسه با نمونه خارجی
</span>
<span className="text-md text-center">نمونه داخلی</span>
<span className="text-md text-center">نمونه خارجی</span>
</div>
<div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]">
{selectedProjectDetails?.technology_params?.map(
(el, index) => {
return (
<div
className="grid grid-cols-3 py-3"
key={`technology-${index}-${el.WorkflowID}`}
>
<span className="text-center">
{el.technology_parameter_title}
</span>
<div className="flex flex-row items-center gap-1 justify-center">
<span>
{formatNumber(
el.domestic_technology_parameter_value
)}
</span>
<span className="text-sm text-gray-400">
میلیون لیتر
</span>
</div>
<div className="flex flex-row items-center gap-1 justify-center">
<span>
{formatNumber(
el.foreign_technology_parameter_value
)}
</span>
<span className="text-sm text-gray-400">
میلیون لیتر
</span>
</div>
</div>
);
}
)}
</div>
</div>
)}
<div style={{ width: "100%", height: 400 }}>
{showDialogItems ? (
<div className="relative h-96 w-full bg-gray-700 rounded-lg overflow-hidden animate-pulse">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="absolute left-0 w-full h-1 bg-gray-600 rounded"
style={{
top: `${20 + i * 20}%`,
transform: `scaleX(${0.8 + i * 0.05})`,
}}
/>
))}
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute bg-gray-500 rounded-full"
style={{
width: "10px",
height: "10px",
left: `${10 + i * 18}%`,
top: `${30 + (i % 2) * 20}%`,
}}
/>
))}
</div>
) : (
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={dialogChartData}
margin={{ top: 20, right: 70, left: 30, bottom: 80 }}
>
<XAxis
dataKey="name"
interval={0}
padding={{ right: 15, left: 15 }}
axisLine={false}
tick={{ fill: "white" }}
fontSize={13}
/>
<Line
type="monotone"
dataKey="value"
stroke="#ff4d6d"
strokeWidth={2}
dot={false}
/>
<ReferenceLine
x={selectedProjectDetails?.technology_maturity_level}
stroke="#00bc7c"
strokeWidth={2}
/>
<Customized
component={(props: any) => {
const { xAxisMap, width, height } = props;
const xAxes = Object.values(xAxisMap || {});
if (!xAxes.length) return null;
const xAxis: any = xAxes[0];
const ticks = xAxis?.ticks || [];
const value =
selectedProjectDetails?.technology_maturity_level;
const xFromScale =
typeof xAxis?.scale === "function"
? xAxis.scale(value)
: undefined;
const tick = ticks.find(
(t: any) =>
t &&
(t.value === value ||
(t.payload && t.payload.value === value))
);
const axisOffsetX = xAxis?.x ?? 0;
const x =
(xFromScale ?? tick?.coordinate ?? width / 2) +
axisOffsetX -
15;
const rectWidth = 130;
const rectHeight = 28;
const rectX = x - rectWidth / 2;
const axisHeight = xAxis?.height ?? 40;
const rectY = height - axisHeight - 30;
return (
<g>
<rect
x={rectX}
y={rectY}
width={rectWidth}
height={rectHeight}
fill="none"
stroke="#00bc7c"
strokeWidth={2}
rx={6}
/>
<text
x={x}
y={rectY + rectHeight / 2}
fill="white"
fontSize={12}
textAnchor="middle"
alignmentBaseline="middle"
>
سطح بلوغ تکنولوژی
</text>
</g>
);
}}
/>
</LineChart>
</ResponsiveContainer>
)}
</div>
</div>
</div>
</DialogContent>
</Dialog>
</DashboardLayout>
);
}
export default InnovationBuiltInsidePage;