import { ChevronDown, ChevronUp, RefreshCw, Eye, Star, TrendingUp, Hexagon, Download } from "lucide-react"; import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import toast from "react-hot-toast"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import apiService from "~/lib/api"; import { formatCurrency, formatNumber } from "~/lib/utils"; import { DashboardLayout } from "../layout"; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig, } from "~/components/ui/chart"; import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, CartesianGrid, LabelList, Cell, RadialBarChart, PolarGrid, RadialBar, PolarRadiusAxis } from "recharts"; import { BaseCard } from "~/components/ui/base-card"; import { Label } from "recharts" import { MetricCard } from "~/components/ui/metric-card"; interface IdeaData { idea_title: string; idea_registration_date: string; idea_status: string; increased_revenue: string; full_name: string; personnel_number: string; management: string; deputy: string; innovator_team_members: string; innovation_type: string; idea_originality: string; idea_axis: string; idea_description: string; idea_current_status_description: string; idea_execution_benefits: string; process_improvements: string; } interface PersonRanking { full_name: string; full_name_count: number; ranking: number; stars: number; } interface IdeaStatusData { idea_status: string; idea_status_count: number; } interface IdeaStatsData { registered_innovation_technology_idea: string; ongoing_innovation_technology_ideas: string; increased_revenue_from_ideas: string; increased_revenue_from_ideas_percent: string; } interface SortConfig { field: string; direction: "asc" | "desc"; } type ColumnDef = { key: string; label: string; sortable: boolean; width: string; }; const columns: ColumnDef[] = [ { key: "idea_title", label: "عنوان ایده", sortable: true, width: "250px" }, { key: "idea_registration_date", label: "تاریخ ثبت ایده", sortable: true, width: "180px" }, { key: "idea_status", label: "وضعیت ایده", sortable: true, width: "150px" }, { key: "increased_revenue", label: "درآمد حاصل از ایده", sortable: true, width: "180px" }, { key: "details", label: "جزئیات بیشتر", sortable: false, width: "120px" }, ]; export function ManageIdeasTechPage() { const [ideas, setIdeas] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(10); const [hasMore, setHasMore] = useState(true); const [totalCount, setTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0); const [selectedIdea, setSelectedIdea] = useState(null); const [isDetailsOpen, setIsDetailsOpen] = useState(false); const [sortConfig, setSortConfig] = useState({ field: "idea_title", direction: "asc", }); // People ranking state const [peopleRanking, setPeopleRanking] = useState([]); const [loadingPeople, setLoadingPeople] = useState(false); // Chart state const [chartData, setChartData] = useState([]); const [loadingChart, setLoadingChart] = useState(false); // Stats state const [statsData, setStatsData] = useState(null); const [loadingStats, setLoadingStats] = useState(false); const observerRef = useRef(null); const fetchingRef = useRef(false); const scrollTimeoutRef = useRef(null); const scrollContainerRef = useRef(null); const fetchIdeas = 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: "idea", OutputFields: [ "idea_title", "idea_registration_date", "idea_status", "increased_revenue", "full_name", "personnel_number", "management", "deputy", "innovator_team_members", "innovation_type", "idea_originality", "idea_axis", "idea_description", "idea_current_status_description", "idea_execution_benefits", "process_improvements", ], Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Sorts: [[sortConfig.field, sortConfig.direction]], Conditions: [], }); 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) { setIdeas(parsedData); setTotalCount(parsedData.length); } else { setIdeas((prev) => [...prev, ...parsedData]); setTotalCount((prev) => prev + parsedData.length); } setHasMore(parsedData.length === pageSize); } else { if (reset) { setIdeas([]); setTotalCount(0); } setHasMore(false); } } catch (parseError) { console.error("Error parsing idea data:", parseError); if (reset) { setIdeas([]); setTotalCount(0); } setHasMore(false); } } else { if (reset) { setIdeas([]); setTotalCount(0); } setHasMore(false); } } else { toast.error(response.message || "خطا در دریافت اطلاعات ایده‌ها"); if (reset) { setIdeas([]); setTotalCount(0); } setHasMore(false); } } catch (error) { console.error("Error fetching ideas:", error); toast.error("خطا در دریافت اطلاعات ایده‌ها"); if (reset) { setIdeas([]); setTotalCount(0); } setHasMore(false); } finally { setLoading(false); setLoadingMore(false); fetchingRef.current = false; } }; const loadMore = useCallback(() => { if (hasMore && !loading && !loadingMore && !fetchingRef.current) { setCurrentPage((prev) => prev + 1); } }, [hasMore, loading, loadingMore]); useEffect(() => { fetchIdeas(true); fetchTotalCount(); fetchPeopleRanking(); fetchChartData(); fetchStatsData(); }, [sortConfig]); useEffect(() => { if (currentPage > 1) { fetchIdeas(false); } }, [currentPage]); // Infinite scroll observer with debouncing useEffect(() => { const scrollContainer = scrollContainerRef.current; const handleScroll = () => { if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return; if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = setTimeout(() => { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; if (scrollPercentage >= 0.95) { loadMore(); } }, 150); }; if (scrollContainer) { scrollContainer.addEventListener("scroll", handleScroll, { passive: true }); } return () => { if (scrollContainer) { scrollContainer.removeEventListener("scroll", handleScroll); } if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } }; }, [loadMore, hasMore, loadingMore]); const handleSort = (field: string) => { fetchingRef.current = false; setSortConfig((prev) => ({ field, direction: prev.field === field && prev.direction === "asc" ? "desc" : "asc", })); setCurrentPage(1); setIdeas([]); setHasMore(true); }; const fetchTotalCount = async () => { try { const response = await apiService.select({ ProcessName: "idea", OutputFields: ["count(idea_title)"], Conditions: [], }); 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]) { setActualTotalCount(parsedData[0].idea_title_count || 0); } } catch (parseError) { console.error("Error parsing count data:", parseError); } } } } catch (error) { console.error("Error fetching total count:", error); } }; const fetchPeopleRanking = async () => { try { setLoadingPeople(true); const response = await apiService.select({ ProcessName: "idea", OutputFields: ["full_name", "count(full_name)"], GroupBy: ["full_name"], }); if (response.state === 0) { const dataString = response.data; if (dataString && typeof dataString === "string") { try { const parsedData = JSON.parse(dataString); if (Array.isArray(parsedData)) { // Calculate rankings and stars const counts = parsedData.map(item => item.full_name_count); const maxCount = Math.max(...counts); const minCount = Math.min(...counts); // Sort by count first (highest first) const sortedData = parsedData.sort((a, b) => b.full_name_count - a.full_name_count); const rankedPeople = []; let currentRank = 1; let sum = 1; for (let i = 0; i < sortedData.length; i++) { const item = sortedData[i]; // If this is not the first person and their count is different from previous if (i > 0 && sortedData[i - 1].full_name_count !== item.full_name_count) { currentRank = sum + 1; // New rank based on position sum++; } const normalizedScore = maxCount === minCount ? 1 : (item.full_name_count - minCount) / (maxCount - minCount); const stars = Math.max(1, Math.round(normalizedScore * 5)); rankedPeople.push({ full_name: item.full_name, full_name_count: item.full_name_count, ranking: currentRank, stars: stars, }); } setPeopleRanking(rankedPeople); } } catch (parseError) { console.error("Error parsing people ranking data:", parseError); } } } else { toast.error(response.message || "خطا در دریافت اطلاعات رتبه‌بندی افراد"); } } catch (error) { console.error("Error fetching people ranking:", error); toast.error("خطا در دریافت اطلاعات رتبه‌بندی افراد"); } finally { setLoadingPeople(false); } }; const fetchChartData = async () => { try { setLoadingChart(true); const response = await apiService.select({ ProcessName: "idea", OutputFields: ["idea_status", "count(idea_status)"], GroupBy: ["idea_status"], }); if (response.state === 0) { const dataString = response.data; if (dataString && typeof dataString === "string") { try { const parsedData: IdeaStatusData[] = JSON.parse(dataString); if (Array.isArray(parsedData)) { setChartData(parsedData?.reverse()); } } catch (parseError) { console.error("Error parsing chart data:", parseError); } } } else { toast.error(response.message || "خطا در دریافت اطلاعات نمودار"); } } catch (error) { console.error("Error fetching chart data:", error); toast.error("خطا در دریافت اطلاعات نمودار"); } finally { setLoadingChart(false); } }; const fetchStatsData = async () => { try { setLoadingStats(true); const response = await apiService.call({ idea_page_function: {} }); if (response.state === 0) { const dataString = response.data; if (dataString && typeof dataString === "string") { try { const parsedData: IdeaStatsData = JSON.parse(dataString); setStatsData(parsedData); } catch (parseError) { console.error("Error parsing stats data:", parseError); } } } else { toast.error(response.message || "خطا در دریافت آمار ایده‌ها"); } } catch (error) { console.error("Error fetching stats data:", error); toast.error("خطا در دریافت آمار ایده‌ها"); } finally { setLoadingStats(false); } }; const toPersianDigits = (input: string | number): string => { const str = String(input); const map: Record = { "0": "۰", "1": "۱", "2": "۲", "3": "۳", "4": "۴", "5": "۵", "6": "۶", "7": "۷", "8": "۸", "9": "۹", }; return str.replace(/[0-9]/g, (d) => map[d] ?? d); }; const formatDate = (dateString: string | null) => { if (!dateString || dateString === "null" || dateString.trim() === "") { return "-"; } const raw = String(dateString).trim(); const jalaliPattern = /^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/; const jalaliMatch = raw.match(jalaliPattern); if (jalaliMatch) { const [, y, m, d] = jalaliMatch; const mm = m.padStart(2, "0"); const dd = d.padStart(2, "0"); return toPersianDigits(`${y}/${mm}/${dd}`); } try { const parsed = new Date(raw); if (isNaN(parsed.getTime())) return "-"; return new Intl.DateTimeFormat("fa-IR-u-ca-persian", { year: "numeric", month: "2-digit", day: "2-digit", }).format(parsed); } catch { return "-"; } }; // Chart configuration for shadcn/ui const chartConfig: ChartConfig = { count: { label: "تعداد", }, }; // Color palette for idea status // Specific colors for idea statuses const getChartStatusColor = (status: string) => { switch (status) { case "اجرا شده": return "#69C8EA"; case "تایید شده": return "#3AEA83"; case "در حال بررسی": return "#EAD069"; case "رد شده": return "#F76276"; default: return "#6B7280"; } }; const statusColorPalette = ["#3AEA83", "#69C8EA", "#F76276", "#FFD700", "#A757FF", "#E884CE", "#C3BF8B", "#FB7185"]; // Build a mapping of status value -> color based on loaded ideas const statusColorMap = useMemo(() => { const map: Record = {}; const seenStatuses = new Set(); ideas.forEach((idea) => { const status = String(idea.idea_status || "").trim(); if (status && !seenStatuses.has(status)) { seenStatuses.add(status); } }); const statusArray = Array.from(seenStatuses).sort(); statusArray.forEach((status, index) => { map[status] = statusColorPalette[index % statusColorPalette.length]; }); return map; }, [ideas]); const getStatusColor = (status: string) => { const statusValue = String(status || "").trim(); return statusColorMap[statusValue] || "#6B7280"; }; const handleShowDetails = (idea: IdeaData) => { setSelectedIdea(idea); setIsDetailsOpen(true); }; const renderCellContent = (item: IdeaData, column: ColumnDef) => { const value = (item as any)[column.key]; switch (column.key) { case "idea_title": return ( {String(value)} ); case "idea_registration_date": return ( {formatDate(String(value))} ); case "idea_status": return ( {!!value ? String(value) : "-"} ); case "increased_revenue": return ( {formatCurrency(String(value || "0")).replace("ریال" , "")} ); case "details": return ( ); default: return ( {(value && String(value)) || "-"} ); } }; // Custom Vertical Bar Chart Component using shadcn/ui const VerticalBarChart = () => { if (loadingChart) { return (
{/* Chart title skeleton */}
{/* Chart area skeleton */}
{/* Y-axis labels */}
{Array.from({ length: 4 }).map((_, i) => (
))}
{/* Bars skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
{/* Bar */}
{/* X-axis label */}
))}
); } if (!chartData.length) { return (

وضعیت ایده ها

هیچ داده‌ای یافت نشد

); } // Prepare data for recharts const rechartData = chartData.map((item) => ({ status: item.idea_status, count: item.idea_status_count, fill: getChartStatusColor(item.idea_status), })); return ( toPersianDigits(value)} label={{ value: "تعداد برنامه ها" , angle: -90, position: "insideLeft", fill: "#94a3b8", fontSize: 11, offset: 0, dy: 0, style: { textAnchor: "middle" }, }} /> `${formatNumber(Math.round(v))}`} /> ); }; return (
{/* People Ranking Table */}

