inogen/app/components/dashboard/project-management/product-innovation-page.tsx

1171 lines
45 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 {
ArrowDownCircle,
ArrowUpCircle,
Building2,
ChevronDown,
ChevronUp,
CirclePause,
DollarSign,
Funnel,
Loader2,
PickaxeIcon,
RefreshCw,
TrendingUp,
UserIcon,
UsersIcon,
Wrench,
} from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { MetricCard } from "~/components/ui/metric-card";
import { BaseCard } from "~/components/ui/base-card";
import { Checkbox } from "~/components/ui/checkbox";
import { Bar, BarChart, LabelList } from "recharts"
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "~/components/ui/popover"
import { FunnelChart } from "~/components/ui/funnel-chart";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { Label } from "~/components/ui/label";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import apiService from "~/lib/api";
import { formatNumber, handleDataValue } from "~/lib/utils";
import { DashboardLayout } from "../layout";
import { Skeleton } from "~/components/ui/skeleton";
import { Tooltip as TooltipSh, TooltipTrigger, TooltipContent } from "~/components/ui/tooltip";
interface ProjectData {
project_no: string;
project_id: string;
title: string;
project_status: string;
current_status?: string;
project_rating: string;
project_description: string;
developed_technology_type: string;
obtained_standard_title: string;
knowledge_based_certificate_obtained: string;
knowledge_based_certificate_number: string;
certificate_obtain_date: string;
issuing_authority: string;
}
interface ProductInnovationStats {
new_products_revenue_share: number;
new_products_revenue_share_percent: number;
new_products_export: number;
import_impact: number;
all_funnel: number;
successful_sample_funnel: number;
successful_products_funnel: number;
successful_improvement_or_change_funnel: number;
new_product_funnel: number;
count_innovation_construction_inside_projects: number;
average_project_score: number;
}
interface ProductInnovationData {
WorkflowID: number;
ValueP1215S1887ValueID: number;
ValueP1215S1887StageID: number;
project_id: string;
project_no: string;
title: string;
project_status: projectStatus;
project_rating: string;
current_status?: string;
project_description: string;
developed_technology_type: string;
obtained_standard_title: string;
knowledge_based_certificate_obtained: string;
knowledge_based_certificate_number: string;
certificate_obtain_date: string;
issuing_authority: string;
}
interface SortConfig {
field: string;
direction: "asc" | "desc";
}
enum projectStatus {
propozal = "پروپوزال",
contract = "پیشنویس قرارداد",
inprogress = "در حال انجام",
stop = "متوقف شده",
mafasa = "مرحله مفاصا",
finish = "پایان یافته",
notstarted = "شروع نشده",
delayed = "تأخیر دارد",
}
const columns = [
{ 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 default function Timeline( valueTimeLine : string) {
const stages = ["تجاری سازی", "توسعه", "تحلیل بازار", "ثبت ایده"];
const currentStage = stages?.toReversed()?.findIndex((x : string) => x == valueTimeLine)
const per = () => {
const main = stages?.findIndex((x) => x == "ثبت ایده")
console.log( 'yay ' , 25 * main + 12.5);
return 25 * main + 12.5
}
return (
<div className="w-full p-4">
{/* Year labels */}
<div className="grid grid-cols-4 place-items-center border-b border-gray-300/40 mb-2 items-center text-slate-300 font-thin text-sm">
<span>۱۴۰۷</span>
<span>۱۴۰۶</span>
<span>۱۴۰۵</span>
<span>۱۴۰۴</span>
</div>
{/* Timeline bar */}
<div className="relative rounded-lg flex mb-4 items-center">
{stages.map((stage, index) => (
<div key={stage} className="flex-1 flex flex-col items-center relative">
<TooltipSh>
<TooltipTrigger asChild>
<div
className={`w-full py-2 text-center transition-colors duration-300 ${
index <= currentStage ? "bg-[#3D7968] text-white" : "bg-[#3AEA83] text-slate-600"
}`}
>
<span className="mt-1 text-sm">{stage}</span>
</div>
</TooltipTrigger>
</TooltipSh>
</div>
))}
{/* Vertical line showing current position */}
{ valueTimeLine?.length > 0 && ( <> <div
className={`absolute top-0 h-[150%] bottom-0 w-[2px] bg-white rounded-full`}
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
/>
<div
className="absolute top-15 h-[max-content] translate-x-[-50%] text-xs text-gray-300 border-gray-400 rounded-md border px-2 bottom-0"
style={{ left: `${(currentStage + 0.5) * (100 / stages.length)}%` }}
>وضعیت فعلی</div>
</> ) }
</div>
</div>
);
}
export function ProductInnovationPage() {
const [showPopup, setShowPopup] = useState(false);
const [projects, setProjects] = useState<ProductInnovationData[]>([]);
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 [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<ProductInnovationStats>({
new_products_revenue_share: 0,
new_products_revenue_share_percent: 0,
new_products_export: 0,
import_impact: 0,
all_funnel: 0,
successful_sample_funnel: 0,
successful_products_funnel: 0,
successful_improvement_or_change_funnel: 0,
new_product_funnel: 0,
count_innovation_construction_inside_projects: 0,
average_project_score: 0,
});
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
direction: "asc",
});
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<ProductInnovationData | null>(null);
const [popupStats, setPopupStats] = useState({
new_products_export: 0,
new_products_export_percent: 0,
import_impact: 0,
import_impact_percent: 0,
});
const [exportChartData, setExportChartData] = useState<any[]>([]);
const [allExportData, setAllExportData] = useState<any[]>([]);
const [popupLoading, setPopupLoading] = useState(false);
const [stateCard, setStateCard] = useState({
revenueNewProducts: {
id: "revenueNewProducts",
title: "سهم از درآمد برای محصولات جدید",
value: "0",
description: "میلیون ریال",
descriptionPercent: "درصد به کل درآمد",
color: "text-[#3AEA83]",
percent : "0"
},
newProductExports: {
id: "newProductExports",
title: "صادرات محصول جدید",
value: "0",
description: "میلیون ریال",
color: "text-[#3AEA83]",
},
impactOnImports: {
id: "impactOnImports",
title: "تأثیر در واردات",
value: "0",
description: "میلیون ریال",
color: "text-[#F76276]",
},
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
const handleProjectDetails = async (project: ProductInnovationData) => {
setSelectedProjectDetails(project);
console.log(project)
setDetailsDialogOpen(true);
await fetchPopupData(project);
};
const fetchPopupData = async (project: ProductInnovationData) => {
try {
setPopupLoading(true);
// Fetch popup stats
const statsResponse = await apiService.call({
innovation_product_popup_function1: {
project_id: project.project_id
}
});
if (statsResponse.state === 0) {
const statsData = JSON.parse(statsResponse.data);
if (statsData.innovation_product_popup_function1 && statsData.innovation_product_popup_function1[0]) {
setPopupStats(JSON.parse(statsData.innovation_product_popup_function1)[0]);
}
}
// Fetch export chart data
const chartResponse = await apiService.select({
ProcessName: "export_product_innovation",
OutputFields: [
"product_title",
"full_season",
"sum(export_revenue)"
],
GroupBy: ["product_title", "full_season"]
});
if (chartResponse.state === 0) {
const chartData = JSON.parse(chartResponse.data);
if (Array.isArray(chartData)) {
// Set all data for line chart
// Filter data for the selected project (bar chart)
const filteredData = chartData.filter(item =>
item.product_title === project?.title
);
setAllExportData(chartData);
setExportChartData(filteredData);
}
}
} catch (error) {
console.error("Error fetching popup data:", error);
} finally {
setPopupLoading(false);
}
};
const loadMore = useCallback(() => {
if (hasMore && !loading) {
setCurrentPage((prev) => prev + 1);
}
}, [hasMore, loading]);
const fetchProjects = async (reset = false) => {
if (fetchingRef.current) {
return;
}
try {
fetchingRef.current = true;
if (reset) {
setLoading(true);
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",
"current_status",
"project_rating",
"project_description",
"developed_technology_type",
"obtained_standard_title",
"knowledge_based_certificate_obtained",
"knowledge_based_certificate_number",
"certificate_obtain_date",
"issuing_authority"
],
Sorts: [["start_date", "asc"]],
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 fetchStats = async () => {
try {
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_product_function: {},
});
let payload: any = JSON.parse(raw?.data);
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<any> = JSON.parse(
payload?.innovation_product_function
);
const stats = data[0];
const normalized: ProductInnovationStats = {
new_products_revenue_share: parseNum(stats?.new_products_revenue_share),
new_products_revenue_share_percent: parseNum(stats?.new_products_revenue_share_percent),
import_impact: parseNum(stats?.import_impact),
new_products_export: parseNum(stats?.new_products_export),
all_funnel: parseNum(stats?.all_funnel),
successful_sample_funnel: parseNum(stats?.successful_sample_funnel),
successful_products_funnel: parseNum(stats?.successful_products_funnel),
successful_improvement_or_change_funnel: parseNum(stats?.successful_improvement_or_change_funnel),
new_product_funnel: parseNum(stats?.new_product_funnel),
count_innovation_construction_inside_projects: parseNum(stats?.count_innovation_construction_inside_projects),
average_project_score: parseNum(stats?.average_project_score),
};
setStateCard((prev) => ({
...prev,
revenueNewProducts: {
...prev.revenueNewProducts,
value: formatNumber(normalized?.new_products_revenue_share),
percent: formatNumber(normalized?.new_products_revenue_share_percent),
},
impactOnImports: {
...prev.impactOnImports,
value: formatNumber(normalized.import_impact),
},
newProductExports: {
...prev.newProductExports,
value: formatNumber(normalized.new_products_export),
},
}));
setStats(normalized);
} catch (error) {
console.error("Error fetching stats:", error);
} finally {
setStatsLoading(false);
}
};
useEffect(() => {
fetchProjects(true);
}, [sortConfig]);
useEffect(() => {
fetchStats();
}, []);
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]);
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 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[]) => {
const seasons = [...new Set(data.map(item => item.full_season))];
const products = [...new Set(data.map(item => item.product_title))];
return seasons.map(season => {
const seasonData: any = { season };
products.forEach(product => {
const productData = data.find(item =>
item.product_title === product && item.full_season === season
);
seasonData[product] = productData?.export_revenue_sum > 0 && productData ? Math.round(productData?.export_revenue_sum) : 0;
});
return seasonData;
});
};
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;
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";
break;
case projectStatus.notstarted:
el = "secondary";
break;
case projectStatus.delayed:
el = "destructive";
break;
}
return el;
};
const renderCellContent = (item: ProductInnovationData, column: any) => {
const value = item[column.key as keyof ProductInnovationData];
switch (column.key) {
case "select":
return null;
case "details":
return (
<Button
variant="ghost"
size="sm"
onClick={() => {
handleProjectDetails(item)}}
className="text-emerald-400 underline underline-offset-4 font-ligth text-sm p-2 h-auto"
>
جزئیات بیشتر
</Button>
);
case "project_no":
return (
<Badge variant="outline" className="font-mono text-sm font-light">
{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 text-sm font-light gap-1">
<Badge
variant={statusColor(value as projectStatus)}
className="font-semibold text-base 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={`font-semibold text-base text-center border-none mx-auto`}
>
{formatNumber(String(value))}
</Badge>
);
default:
return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
}
};
const seasonOrder = ["بهار", "تابستان", "پاییز", "زمستان"];
const sortedBarData = exportChartData
.sort((a, b) => {
const getSeasonIndex = (s: string) => {
const [seasonName, year] = s.split(" ");
return parseInt(year) * 4 + seasonOrder.indexOf(seasonName);
};
return getSeasonIndex(a.full_season) - getSeasonIndex(b.full_season);
})
.map((item) => ({
label: item.full_season,
value: item.export_revenue_sum < 0 ? 0 : Math.round(item.export_revenue_sum) ,
}));
return (
<DashboardLayout title="نوآوری در محصول">
<div className=" flex w-full gap-4">
{/* Stats Cards */}
<div className="flex flex-col flex-1 gap-6">
<div className="space-y-6 w-full">
{/* Stats Grid */}
<div className="grid grid-cols-2 grid-rows-2 gap-5 h-full">
{loading || statsLoading ? (
// Loading skeleton for stats cards - matching new design
Array.from({ length: 3 }).map((_, index) => (
<Card
key={`skeleton-${index}`}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden [&>*:first-child]:row-span-1"
>
<CardContent className="p-2">
<div className="flex flex-col justify-between gap-2">
<div className="flex justify-between items-center border-b-2 mx-4 border-gray-500/20">
<div
className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }}
/>
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
</div>
</div>
<div className="flex items-center justify-center flex-col p-1">
<div
className="h-8 bg-gray-600 rounded animate-pulse mb-1"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div>
</CardContent>
</Card>
))
) : (
<>
{/* First card (Metric/Matrix style) - span two columns */}
<div className="col-span-2">
<MetricCard
title={stateCard.revenueNewProducts.title}
value={formatNumber(stateCard.revenueNewProducts.value)}
percentValue={formatNumber(stateCard.revenueNewProducts.percent)}
valueLabel={stateCard.revenueNewProducts.description}
percentLabel={stateCard.revenueNewProducts.descriptionPercent}
/>
</div>
{/* Second card */}
<div>
<BaseCard title={stateCard.newProductExports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl font-bold mb-1 text-pr-green">{stateCard.newProductExports.value}</p>
<div className="text-xs text-gray-400 font-persian">{stateCard.newProductExports.description}</div>
</div>
</div>
</div>
</BaseCard>
</div>
{/* Third card - basic BaseCard */}
<div>
<BaseCard title={stateCard.impactOnImports.title} className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50">
<div className="flex items-center justify-center flex-col">
<div className="flex items-center gap-4">
<div className="text-center">
<p className="text-3xl font-bold mb-1 text-pr-red">{stateCard.impactOnImports.value}</p>
<div className="text-xs text-gray-400 font-persian">{stateCard.impactOnImports.description}</div>
</div>
</div>
</div>
</BaseCard>
</div>
</>
)}
</div>
</div>
{/* Funnel Chart */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-full backdrop-blur-sm rounded-2xl w-full overflow-hidden">
<CardContent className="px-0 py-4">
<FunnelChart
title="قيف فرآیند پروژه ها"
data={[
{
name: "تعداد کل",
value: stats.all_funnel,
label: "تعداد کل",
},
{
name: "نمونه موفق",
value: stats.successful_sample_funnel,
label: "نمونه موفق",
},
{
name: "محصولات موفق",
value: stats.successful_products_funnel,
label: "محصولات موفق",
},
{
name: "بهبود یا تغییر موفق",
value: stats.successful_improvement_or_change_funnel,
label: "بهبود یا تغییر موفق",
},
{
name: "محصول جدید",
value: stats.new_product_funnel,
label: "محصول جدید",
},
]}
/>
</CardContent>
</Card>
</div>
{/* Data Table */}
<Card className="bg-transparent flex-2 rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar backdrop max-h-[calc(100vh-200px)]">
<TableHeader>
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-center font-persian whitespace-nowrap text-white font-medium sticky top-0 z-20 bg-pr-gray text-sm font-semibold"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<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`}
>
{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>
{/* Footer */}
<div className="p-2 px-4 bg-pr-gray">
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
<div className="text-sm font-semibold text-white">
کل پروژه ها :{formatNumber(stats?.count_innovation_construction_inside_projects)}
</div>
</div>
<div className="flex items-center flex-row gap-20 status justify-center xl:w-2/3 sm:w-full">
<div className="flex flex-row-reverse ml-[-1rem]">
<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-bold text-sm text-white">میانگین :</div>
<div className="font-bold text-sm text-white">
{formatNumber(
((stats.average_project_score ?? 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-7xl max-h-[95vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-white mr-4 border-b-2 border-gray-600 pb-2 text-sm font-semibold font-persian text-right">
شرح پروژه
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 px-6 py-2">
{/* right Column - Stats Cards and Details */}
<div className="space-y-4">
{/* Stats Cards */}
<div className="space-y-4">
<h3 className="font-bold text-base">{selectedProjectDetails?.title}</h3>
<p className="py-2">{selectedProjectDetails?.project_description}</p>
</div>
<Timeline valueTimeLine={selectedProjectDetails?.current_status} />
{/* Technical Knowledge */}
<div className=" rounded-lg py-2 mb-0">
<h3 className="text-sm text-white font-semibold mb-2">دانش فنی محصول جدید</h3>
<div className="flex gap-4 items-center">
<div className="flex items-center gap-2">
<span className="text-sm text-white font-light">توسعه درونزا</span>
<Checkbox
checked={selectedProjectDetails?.developed_technology_type === "توسعه درونزا"}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-white font-light">همکاری فناورانه</span>
<Checkbox
checked={selectedProjectDetails?.developed_technology_type === "همکاری فناوری"}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-white font-light">سایر</span>
<Checkbox
checked={selectedProjectDetails?.developed_technology_type === "سایر"}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
</div>
</div>
{/* Standards */}
<div className="rounded-lg py-4">
<h3 className="text-sm text-white font-semibold mb-4">
استانداردهای ملی و بینالمللی اخذ شده
</h3>
{selectedProjectDetails?.obtained_standard_title && selectedProjectDetails?.obtained_standard_title.length > 0 ? (
<div className="space-y-2">
{(Array.isArray(selectedProjectDetails?.obtained_standard_title)
? selectedProjectDetails?.obtained_standard_title
: [selectedProjectDetails?.obtained_standard_title]
).map((standard, index) => (
<div key={index} className="flex items-center gap-2">
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
<span className="text-sm text-white font-light">{standard}</span>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500">
هیچ استانداردی ثبت نشده است.
</p>
)}
</div>
{/* Knowledge-based Certificate Button */}
<div className="justify-self-centerr grid py-1 mx-auto">
{selectedProjectDetails?.knowledge_based_certificate_obtained === "خیر" ? (
<div className=" border border-pr-red mx-auto rounded-lg p-2 text-center">
<button className="text-pr-red font-bold text-sm">
گواهی دانشبنیان ندارد
</button>
</div>
) : (
<Card className="justify-self-center border-pr-green bg-transparent py-0">
<CardContent className="p-2 text-center">
<Popover>
<PopoverTrigger asChild>
<Button
variant="default"
className=" text-pr-green font-bold text-sm hover:bg-transparent cursor-pointer bg-transparent"
>
مشاهده اطلاعات گواهی دانشبنیان
</Button>
</PopoverTrigger>
<PopoverContent
className="w-64 bg-gray-900 border border-gray-700 text-right"
align="center"
>
<div className="space-y-2">
<p className="text-sm text-white">
<span className="font-bold">شماره گواهی: </span>
{selectedProjectDetails?.knowledge_based_certificate_number ||
"—"}
</p>
<p className="text-sm text-white">
<span className="font-bold">تاریخ اخذ: </span>
{handleDataValue(selectedProjectDetails?.certificate_obtain_date) || "—"}
</p>
<p className="text-sm text-white">
<span className="font-bold">مرجع صادرکننده: </span>
{selectedProjectDetails?.issuing_authority || "—"}
</p>
</div>
</PopoverContent>
</Popover>
</CardContent>
</Card>
)}
</div>
</div>
{/* Left Column - Project Description and Charts */}
{popupLoading ? (
<div className="lg:col-span-2 border-r-2 flex flex-col gap-2 pr-4 pb-2 border-r-[#5F6284]/50">
<div className="rounded-lg pt-4 flex w-full gap-2">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] flex-1 backdrop-blur-sm border-gray-700/50 col-span-2">
<CardContent className="p-2 h-full">
<Skeleton className="h-full w-full" />
</CardContent>
</Card>
<Card className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] flex-1 backdrop-blur-sm border-gray-700/50 col-span-2">
<CardContent className="p-2 h-full">
<Skeleton className="h-full w-full" />
</CardContent>
</Card>
</div>
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
<Skeleton className="h-8 w-1/3 mb-4" />
<Skeleton className="h-60 w-full" />
</div>
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
<Skeleton className="h-8 w-1/3 mb-4" />
<Skeleton className="h-60 w-full" />
</div>
</div>
) : (
<div className="lg:col-span-2 border-r-2 flex flex-col gap-2 pr-4 pb-2 border-r-[#5F6284]/50">
{/* Project Description - two MetricCards side by side */}
<div className="rounded-lg pt-4 grid grid-cols-2 gap-4 w-full">
<MetricCard
title="میزان صادارت محصول جدید"
value={Math.round(popupStats?.new_products_export > 0 ? popupStats?.new_products_export : 0)}
percentValue={Math.round(popupStats?.new_products_export_percent > 0 ? popupStats?.new_products_export_percent : 0)}
valueLabel="میلیون ریال"
percentLabel="درصد به کل صادرات"
/>
<MetricCard
title="تاثیر در واردات"
value={Math.round(popupStats?.import_impact > 0 ? popupStats?.import_impact : 0)}
percentValue={Math.round(popupStats?.import_impact_percent > 0 ? popupStats?.import_impact_percent : 0)}
valueLabel="میلیون ریال"
percentLabel="درصد صرفه جویی"
/>
</div>
{/* Export Revenue Bar Chart */}
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
<div className="h-60">
{exportChartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart
className="aspect-auto w-full"
data={sortedBarData}
barGap={15}
barSize={30}
margin={{ top: 18 }}
>
<CartesianGrid vertical={false} stroke="#475569" />
<XAxis
dataKey="label"
tickLine={false}
axisLine={false}
stroke="#C3C3C3"
tickMargin={8}
tickFormatter={(value: string) => `${value.split(" ")[0]} ${formatNumber(value.split(" ")[1]).replaceAll('٬','')}`}
fontSize={11}
/>
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value: number) => `${formatNumber(value)} میلیون`} />
<Bar dataKey="value" fill="#10B981" radius={10}>
<LabelList formatter={(value: number) => `${formatNumber(value)}`} position="top" offset={15} fill="F9FAFB" className="fill-foreground" fontSize={16} />
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
)}
</div>
</div>
{/* Export Revenue Line Chart */}
<div className="bg-[linear-gradient(to_bottom_left,#464861,45%,#111628)] rounded-lg px-6 py-4">
<h3 className="text-sm font-semibold text-white">ظرفیت صادر شده</h3>
<div className="h-60">
{allExportData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart className="aspect-auto w-full" data={transformDataForLineChart(allExportData)} margin={{ top: 20, right: 30, left: 10, bottom: 50 }}>
<CartesianGrid vertical={false} stroke="#374151" />
<XAxis dataKey="season" stroke="#9CA3AF" fontSize={11} tick={({ x, y, payload }) => (
<g transform={`translate(${x},${y + 10})`}>
<text x={-40} y={15} dy={0} textAnchor="end" fill="#9CA3AF" fontSize={11} transform="rotate(-45)">{(payload as any).value}</text>
</g>
)} />
<YAxis tickLine={false} axisLine={false} stroke="#9CA3AF" fontSize={11} tick={{ dx: -50 }} tickFormatter={(value) => `${formatNumber(value)} میلیون`} />
<Tooltip formatter={(value: number) => `${formatNumber(value)} میلیون`} contentStyle={{ backgroundColor: "#1F2937", border: "1px solid #374151", borderRadius: "6px", padding: "6px 10px", fontSize: "11px", color: "#F9FAFB" }} />
<Legend layout="vertical" verticalAlign="middle" align="right" iconType={"plainline"} className="!flex" wrapperStyle={{ fontSize: 11, paddingLeft: 12, gap: 10 }} />
{[...new Set(allExportData.map((item) => item.product_title))].slice(0, 5).map((product, index) => {
const colors = ["#10B981", "#EF4444", "#3B82F6", "#F59E0B", "#8B5CF6"];
return <Line key={product} type="linear" dot={false} activeDot={{ r: 5 }} dataKey={product} stroke={colors[index % colors.length]} strokeWidth={2} />;
})}
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-gray-400">دادهای برای نمایش وجود ندارد</div>
)}
</div>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</DashboardLayout>
);
}