import { ChevronDown, ChevronUp, Download, Hexagon, RefreshCw, Star, TrendingUp, } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; import { Bar, BarChart, CartesianGrid, Label, LabelList, PolarGrid, PolarRadiusAxis, RadialBar, RadialBarChart, ResponsiveContainer, XAxis, YAxis, } from "recharts"; import { BaseCard } from "~/components/ui/base-card"; import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; import { ChartContainer, type ChartConfig } from "~/components/ui/chart"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { MetricCard } from "~/components/ui/metric-card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table"; import { useStoredDate } from "~/hooks/useStoredDate"; import apiService from "~/lib/api"; import { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import type { CalendarDate } from "~/types/util.type"; import { DashboardLayout } from "../layout"; interface 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" }, ]; // Memoized Vertical Bar Chart Component const VerticalBarChart = memo<{ chartData: IdeaStatusData[]; loadingChart: boolean; chartConfig: ChartConfig; getChartStatusColor: (status: string) => string; toPersianDigits: (input: string | number) => string; formatNumber: (value: number) => string; }>( ({ chartData, loadingChart, chartConfig, getChartStatusColor, toPersianDigits, formatNumber, }) => { 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 = useMemo( () => chartData.map((item) => ({ status: item.idea_status, count: item.idea_status_count, fill: getChartStatusColor(item.idea_status), })), [chartData, getChartStatusColor] ); return ( toPersianDigits(value)} label={{ value: "تعداد برنامه ها", angle: -90, position: "insideLeft", fill: "#94a3b8", fontSize: 11, offset: 0, dy: 0, style: { textAnchor: "middle" }, }} /> `${formatNumber(Math.round(v))}`} /> ); } ); const MemoizedVerticalBarChart = VerticalBarChart; 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", }); const [date, setDate] = useStoredDate(); // 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: [ ["idea_registration_date", ">=", date?.start || null, "and"], ["idea_registration_date", "<=", date?.end || null], ], }); 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(() => { const handler = (date: CalendarDate) => { if (date) setDate(date); }; EventBus.on("dateSelected", handler); return () => { EventBus.off("dateSelected", handler); }; }, []); useEffect(() => { if (date.end && date.start) { fetchIdeas(true); fetchTotalCount(); fetchPeopleRanking(); fetchChartData(); fetchStatsData(); } }, [sortConfig, date]); 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: [ ["idea_registration_date", ">=", date?.start || null, "and"], ["idea_registration_date", "<=", date?.end || null], ], }); 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"], Conditions: [ ["idea_registration_date", ">=", date?.start || null, "and"], ["idea_registration_date", "<=", date?.end || null], ], }); 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"], Conditions: [ ["idea_registration_date", ">=", date?.start || null, "and"], ["idea_registration_date", "<=", date?.end || null], ], }); 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: { start_date: date?.start || null, end_date: date?.end || null, }, }); 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 = useCallback((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 = useMemo( () => ({ count: { label: "تعداد", }, }), [] ); // Color palette for idea status // Specific colors for idea statuses const getChartStatusColor = useCallback((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)) || "-"} ); } }; 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 */}
{/* Footer */}
کل ایده‌ها: {toPersianDigits(actualTotalCount)}
{loadingMore && (
)}
{/* 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 || "-"}

)}
); }