inogen/app/components/dashboard/project-management/green-innovation-page.tsx
2025-10-10 19:11:38 +03:30

1201 lines
42 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.

// import moment from "moment-jalaali";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
} from "recharts";
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 { formatNumber } from "~/lib/utils";
import {
Building2,
ChevronDown,
ChevronUp,
Flame,
Key,
LoaderCircle,
PickaxeIcon,
RefreshCw,
Sparkle,
TrendingUp,
UserIcon,
UsersIcon,
Zap,
} from "lucide-react";
import toast from "react-hot-toast";
import apiService from "~/lib/api";
import { formatCurrency } from "~/lib/utils";
import DashboardLayout from "../layout";
// moment.loadPersian({ usePersianDigits: true });
interface GreenInnovationData {
WorkflowID: string;
approved_budget: string;
done_date: string | null;
observer: string;
project_description: string;
project_id: string;
project_no: string;
project_rating: string;
project_status: string;
start_date: string;
title: string;
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
}
interface StateItem {
id: string;
title: string;
percent: {
value: number;
description: string;
};
total: {
value: number;
description: string;
};
}
interface StatsCard {
pollution: StateItem;
waste: StateItem;
}
interface InnovationStats {
electricity_recovery_reduction: number;
electricity_recovery_reduction_percent: number;
feed_recovery_reduction: number;
feed_recovery_reduction_percent: number;
fuel_recovery_reduction: number;
fuel_recovery_reduction_percent: number;
pollution_reduction: number;
pollution_reduction_percent: number;
waste_reduction: number;
waste_reductionn_percent: number;
water_recovery_reduction: number;
water_recovery_reduction_percent: number;
average_project_score: number;
count_innovation_green_projects: number;
standard_regulations: string;
}
interface Params {
icon: any;
label: string;
value: number;
suffix: string;
percent: number;
}
interface RecycleParams {
water: Params;
food: Params;
power: Params;
oil: Params;
}
interface stateCounter {
totalProjects: number;
}
interface ChartDataItem {
name: string;
pv: any; // actual value
amt: number; // max value or target
}
enum projectStatus {
propozal = "پروپوزال",
contract = "پیشنویس قرارداد",
inprogress = "در حال انجام",
stop = "متوقف شده",
mafasa = "مرحله مفاصا",
finish = "پایان یافته",
}
const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
{ key: "title", label: "عنوان پروژه", sortable: true, width: "400px" },
{
key: "project_status",
label: "وضعیت پروژه",
sortable: true,
width: "140px",
},
{
key: "project_rating",
label: "امتیاز پروژه",
sortable: true,
width: "140px",
},
{ key: "details", label: "جزئیات پروژه", sortable: false, width: "140px" },
];
export function GreenInnovationPage() {
const [projects, setProjects] = useState<GreenInnovationData[]>([]);
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 [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set()
);
const [standartRegulation, setStandardRegulation] = useState<Array<string>>(
[]
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<GreenInnovationData | null>(null);
const [recycleParams, setRecycleParams] = useState<RecycleParams>({
water: {
icon: <Key className="text-success" size={"18px"} />,
label: "آب",
value: 0,
suffix: "لیتر",
percent: 0,
},
food: {
icon: <Sparkle className="text-success" size={"18px"} />,
label: "خوراک",
value: 0,
suffix: "تن",
percent: 0,
},
power: {
icon: <Zap className="text-success" size={"18px"} />,
label: "برق",
value: 0,
suffix: "میلیون مگاوات",
percent: 0,
},
oil: {
icon: <Flame className="text-success" size={"18px"} />,
label: "سوخت",
value: 0,
suffix: "متر مربع",
percent: 0,
},
});
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
pollution: {
id: "reduce-pollution",
title: "کاهش آلایندگی",
total: {
value: 10.45,
description: "میلیون ریال",
},
percent: {
value: 10,
description: "درصد به کل درآمد",
},
},
waste: {
id: "reduce-junkfull",
title: "کاهش ضایعات",
total: {
value: 10,
description: "میلیون ریال",
},
percent: {
value: 10,
description: "درصد به کل درآمد",
},
},
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) {
newSelected.delete(projectNo);
} else {
newSelected.add(projectNo);
}
setSelectedProjects(newSelected);
};
const handleProjectDetails = (project: GreenInnovationData) => {
setSelectedProjectDetails(project);
setDetailsDialogOpen(true);
};
// ...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",
"start_date",
"done_date",
"approved_budget",
"observer",
],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
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 loadMore = useCallback(() => {
if (hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
useEffect(() => {
fetchProjects(true);
fetchTotalCount();
}, [sortConfig]);
useEffect(() => {
fetchStats();
}, [selectedProjects]);
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 fetchTotalCount = async () => {
try {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
try {
const parsedData = JSON.parse(dataString);
if (Array.isArray(parsedData) && parsedData[0]) {
const count = parsedData[0].project_no_count || 0;
// Keep stats in sync if backend stats not yet loaded
setStats((prev) => ({ ...prev, totalProjects: count }));
}
} catch (parseError) {
console.error("Error parsing count data:", parseError);
}
}
}
} catch (error) {
console.error("Error fetching total count:", error);
}
};
// Fetch aggregated stats from backend call API (innovation_process_function)
const fetchStats = async () => {
try {
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_green_function: {
project_ids:
selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
},
});
let payload: any = raw?.data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch {}
}
const parseNum = (v: unknown): any => {
const convertNumber = typeof v === "number" ? Math.max(0, v) : 0;
if (v == null) return 0;
if (typeof v === "number") return convertNumber;
if (typeof v === "string") {
const cleaned = v.replace(/,/g, "").trim();
const n = parseFloat(cleaned);
return isNaN(n) ? 0 : convertNumber;
}
return 0;
};
const data: Array<InnovationStats> = JSON.parse(
payload?.innovation_green_function
);
const stats = data[0];
const normalized: any = {
food: {
value: formatNumber(parseNum(stats?.feed_recovery_reduction)),
percent: parseNum(stats?.feed_recovery_reduction_percent),
},
oil: {
value: formatNumber(parseNum(stats?.fuel_recovery_reduction)),
percent: parseNum(stats?.fuel_recovery_reduction_percent),
},
power: {
value: formatNumber(parseNum(stats?.electricity_recovery_reduction)),
percent: parseNum(stats?.electricity_recovery_reduction_percent),
},
water: {
value: formatNumber(parseNum(stats.water_recovery_reduction)),
percent: parseNum(stats.water_recovery_reduction_percent),
},
pollution: {
value: formatNumber(parseNum(stats.pollution_reduction)),
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
},
waste: {
value: formatNumber(parseNum(stats.waste_reduction)),
percent: formatNumber(parseNum(stats.waste_reductionn_percent)),
},
avarage: stats.average_project_score,
countInnovationGreenProjects: stats.count_innovation_green_projects,
standardRegulation: stats.standard_regulations
.replace("\r", "")
.split("\n"),
};
setStandardRegulation(normalized.standardRegulation);
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,
pollution: {
...prev.pollution,
total: { ...prev.pollution.total, value: normalized.pollution.value },
percent: {
...prev.pollution.percent,
value: normalized.pollution.percent,
},
},
waste: {
...prev.waste,
total: { ...prev.waste.total, value: normalized.waste.value },
percent: { ...prev.waste.percent, value: normalized.waste.percent },
},
}));
setRecycleParams((prev) => ({
...prev,
water: {
...prev.water,
value: normalized.water.value,
percent: normalized.water.percent,
},
food: {
...prev.food,
value: normalized.food.value,
percent: normalized.food.percent,
},
oil: {
...prev.oil,
value: normalized.oil.value,
percent: normalized.oil.percent,
},
power: {
...prev.power,
value: normalized.power.value,
percent: normalized.power.percent,
},
}));
setChartData([
{
name: recycleParams.water.label,
pv: Math.max(0, normalized.water.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.power.label,
pv: Math.max(0, normalized.power.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.oil.label,
pv: Math.max(0, normalized.oil.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.food.label,
pv: Math.max(0, normalized.food.percent).toFixed(2),
amt: 100,
},
]);
};
const renderCellContent = (item: GreenInnovationData, column: any) => {
const value = item[column.key as keyof GreenInnovationData];
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;
};
const [chartData, setChartData] = useState<Array<ChartDataItem>>([
{ name: recycleParams.water.label, pv: 70, amt: 80 },
{ name: recycleParams.power.label, pv: 45, amt: 60 },
{ name: recycleParams.oil.label, pv: 90, amt: 75 },
{ 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]">
{/* Stats Cards */}
<div className="flex gap-6 mb-5 md:flex-col xl:flex-row">
<div className="flex flex-col justify-between xl:w-1/2 sm:w-full sm:gap-2">
{loading || 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-bold text-white font-persian 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>
))}
</div>
{/* Process Impacts Chart */}
{statsLoading ? (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-full backdrop-blur-sm rounded-lg w-full overflow-hidden">
<CardContent className="p-0 h-full">
<div className="border-b-2 border-gray-500/20">
<div className="w-full p-4 px-6">
<div className="h-6 bg-gray-600 rounded animate-pulse w-1/2" />
</div>
</div>
<div className="content grid gap-6 h-full p-8 box-border items-center justify-between sm:grid-cols-1 xl:grid-cols-[30%_70%]">
<div className="params flex flex-col gap-3.5">
{[...Array(3)].map((_, paramIndex) => (
<div
key={paramIndex}
className="param flex flex-row justify-between items-center"
>
<div className="flex flex-row gap-2 items-center">
<div className="h-6 w-6 bg-gray-600 rounded-full animate-pulse" />
<div className="h-4 bg-gray-600 rounded animate-pulse w-24" />
</div>
<div className="flex flex-row gap-1.5 items-center">
<div className="h-4 bg-gray-600 rounded animate-pulse w-12" />
<div className="h-4 bg-gray-600 rounded animate-pulse w-8" />
</div>
</div>
))}
</div>
<div className="h-[17rem] w-full min-w-[35rem] flex items-end justify-between px-6">
{[...Array(8)].map((_, barIndex) => (
<div
key={barIndex}
className="bg-gray-600 rounded-t animate-pulse"
style={{
width: "14px",
height: `${30 + barIndex * 8}%`, // ارتفاع تصادفی یا ترتیب مشخص
}}
/>
))}
</div>
</div>
</CardContent>
</Card>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-full backdrop-blur-sm rounded-lg w-full overflow-hidden">
<CardContent className="p-0 h-full overflow-hidden">
<div className="border-b-2 border-gray-500/20">
<div className="w-full p-4 px-6">
<span>بازیافت و بازیابی منابع</span>
</div>
</div>
<div className="content grid gap-9 h-max p-8 box-border items-center justify-between sm:grid-cols-1 sm:overflow-x-scroll xl:overflow-hidden xl:grid-cols-[30%_70%]">
<div className="params flex flex-col gap-3.5">
{Object.entries(recycleParams).map((el, index) => {
return (
<div
key={index}
className="param flex flex-row justify-between items-center"
>
<div className="flex flex-row gap-2">
{el[1].icon}
<span className="font-normal text-sm font-persian">
{el[1].label}:
</span>
</div>
<div className="flex flex-row gap-1.5 items-center">
<span className="text-sm font-normal font-persian">
{el[1].value}
</span>
<span className="text-sm font-persian">
{el[1].suffix}
</span>
</div>
</div>
);
})}
</div>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={500}
height={300}
data={chartData}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid
stroke="#374151"
strokeDasharray="0"
vertical={false}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{
fill: "#fff",
fontSize: 14,
fontWeight: "normal",
dy: 10, // جابجایی عمودی (مثبت = پایین‌تر، منفی = بالاتر)
}}
/>
<YAxis
type="number"
domain={[0, 100]}
axisLine={false}
tickLine={false}
tick={{
fill: "#99a1af", // رنگ متن
fontSize: 14, // سایز فونت
dx: -30, // جابجایی افقی (اعداد نزدیک‌تر یا دورتر از محور)
}}
tickFormatter={(val) => `${formatNumber(val)}%`}
/>
<Bar
dataKey="pv"
fill="#3AEA83"
radius={[20, 20, 0, 0]}
barSize={14}
label={{
position: "top",
fill: "#fff",
fontWeight: "bold",
formatter: (value: any) =>
`${formatNumber(value)}%`,
}}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</CardContent>
</Card>
)}
<Card className="w-1/2 bg-pr-gray backdrop-blur-sm rounded-lg overflow-hidden">
<CardContent className="p-0">
<div className="border-b-2 border-gray-500/20">
<div className="flex flex-row justify-between w-full p-4">
{statsLoading ? (
<>
<span className="h-4 w-28 bg-gray-500/40 rounded animate-pulse"></span>
<span className="h-5 w-5 bg-gray-500/40 rounded-full animate-pulse"></span>
</>
) : (
<>
<span>استاندارد ها و مقررات</span>
<TrendingUp />
</>
)}
</div>
</div>
<div className="flex flex-col gap-3 p-4 overflow-y-scroll h-[20rem]">
{statsLoading
? Array.from({ length: 10 }).map((_, index) => (
<div
key={`skeleton-${index}`}
className="flex gap-2 items-center"
>
<span className="h-4 w-4 bg-gray-500/40 rounded-full animate-pulse"></span>
<span className="h-3 w-32 bg-gray-500/40 rounded animate-pulse"></span>
</div>
))
: standartRegulation.map((item, index) => (
<div
key={`${item}-${index}-1`}
className="flex flex-row gap-2"
>
<LoaderCircle
size={"20px"}
height={"20px"}
className="text-emerald-400 w-5 h-5 shrink-0"
/>
<span className="text-sm truncate">{item}</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-lg overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar h-full">
<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: 10 }).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-400" />
<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-[41%_41%] text-sm text-gray-300 font-persian justify-between">
<div className="text-center gap-2 items-center">
<div className="text-base text-gray-401 mr-10">
کل پروژه ها :{formatNumber(actualTotalCount)}
</div>
</div>
<div className="flex items-center flex-row status justify-between w-[calc(40%-5px)]">
<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>
{/* <div className="p-2 px-4 bg-gray-700/50">
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian">
<div className="text-center gap-2 items-center flex">
<div className="text-base text-gray-401 mb-1">
{" "}
کل پروژه ها :{" "}
{formatNumber(stats?.totalProjects || actualTotalCount)}
</div>
</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) ??
tblAvarage ??
0
)}
</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-4xl max-h-[80vh] 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="space-y-4 flex justify-between text-right p-6">
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian px-1 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>
</div>
{/* Project Details */}
<div className="flex flex-[3] gap-2 flex-col px-4">
<div className="font-bold text-right ">جزئیات پروژه</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<Building2 className="h-4 text-green-500" />
زمان شروع:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.start_date
? moment(
selectedProjectDetails?.start_date,
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<PickaxeIcon className="h-4 text-green-500" />
زمان پایان:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.done_date
? moment(
selectedProjectDetails?.done_date,
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<UsersIcon className="h-4 text-green-500" />
هزینه برآورد شده:
</h4>
<span className="text-white font-bold font-persian">
{formatNumber(
Number(
selectedProjectDetails?.approved_budget.replaceAll(
",",
""
)
)
) || "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<UserIcon className="h-4 text-green-500" />
نفر مرتبط:
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.observer || "-"}
</span>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</DashboardLayout>
);
}
export default GreenInnovationPage;