import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent } from "~/components/ui/card"; 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"; interface ProjectData { WorkflowID: number; ValueP1215S1887ValueID: number; ValueP1215S1887StageID: number; project_no: string; importance_project: string; title: string; strategic_theme: string; value_technology_and_innovation: string; type_of_innovation: string; innovation: string; person_executing: string; excellent_observer: string; observer: string; moderator: string; start_date: string; end_date: string | null; done_date: string | null; approved_budget: string; budget_spent: string; } interface SortConfig { field: string; // uses column.key direction: "asc" | "desc"; } type ColumnDef = { key: string; // UI key label: string; sortable: boolean; width: string; apiField?: string; // API field name; defaults to key computed?: boolean; // not fetched from API }; // const columns: ColumnDef[] = [ // { key: "idea_title", label: "عنوان پروژه", sortable: true, width: "200px" }, // { // key: "idea_status", // label: "میزان اهمیت", // sortable: true, // width: "150px", // }, // { // key: "strategic_theme", // label: "مضمون راهبردی", // sortable: true, // width: "160px", // }, // { // key: "value_technology_and_innovation", // label: "ارزش فناوری و نوآوری", // sortable: true, // width: "200px", // }, // { // key: "type_of_innovation", // label: "انواع نوآوری", // sortable: true, // width: "140px", // }, // { key: "innovation", label: "میزان نوآوری", sortable: true, width: "120px" }, // { // key: "person_executing", // label: "مسئول اجرا", // sortable: true, // width: "140px", // }, // { // key: "excellent_observer", // label: "ناطر عالی", // sortable: true, // width: "140px", // }, // { key: "observer", label: "ناظر پروژه", sortable: true, width: "140px" }, // { key: "moderator", label: "مجری", sortable: true, width: "140px" }, // { // key: "executive_phase", // label: "فاز اجرایی", // sortable: true, // width: "140px", // }, // { key: "start_date", label: "تاریخ شروع", sortable: true, width: "120px" }, // { // key: "remaining_time", // label: "زمان باقی مانده", // sortable: true, // width: "140px", // computed: true, // }, // { // key: "end_date", // label: "تاریخ پایان (برنامه‌ریزی)", // sortable: true, // width: "160px", // }, // { // key: "renewed_duration", // label: "مدت زمان تمدید", // sortable: true, // width: "140px", // }, // { // key: "done_date", // label: "تاریخ پایان (واقعی)", // sortable: true, // width: "160px", // }, // { // key: "deviation_from_program", // label: "متوسط انحراف برنامه‌ای", // sortable: true, // width: "160px", // }, // { // key: "approved_budget", // label: "بودجه مصوب", // sortable: true, // width: "150px", // }, // { // key: "budget_spent", // label: "بودجه صرف شده", // sortable: true, // width: "150px", // }, // { // key: "cost_deviation", // label: "متوسط انحراف هزینه‌ای", // sortable: true, // width: "160px", // }, // ]; const columns: ColumnDef[] = [ { key: "idea_title", label: "عنوان ایده", sortable: true, width: "200px" }, { key: "idea_status", label: "وضعیت ایده", sortable: true, width: "260px", }, { key: "idea_axis", label: "محور ایده", sortable: true, width: "160px" }, { key: "idea_current_status_description", label: "توضیح وضعیت فعلی ایده", sortable: true, width: "220px", }, { key: "idea_description", label: "شرح ایده", sortable: true, width: "200px", }, { key: "full_name", label: "نام و نام خانوادگی", sortable: true, width: "160px", }, { key: "personnel_number", label: "شماره پرسنلی", sortable: true, width: "140px", }, { key: "innovator_team_members", label: "اعضای تیم نوآور", sortable: true, width: "200px", }, { key: "idea_registration_date", label: "تاریخ ثبت ایده", sortable: true, width: "160px", }, { key: "deputy", label: "معاونت مربوطه", sortable: true, width: "160px" }, { key: "management", label: "مدیریت", sortable: true, width: "140px" }, { key: "idea_execution_benefits", label: "مزایای اجرای ایده", sortable: true, width: "220px", }, { key: "innovation_type", label: "نوع نوآوری", sortable: true, width: "160px", }, { key: "idea_originality", label: "میزان اصالت ایده", sortable: true, width: "160px", }, { key: "idea_income", label: "درآمد حاصل از ایده", sortable: true, width: "160px", }, { key: "process_improvements", label: "بهبودهای فرآیندی", sortable: true, width: "180px", }, ]; export function ManageIdeasTechPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(25); const [hasMore, setHasMore] = useState(true); const [totalCount, setTotalCount] = useState(0); const [actualTotalCount, setActualTotalCount] = useState(0); const [sortConfig, setSortConfig] = useState({ field: "idea_title", direction: "asc", }); const observerRef = useRef(null); const fetchingRef = useRef(false); const fetchProjects = async (reset = false) => { // Prevent concurrent API calls if (fetchingRef.current) { return; } try { fetchingRef.current = true; if (reset) { setLoading(true); setCurrentPage(1); } else { setLoadingMore(true); } const pageToFetch = reset ? 1 : currentPage; const fetchableColumns = columns.filter((c) => !c.computed); const outputFields = fetchableColumns.map((c) => c.apiField ?? c.key); const sortCol = columns.find((c) => c.key === sortConfig.field); const sortField = sortCol?.computed ? undefined : (sortCol?.apiField ?? sortCol?.key); const response = await apiService.select({ ProcessName: "idea", OutputFields: outputFields, Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, Sorts: sortField ? [[sortField, sortConfig.direction]] : [], Conditions: [], }); if (response.state === 0) { // Parse the JSON string from the API response 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); } // Check if there are more items to load 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 (!loadingMore && hasMore && !loading) { setCurrentPage((prev) => prev + 1); } }, [loadingMore, hasMore, loading]); useEffect(() => { fetchProjects(true); fetchTotalCount(); }, [sortConfig]); useEffect(() => { if (currentPage > 1) { fetchProjects(false); } }, [currentPage]); // Infinite scroll observer useEffect(() => { const scrollContainer = document.querySelector(".overflow-auto"); const handleScroll = () => { if (!scrollContainer || !hasMore || loadingMore) return; const { scrollTop, scrollHeight, clientHeight } = scrollContainer; const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; // Trigger load more when scrolled to 90% of the container if (scrollPercentage >= 0.9) { loadMore(); } }; if (scrollContainer) { scrollContainer.addEventListener("scroll", handleScroll); } return () => { if (scrollContainer) { scrollContainer.removeEventListener("scroll", handleScroll); } }; }, [loadMore, hasMore, loadingMore]); const handleSort = (field: string) => { fetchingRef.current = false; // Reset fetching state on sort setSortConfig((prev) => ({ field, direction: prev.field === field && prev.direction === "asc" ? "desc" : "asc", })); setCurrentPage(1); setProjects([]); 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 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); }; // ----- Jalali <-> Gregorian conversion helpers (lightweight, local) ----- const isJalaliDateString = (raw: string): boolean => { return /^(\d{4})[\/](\d{1,2})[\/](\d{1,2})$/.test(raw.trim()); }; const div = (a: number, b: number) => ~~(a / b); const jalaliToJDN = (jy: number, jm: number, jd: number): number => { jy = jy - (jy >= 0 ? 474 : 473); const cycle = 1029983; const yCycle = 474 + (jy % 2820); const jdn = jd + (jm <= 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186) + div((yCycle * 682 - 110) as number, 2816) + (yCycle - 1) * 365 + div(jy, 2820) * cycle + (1948320 - 1); return jdn; }; const jdnToGregorian = (jdn: number): [number, number, number] => { let j = jdn + 32044; const g = div(j, 146097); const dg = j % 146097; const c = div((div(dg, 36524) + 1) * 3, 4); const dc = dg - c * 36524; const b = div(dc, 1461); const db = dc % 1461; const a = div((div(db, 365) + 1) * 3, 4); const da = db - a * 365; const y = g * 400 + c * 100 + b * 4 + a; const m = div(da * 5 + 308, 153) - 2; const d = da - div((m + 4) * 153, 5) + 122; const year = y - 4800 + div(m + 2, 12); const month = ((m + 2) % 12) + 1; const day = d + 1; return [year, month, day]; }; const jalaliToGregorianDate = (jy: number, jm: number, jd: number): Date => { const jdn = jalaliToJDN(jy, jm, jd); const [gy, gm, gd] = jdnToGregorian(jdn); return new Date(gy, gm - 1, gd); }; const parseToDate = (value: string | null): Date | null => { if (!value) return null; const raw = String(value).trim(); if (isJalaliDateString(raw)) { const [jy, jm, jd] = raw.split("/").map((s) => Number(s)); if ([jy, jm, jd].some((n) => Number.isNaN(n))) return null; return jalaliToGregorianDate(jy, jm, jd); } const tryDate = new Date(raw); return Number.isNaN(tryDate.getTime()) ? null : tryDate; }; const getTodayMidnight = (): Date => { const now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()); }; const calculateRemainingDays = (end: string | null): number | null => { if (!end) return null; // if either missing const endDate = parseToDate(end); if (!endDate) return null; const today = getTodayMidnight(); const MS_PER_DAY = 24 * 60 * 60 * 1000; const diff = Math.round((endDate.getTime() - today.getTime()) / MS_PER_DAY); return diff; }; const formatDate = (dateString: string | null) => { if (!dateString || dateString === "null" || dateString.trim() === "") { return "-"; } // If API already returns Jalali like 1404/05/30, just convert digits 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}`); } // Otherwise, try to parse and render Persian calendar 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 "-"; } }; const phaseColors: Record = { "تحقیق و توسعه": "#FFD700", // Yellow آزمایش: "#1E90FF", // Blue تولید: "#32CD32", // Green default: "#ccc", // Fallback gray }; const getImportanceColor = (importance: string) => { switch (importance?.toLowerCase()) { case "تایید شده": return "var(--success)"; // سبز case "در حال بررسی": return "var(--info)"; // آبی case "رد شده": return "var(--destructive)"; // قرمز case "اجرا شده": return "var(--warning)"; // زرد/نارنجی default: return "var(--muted)"; // خاکستری پیش‌فرض } }; // idea_income , idea_registration_date , idea_originality , personnel_number const renderCellContent = (item: ProjectData, column: ColumnDef) => { const apiField = column.apiField ?? column.key; const value = (item as any)[apiField]; switch (column.key) { case "remaining_time": { const days = calculateRemainingDays(item.end_date); if (days == null) { return -; } const color = days > 0 ? "var(--success)" : days < 0 ? "var(--destructive)" : undefined; return ( روز {toPersianDigits(days)} ); } case "idea_income": return ( {formatCurrency(String(value))} ); case "personnel_number": // case "idea_originality": return ( {toPersianDigits(value as any)}{" "} ); case "idea_registration_date": return ( {formatDate(String(value))} ); case "project_no": return ( {String(value)} ); case "idea_title": return {String(value)}; case "idea_status": return ( {String(value)} ); default: return ( {(value && String(value)) || "-"} ); } }; const totalPages = Math.ceil(totalCount / pageSize); return (
{/* Data Table */}
{columns.map((column) => ( {column.sortable ? ( ) : ( column.label )} ))} {loading ? ( // Skeleton loading rows (compact) Array.from({ length: 20 }).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)}
); }