رتبه بندی نوآوران

رتبه ایده پرداز امتیاز {loadingPeople ? ( Array.from({ length: 10 }).map((_, index) => (
{Array.from({ length: 5 }).map((_, starIndex) => (
))}
)) ) : peopleRanking.length === 0 ? ( هیچ داده‌ای یافت نشد ) : ( peopleRanking.map((person) => (
{toPersianDigits(person.ranking)}
{person.full_name}
{Array.from({ length: 5 }).map((_, starIndex) => ( ))}
)) )}
کل افراد: {toPersianDigits(peopleRanking.length)}
{/* Main Ideas Table */}

لیست ایده ها

{columns.map((column) => ( {column.sortable ? ( ) : ( column.label )} ))} {loading ? ( Array.from({ length: 20 }).map((_, index) => ( {columns.map((column) => (
))} )) ) : ideas.length === 0 ? ( هیچ ایده‌ای یافت نشد ) : ( ideas.map((idea, index) => ( {columns.map((column) => ( {renderCellContent(idea, column)} ))} )) )}
{/* Infinite scroll trigger */}
{loadingMore && (
)}
{/* Footer */}
کل ایده‌ها: {toPersianDigits(actualTotalCount)}
{/* Chart Section */}
{loadingStats ? (
) : (
0 ? Math.round( (parseFloat( statsData?.registered_innovation_technology_idea || "0", ) / parseFloat( statsData ?.registered_innovation_technology_idea || "1", )) * 100, ) : 0, fill: "var(--color-green)", }, ]} startAngle={90} endAngle={ 90 + ((parseFloat( statsData ?.registered_innovation_technology_idea || "0", ) > 0 ? Math.round( (parseFloat( statsData ?.ongoing_innovation_technology_ideas || "0", ) / parseFloat( statsData ?.registered_innovation_technology_idea || "1", )) * 100, ) : 0) / 100) * 360 } innerRadius={35} outerRadius={55} >
ثبت شده :
{formatNumber( statsData ?.registered_innovation_technology_idea || "0", )}
در حال اجرا :
{formatNumber( statsData ?.ongoing_innovation_technology_ideas || "0", )}
)}
{loadingStats ? (
) : ( )}
{/* Details Dialog */} عنوان ایده: میکروکاتالیزورهای دما بالا {selectedIdea &&
{/* مشخصات ایده پردازان Section */}

مشخصات ایده پردازان

نام ایده پرداز:
{selectedIdea.full_name || "-"}
شماره پرسنلی:
{toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}
مدیریت:
{selectedIdea.management || "مدیریت توسعه"}
معاونت:
{selectedIdea.deputy || "توسعه"}
اعضای تیم:
{selectedIdea.innovator_team_members || "رضا حسین پور, محمد رضا شیاطی, محمد مددی"}
{/* مشخصات ایده Section */}

مشخصات ایده

تاریخ ثبت ایده:
{formatDate(selectedIdea.idea_registration_date) || "-"}
نوع نوآوری:
{selectedIdea.innovation_type || "-"}
اصالت ایده:
{selectedIdea.idea_originality || "-"}
محور ایده:
{selectedIdea.idea_axis || "-"}
{/* نتایج و خروجی ها Section */}

نتایج و خروجی ها

درآمد حاصل:
{formatNumber(selectedIdea.increased_revenue) || "-"} میلیون ریال
مقاله چاپ شده:
دانلود
پتنت ثبت شده:
دانلود
{/* شرح ایده Section */}

شرح ایده

{selectedIdea.idea_description || "-" }

{/* شرح وضعیت موجود ایده Section */}

شرح وضعیت موجود ایده

{selectedIdea.idea_current_status_description || "-" }

{/* منافع حاصل از ایده Section */}

منافع حاصل از ایده

{selectedIdea.idea_execution_benefits || "-" }

{/* بهبود های فرآیندی ایده Section */}

بهبود های فرآیندی ایده

{selectedIdea.process_improvements || "-" }

}
); }