import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { Bar, BarChart, LabelList } from "recharts";
import { Badge } from "~/components/ui/badge";
import { BaseCard } from "~/components/ui/base-card";
import { Button } from "~/components/ui/button";
import { Card, CardContent } from "~/components/ui/card";
import { Checkbox } from "~/components/ui/checkbox";
import { MetricCard } from "~/components/ui/metric-card";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import {
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { FunnelChart } from "~/components/ui/funnel-chart";
import { Skeleton } from "~/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { Tooltip as TooltipSh, TooltipTrigger } from "~/components/ui/tooltip";
import { useStoredDate } from "~/hooks/useStoredDate";
import apiService from "~/lib/api";
import { EventBus, formatNumber, handleDataValue } from "~/lib/utils";
import type { CalendarDate } from "~/types/util.type";
import { DashboardLayout } from "../layout";
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 (
{/* Year labels */}
۱۴۰۷
۱۴۰۶
۱۴۰۵
۱۴۰۴
{/* Timeline bar */}
{stages.map((stage, index) => (
))}
{/* Vertical line showing current position */}
{valueTimeLine?.length > 0 && (
<>
{" "}
وضعیت فعلی
>
)}
);
}
export function ProductInnovationPage() {
// const [showPopup, setShowPopup] = useState(false);
const [projects, setProjects] = useState([]);
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({
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({
field: "start_date",
direction: "asc",
});
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState(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([]);
const [allExportData, setAllExportData] = useState([]);
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 [date, setDate] = useStoredDate();
const observerRef = useRef(null);
const fetchingRef = useRef(false);
const handleProjectDetails = async (project: ProductInnovationData) => {
setSelectedProjectDetails(project);
setDetailsDialogOpen(true);
await fetchPopupData(project, date?.start, date?.end);
};
const fetchPopupData = async (
project: ProductInnovationData,
startDate?: string,
endDate?: string
) => {
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", "=", "نوآوری در محصول", "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 fetchStats = async () => {
try {
setStatsLoading(true);
const raw = await apiService.call({
innovation_product_function: {
start_date: date?.start || null,
end_date: date?.end || null,
},
});
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 = 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: normalized.new_products_revenue_share,
percent: 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(() => {
EventBus.on("dateSelected", (date: CalendarDate) => {
if (date) {
setDate(date);
}
});
}, []);
useEffect(() => {
if (date.end && date.start) fetchProjects(true);
}, [sortConfig, date]);
useEffect(() => {
if (date.end && date.start) fetchStats();
}, [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]);
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 (
);
case "project_no":
return (
{String(value)}
);
case "title":
return (
{String(value)}
);
case "project_status":
return (
{String(value)}
);
case "project_rating":
return (
{formatNumber(String(value))}
);
default:
return (
{String(value) || "-"}
);
}
};
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 (
{/* Stats Cards */}
{/* Stats Grid */}
{loading || statsLoading ? (
// Loading skeleton for stats cards - matching new design
Array.from({ length: 3 }).map((_, index) => (
))
) : (
<>
{/* First card (Metric/Matrix style) - span two columns */}
{/* Second card */}
{stateCard.newProductExports.value}
{stateCard.newProductExports.description}
{/* Third card - basic BaseCard */}
{stateCard.impactOnImports.value}
{stateCard.impactOnImports.description}
>
)}
{/* Funnel Chart */}
{/* Data Table */}
{columns.map((column) => (
{column.sortable ? (
) : (
column.label
)}
))}
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 10 }).map((_, index) => (
{columns.map((column) => (
))}
))
) : projects.length === 0 ? (
هیچ پروژهای یافت نشد
) : (
projects.map((project, index) => (
{columns.map((column) => (
{renderCellContent(project, column)}
))}
))
)}
{/* Infinite scroll trigger */}
{/* Footer */}
کل پروژه ها :
{formatNumber(
stats?.count_innovation_construction_inside_projects
)}
میانگین :
{formatNumber(
((stats.average_project_score ?? 0) as number).toFixed?.(
1
) ?? 0
)}
{/* Project Details Dialog */}
);
}