import jalaali from "jalaali-js"; import { BrainCircuit, ChevronDown, ChevronUp, Database, Key, LoaderCircle, RefreshCw, Sprout, TrendingDown, TrendingUp, Zap, } from "lucide-react"; import moment from "moment-jalaali"; 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 } from "~/components/ui/card"; import { Checkbox } from "~/components/ui/checkbox"; import { CustomBarChart } from "~/components/ui/custom-bar-chart"; 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 { EventBus, formatCurrency, formatNumber } from "~/lib/utils"; import type { CalendarDate } from "~/types/util.type"; import { DashboardLayout } from "../layout"; moment.loadPersian({ usePersianDigits: true }); interface SortConfig { field: string; direction: "asc" | "desc"; } interface StatsCard { id: string; title: string; value: string; description?: string; icon: React.ReactNode; color: string; } // Raw API response interface for digital innovation metrics interface DigitalInnovationMetrics { count_innovation_digital_projects: string; increased_revenue: string; increased_revenue_percent: string; reduce_costs: string; reduce_costs_percent: string; reduce_energy_consumption: string; reduce_energy_consumption_percent: string; resource_productivity: string; resource_productivity_percent: string; average_project_score?: number; } // Normalized interface for digital innovation stats interface DigitalInnovationStats { // totalDigitalProjects: number; increasedRevenue: number; increasedRevenuePercent: number; reduceCosts: number; reduceCostsPercent: number; reduceEnergyConsumption: number; reduceEnergyConsumptionPercent: number; resourceProductivity: number; resourceProductivityPercent: number; avarageProjectScore: number; countInnovationDigitalProjects: number; } enum DigitalCardLabel { decreasCost = "کاهش هزینه‌ها", increaseRevenue = "افزایش درآمد", performance = "بهره‌وری منابع", decreaseEnergy = "کاهش مصرف انرژی", } enum projectStatus { propozal = "پروپوزال", contract = "پیشنویس قرارداد", inprogress = "در حال انجام", stop = "متوقف شده", mafasa = "مرحله مفاصا", finish = "پایان یافته", } interface ProcessInnovationData { WorkflowID: number; desired_strategy: string; digital_capability: string; digital_competence: string; digital_puberty_elements: string; innovation_cost_reduction: number | string; operational_plan: string; originality_digital_solution: string; project_description: string; project_no: string; project_rating: number | string; project_status: string; reduce_costs_percent: number; title: string; } interface HouseItem { index: number; color?: string; style?: string; } interface ListItem { label: string; development: number; house: HouseItem[]; } 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 DigitalInnovationPage() { const { jy } = jalaali.toJalaali(new Date()); 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 [date, setDate] = useState({ start: `${jy}/01/01`, end: `${jy}/12/30`, }); const [actualTotalCount, setActualTotalCount] = useState(0); const [statsLoading, setStatsLoading] = useState(false); const [rating, setRating] = useState([]); const [dialogInfo, setDialogInfo] = useState(); const [stats, setStats] = useState({ increasedRevenue: 0, increasedRevenuePercent: 0, reduceCosts: 0, reduceCostsPercent: 0, reduceEnergyConsumption: 0, reduceEnergyConsumptionPercent: 0, resourceProductivity: 0, resourceProductivityPercent: 0, avarageProjectScore: 0, countInnovationDigitalProjects: 0, }); const [sortConfig, setSortConfig] = useState({ field: "start_date", direction: "asc", }); const [selectedProjects, setSelectedProjects] = useState>( new Set() ); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); // const [avarage, setAvarage] = useState(0); const observerRef = useRef(null); const fetchingRef = useRef(false); const scrollTimeoutRef = useRef(null); const scrollContainerRef = useRef(null); // Selection handlers const handleSelectAll = () => { if (selectedProjects.size === projects.length) { setSelectedProjects(new Set()); } else { setSelectedProjects(new Set(projects.map((p: any) => p.project_no))); } }; const handleProjectDetails = (project: ProcessInnovationData) => { const model: ListItem = { label: `فرآیند-${project.WorkflowID}`, development: +project.project_rating, house: [], }; setRating([model]); setDialogInfo(project); setDetailsDialogOpen(true); }; // ...existing code... const statsCards: StatsCard[] = [ { id: "production-stops-prevention", title: DigitalCardLabel.decreasCost, value: formatNumber(stats.reduceCosts.toFixed?.(1) ?? stats.reduceCosts), description: "میلیون ریال کاهش یافته", icon: , color: "text-pr-green", }, { id: "bottleneck-removal", title: DigitalCardLabel.increaseRevenue, value: formatNumber(stats.increasedRevenue), description: "میلیون ریال افزایش یافته", icon: , color: "text-pr-green", }, { id: "currency-reduction", title: DigitalCardLabel.performance, value: formatNumber( stats.resourceProductivity.toFixed?.(0) ?? stats.resourceProductivity ), description: "هزار تن صرفه جوریی شده", icon: , color: "text-pr-green", }, { id: "frequent-failures-reduction", title: DigitalCardLabel.decreaseEnergy, value: formatNumber( stats.reduceEnergyConsumption.toFixed?.(1) ?? stats.reduceEnergyConsumption ), description: "مگاوات کاهش یافته", icon: , color: "text-pr-green", }, ]; const fetchTable = 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_no", "title", "project_status", "project_rating", "project_description", "digital_competence", "originality_digital_solution", "digital_puberty_elements", "digital_capability", "operational_plan", "desired_strategy", "innovation_cost_reduction", "reduce_costs_percent", ], Sorts: [[sortConfig.field, sortConfig.direction]], 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: ProcessInnovationData = JSON.parse(dataString); if (Array.isArray(parsedData)) { if (reset) { setProjects(parsedData); // calculateAverage(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 && !loadingMore && !fetchingRef.current) { setCurrentPage((prev) => prev + 1); } }, [hasMore, loading, loadingMore]); useEffect(() => { fetchTable(true); fetchTotalCount(); fetchStats(); }, [sortConfig, date]); useEffect(() => { EventBus.on("dateSelected", (date: CalendarDate) => { if (date) { setDate(date); } }); }, []); useEffect(() => { if (currentPage > 1) { fetchTable(false); } }, [currentPage]); // Infinite scroll observer with debouncing useEffect(() => { const scrollContainer = scrollContainerRef.current; const handleScroll = () => { if (!scrollContainer || !hasMore || loadingMore || fetchingRef.current) return; // Clear previous timeout if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } // Debounce scroll events scrollTimeoutRef.current = setTimeout(() => { const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; // Trigger load more when scrolled to 95% of the container 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", })); fetchTotalCount(date?.start, date?.end); fetchStats(date?.start, date?.end); setCurrentPage(1); setProjects([]); setHasMore(true); }; const fetchTotalCount = async (startDate?: string, endDate?: string) => { try { const response = await apiService.select({ ProcessName: "project", OutputFields: ["count(project_no)"], Conditions: [ ["type_of_innovation", "=", "نوآوری دیجیتال", "and"], ["start_date", ">=", date?.start || null, "and"], ["start_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]) { const count = parsedData[0].project_no_count || 0; 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({ innovation_digital_function: { start_date: date?.start || null, end_date: date?.end || null, }, }); // let payload: DigitalInnovationMetrics = raw?.data; // console.log("*-*-*-*" +payload); // if (typeof payload === "string") { // try { // payload = JSON.parse(payload).innovation_digital_function; // } catch {} // } let payload: DigitalInnovationMetrics | null = null; if (raw?.data) { try { // مرحله اول: data رو از string به object تبدیل کن const parsedData = JSON.parse(raw.data); // مرحله دوم: innovation_digital_function رو که خودش string هست parse کن const arr = JSON.parse(parsedData.innovation_digital_function); // مرحله سوم: اولین خانه آرایه رو بردار if (Array.isArray(arr) && arr.length > 0) { payload = arr[0]; } } catch (err) { console.error("Error parsing API response:", err); } } const parseNum = (v: unknown): number => { if (v == null) return 0; if (typeof v === "number") return v; if (typeof v === "string") { const cleaned = v.replace(/,/g, "").trim(); const n = parseFloat(cleaned); return isNaN(n) ? 0 : n; } return 0; }; const normalized: DigitalInnovationStats = { increasedRevenue: parseNum(payload?.increased_revenue), increasedRevenuePercent: parseNum(payload?.increased_revenue_percent), reduceCosts: parseNum(payload?.reduce_costs), reduceCostsPercent: parseNum(payload?.reduce_costs_percent), reduceEnergyConsumption: parseNum(payload?.reduce_energy_consumption), reduceEnergyConsumptionPercent: parseNum( payload?.reduce_energy_consumption_percent ), resourceProductivity: parseNum(payload?.resource_productivity), resourceProductivityPercent: parseNum( payload?.resource_productivity_percent ), avarageProjectScore: parseNum(payload?.average_project_score), countInnovationDigitalProjects: parseNum( payload?.count_innovation_digital_projects ), }; setActualTotalCount(normalized.countInnovationDigitalProjects); setStats(normalized); } catch (error) { console.error("Error fetching stats:", error); } finally { setStatsLoading(false); } }; // const handleRefresh = () => { // fetchingRef.current = false; // setCurrentPage(1); // setProjects([]); // setHasMore(true); // fetchTable(true); // fetchTotalCount(); // fetchStats(); // }; // const renderProgress = useMemo(() => { // const total = 10; // for (let i = 0; i < rating.length; i++) { // const currentElm = rating[i]; // currentElm.house = []; // const greenBoxes = Math.floor((total * currentElm.development) / 100); // const partialPercent = // (total * currentElm.development) / 100 - greenBoxes; // for (let j = 0; j < greenBoxes; j++) { // currentElm.house.push({ // index: j, // color: "!bg-emerald-400", // }); // } // if (partialPercent != 0 && greenBoxes != 10) // currentElm.house.push({ // index: greenBoxes + 1, // style: `linear-gradient( // to right, // oklch(76.5% 0.177 163.223) 0%, // oklch(76.5% 0.177 163.223) ${partialPercent * 100}%, // oklch(55.1% 0.027 264.364) ${partialPercent * 100}%, // oklch(55.1% 0.027 264.364) 100% // )`, // }); // } // }, [rating]); 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 renderCellContent = (item: any, column: any) => { const value = item[column.key as keyof ProcessInnovationData]; switch (column.key) { case "select": return ( handleSelectProject(item.project_no)} className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" /> ); case "details": return ( ); case "amount_currency_reduction": return ( {formatCurrency(String(value))} ); case "project_no": return ( {String(value)} ); case "title": return ( {String(value)} ); case "project_status": return (
{String(value)}
); case "project_rating": return ( {formatNumber(String(value))} ); case "reduce_prevention_production_stops": case "throat_removal": case "Reduce_rate_failure": return ( {formatNumber(String(value))} ); default: return {String(value) || "-"}; } }; return (
{/* Stats Cards */}
{/* Stats Grid */}
{loading ? // Loading skeleton for stats cards - matching new design Array.from({ length: 4 }).map((_, index) => (
)) : statsCards.map((card) => (

{card.title}

{card.icon}

{card.value}

{card.description}

))}
{/* Process Impacts Chart */} {/* */} {/* */}
{/* Data Table */}
{columns.map((column) => ( {column.key === "select" ? (
0 } onCheckedChange={handleSelectAll} className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" />
) : 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 */}
{loadingMore && (
)}
{/* Footer */}
کل پروژه‌ها:{" "} {formatNumber(actualTotalCount)}
میانگین:{" "} {formatNumber( ((stats.avarageProjectScore ?? 0) as number).toFixed?.(1) ?? 0 )}
{/*
کل پروژه ها : {formatNumber(actualTotalCount)}
میانگین :‌
{formatNumber( ((stats.avarageProjectScore ?? 0) as number).toFixed?.( 1 ) ?? 0 )}
*/}
{/* Project Details Dialog */} شرح پروژه
{dialogInfo?.title}

{dialogInfo?.project_description}

ویژگی های اصلی پروژه:
شایستگی دیجیتال:
{dialogInfo?.digital_capability}
اصالت راهکار دیجیتال:
{dialogInfo?.digital_competence}
المان های بلوغ دیجیتال:
{dialogInfo?.digital_puberty_elements}
توسعه قابلیت های دیجیتال:{" "}
{dialogInfo?.digital_capability}
برنامه های عملیاتی مرتبط:
{dialogInfo?.operational_plan}
استراتژی های مورد نظر:
{dialogInfo?.desired_strategy}
{/*
قابلیت شماره یک
*/} {/*
قابلیت شماره یک
*/}
کاهش هزینه ها
%{" "} {formatNumber( ( Math.round( dialogInfo?.reduce_costs_percent! * 100 ) / 100 ).toFixed(2) )} درصد به کل هزینه ها
{formatNumber(+dialogInfo?.innovation_cost_reduction!)} میلیون ریال
عنوان فرآیند درصد پیشرفت
{rating.map((el, index) => { return (
{el.label}
{Array.from({ length: 10 }, (_, i) => { return ( ); })}
); })}
); } export default DigitalInnovationPage;