From e10c25fc3e81e009f5065129e17088f415728aaa Mon Sep 17 00:00:00 2001 From: Saeed Abadiyan Date: Sun, 28 Sep 2025 19:07:15 +0330 Subject: [PATCH 1/3] refactor: the component ,feat: add tables in componente --- .../mange-ideas-tech-page.tsx | 1049 +++++++++-------- app/components/dashboard/sidebar.tsx | 13 +- app/components/ui/table.tsx | 2 +- app/routes/ecosystem.tsx | 2 +- 4 files changed, 555 insertions(+), 511 deletions(-) diff --git a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx index e5500f5..aef763f 100644 --- a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx +++ b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx @@ -1,8 +1,15 @@ -import { ChevronDown, ChevronUp, RefreshCw } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { ChevronDown, ChevronUp, RefreshCw, Eye, Star } 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, @@ -15,240 +22,78 @@ 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 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 SortConfig { - field: string; // uses column.key + field: string; direction: "asc" | "desc"; } type ColumnDef = { - key: string; // UI key + key: string; 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", - }, + { 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 [projects, setProjects] = useState([]); + const [ideas, setIdeas] = useState([]); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const [pageSize] = useState(25); + 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); + const observerRef = useRef(null); const fetchingRef = useRef(false); const scrollTimeoutRef = useRef(null); - const scrollContainerRef = useRef(null); + const scrollContainerRef = useRef(null); - const fetchProjects = async (reset = false) => { - // Prevent concurrent API calls + const fetchIdeas = async (reset = false) => { if (fetchingRef.current) { return; } @@ -265,73 +110,80 @@ export function ManageIdeasTechPage() { 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, + 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: sortField ? [[sortField, sortConfig.direction]] : [], + Sorts: [[sortConfig.field, 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); + setIdeas(parsedData); setTotalCount(parsedData.length); } else { - setProjects((prev) => [...prev, ...parsedData]); + setIdeas((prev) => [...prev, ...parsedData]); setTotalCount((prev) => prev + parsedData.length); } - - // Check if there are more items to load setHasMore(parsedData.length === pageSize); } else { if (reset) { - setProjects([]); + setIdeas([]); setTotalCount(0); } setHasMore(false); } } catch (parseError) { - console.error("Error parsing project data:", parseError); + console.error("Error parsing idea data:", parseError); if (reset) { - setProjects([]); + setIdeas([]); setTotalCount(0); } setHasMore(false); } } else { if (reset) { - setProjects([]); + setIdeas([]); setTotalCount(0); } setHasMore(false); } } else { - toast.error(response.message || "خطا در دریافت اطلاعات پروژه‌ها"); + toast.error(response.message || "خطا در دریافت اطلاعات ایده‌ها"); if (reset) { - setProjects([]); + setIdeas([]); setTotalCount(0); } setHasMore(false); } } catch (error) { - console.error("Error fetching projects:", error); - toast.error("خطا در دریافت اطلاعات پروژه‌ها"); + console.error("Error fetching ideas:", error); + toast.error("خطا در دریافت اطلاعات ایده‌ها"); if (reset) { - setProjects([]); + setIdeas([]); setTotalCount(0); } setHasMore(false); @@ -349,13 +201,14 @@ export function ManageIdeasTechPage() { }, [hasMore, loading, loadingMore]); useEffect(() => { - fetchProjects(true); + fetchIdeas(true); fetchTotalCount(); + fetchPeopleRanking(); }, [sortConfig]); useEffect(() => { if (currentPage > 1) { - fetchProjects(false); + fetchIdeas(false); } }, [currentPage]); @@ -366,17 +219,14 @@ export function ManageIdeasTechPage() { 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(); } @@ -398,14 +248,14 @@ export function ManageIdeasTechPage() { }, [loadMore, hasMore, loadingMore]); const handleSort = (field: string) => { - fetchingRef.current = false; // Reset fetching state on sort + fetchingRef.current = false; setSortConfig((prev) => ({ field, direction: prev.field === field && prev.direction === "asc" ? "desc" : "asc", })); setCurrentPage(1); - setProjects([]); + setIdeas([]); setHasMore(true); }; @@ -435,6 +285,71 @@ export function ManageIdeasTechPage() { } }; + 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 toPersianDigits = (input: string | number): string => { const str = String(input); const map: Record = { @@ -452,85 +367,11 @@ export function ManageIdeasTechPage() { 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); @@ -541,7 +382,6 @@ export function ManageIdeasTechPage() { return toPersianDigits(`${y}/${mm}/${dd}`); } - // Otherwise, try to parse and render Persian calendar try { const parsed = new Date(raw); if (isNaN(parsed.getTime())) return "-"; @@ -555,228 +395,435 @@ export function ManageIdeasTechPage() { } }; - const phaseColors: Record = { - "تحقیق و توسعه": "#FFD700", // Yellow - آزمایش: "#1E90FF", // Blue - تولید: "#32CD32", // Green - default: "#ccc", // Fallback gray + // Color palette for idea status + 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 getImportanceColor = (importance: string) => { - switch (importance?.toLowerCase()) { - case "تایید شده": - return "var(--color-pr-green)"; // green - case "در حال بررسی": - return "var(--info)"; // آبی - case "رد شده": - return "#F76276"; // red - case "اجرا شده": - return "#69C8EA"; - default: - return "var(--muted)"; // خاکستری پیش‌فرض - } + const handleShowDetails = (idea: IdeaData) => { + setSelectedIdea(idea); + setIsDetailsOpen(true); }; - // 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]; + const renderCellContent = (item: IdeaData, column: ColumnDef) => { + const value = (item as any)[column.key]; 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; + case "idea_title": return ( - - روز {toPersianDigits(days)} - - ); - } - case "idea_income": - return ( - - {formatCurrency(String(value))} - - ); - case "personnel_number": - // case "idea_originality": - return ( - - {toPersianDigits(value as any)}{" "} - + {String(value)} ); 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)} - + + + {!!value ? String(value) : "-"} + + + ); + case "increased_revenue": + return ( + + {formatCurrency(String(value || "0")).replace("ریال" , "")} + + ); + case "details": + return ( + ); 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) => ( - -
-
-
-
- - ))} +
+
+ {/* People Ranking Table */} +
+

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

+ + +
+
+ + + + رتبه + + + ایده پرداز + + + امتیاز + - )) - ) : projects.length === 0 ? ( - - - - هیچ پروژه‌ای یافت نشد - - - - ) : ( - projects.map((project, index) => ( - - {columns.map((column) => ( - - {renderCellContent(project, column)} + + + {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) => ( + + ))} +
+
+
+ )) + )} + + +
- {/* Infinite scroll trigger */} -
- {loadingMore && ( -
-
- - +
+
+ کل افراد: {toPersianDigits(peopleRanking.length)}
- )} -
- - - {/* Footer */} -
-
- کل پروژه‌ها: {formatNumber(actualTotalCount)} -
+ +
- + + {/* 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)} +
+
+
+
+
+ + {/* Details Dialog */} + + + + + جزئیات ایده: {selectedIdea?.idea_title} + + + + {selectedIdea && ( +
+
+
+
+ +

{selectedIdea.full_name || "-"}

+
+ +
+ +

{toPersianDigits(selectedIdea.personnel_number) || "-"}

+
+ +
+ +

{selectedIdea.management || "-"}

+
+ +
+ +

{selectedIdea.deputy || "-"}

+
+ +
+ +

{selectedIdea.innovation_type || "-"}

+
+ +
+ +

{selectedIdea.idea_originality || "-"}

+
+ +
+ +

{selectedIdea.idea_axis || "-"}

+
+
+ +
+
+ +

{selectedIdea.innovator_team_members || "-"}

+
+ +
+ +

+ {selectedIdea.idea_description || "-"} +

+
+ +
+ +

+ {selectedIdea.idea_current_status_description || "-"} +

+
+ +
+ +

+ {selectedIdea.idea_execution_benefits || "-"} +

+
+ +
+ +

+ {selectedIdea.process_improvements || "-"} +

+
+ +
+ +

+ {formatCurrency(selectedIdea.increased_revenue)} +

+
+
+
+
+ )} +
+
); diff --git a/app/components/dashboard/sidebar.tsx b/app/components/dashboard/sidebar.tsx index d4f1aa3..7f237e6 100644 --- a/app/components/dashboard/sidebar.tsx +++ b/app/components/dashboard/sidebar.tsx @@ -8,6 +8,9 @@ import { Radar, Settings, Star, + Workflow, + DiscAlbum, + LucideLightbulb } from "lucide-react"; import React, { useState } from "react"; import { Link, useLocation } from "react-router"; @@ -95,16 +98,10 @@ const menuItems: MenuItem[] = [ { id: "ideas", label: "ایده‌های فناوری و نوآوری", - icon: House, + icon: LucideLightbulb, href: "/dashboard/manage-ideas-tech", }, - { - id: "top-innovations", - label: "نوآور برتر", - icon: Star, - href: "/dashboard/top-innovations", - }, - { + { id: "strategic-alignment", label: "میزان انطباق راهبردی", icon: null, diff --git a/app/components/ui/table.tsx b/app/components/ui/table.tsx index 7aa9a12..6626be6 100644 --- a/app/components/ui/table.tsx +++ b/app/components/ui/table.tsx @@ -4,7 +4,7 @@ import { cn } from "~/lib/utils" interface TableProps extends React.HTMLAttributes { containerClassName?: string - containerRef?: React.RefObject + containerRef?: React.RefObject } const Table = React.forwardRef( diff --git a/app/routes/ecosystem.tsx b/app/routes/ecosystem.tsx index 87009ca..696ced1 100644 --- a/app/routes/ecosystem.tsx +++ b/app/routes/ecosystem.tsx @@ -70,7 +70,7 @@ export default function EcosystemPage() { return ( -
+
-- 2.46.0.windows.1 From 67815aec2dc75d69eb2cbad5f4b3532be8e5908b Mon Sep 17 00:00:00 2001 From: Saeed Abadiyan Date: Sat, 4 Oct 2025 01:54:21 +0330 Subject: [PATCH 2/3] update the ideas page --- .../digital-innovation-page.tsx | 2 +- .../mange-ideas-tech-page.tsx | 639 +++++++++++++++--- .../product-innovation-page.tsx | 10 +- 3 files changed, 546 insertions(+), 105 deletions(-) diff --git a/app/components/dashboard/project-management/digital-innovation-page.tsx b/app/components/dashboard/project-management/digital-innovation-page.tsx index c5ebd2f..1b5b35b 100644 --- a/app/components/dashboard/project-management/digital-innovation-page.tsx +++ b/app/components/dashboard/project-management/digital-innovation-page.tsx @@ -548,7 +548,7 @@ export function DigitalInnovationPage() { index: greenBoxes + 1, style: `linear-gradient( to right, - oklch(76.5% 0.177 163.223) 0%, + 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% diff --git a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx index aef763f..12aba32 100644 --- a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx +++ b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, ChevronUp, RefreshCw, Eye, Star } from "lucide-react"; +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"; @@ -21,6 +21,18 @@ import { 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 "~/components/ui/label"; +import { MetricCard } from "~/components/ui/metric-card"; + + interface IdeaData { idea_title: string; @@ -48,6 +60,18 @@ interface PersonRanking { 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"; @@ -88,6 +112,14 @@ export function ManageIdeasTechPage() { 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); @@ -204,6 +236,8 @@ export function ManageIdeasTechPage() { fetchIdeas(true); fetchTotalCount(); fetchPeopleRanking(); + fetchChartData(); + fetchStatsData(); }, [sortConfig]); useEffect(() => { @@ -350,6 +384,68 @@ export function ManageIdeasTechPage() { } }; + 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 = { @@ -395,7 +491,30 @@ export function ManageIdeasTechPage() { } }; + // 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 @@ -469,7 +588,7 @@ export function ManageIdeasTechPage() { variant="ghost" size="sm" onClick={() => handleShowDetails(item)} - className="underline text-pr-green underline-offset-4 text-sm hover:bg-emerald-500/20" + className="underline text-pr-green underline-offset-4 text-sm hover:bg-pr-green/20" > جزئیات بیشتر ); @@ -482,9 +601,104 @@ export function ManageIdeasTechPage() { } }; + // Custom Vertical Bar Chart Component using shadcn/ui + const VerticalBarChart = () => { + if (loadingChart) { + return ( +
+
+
+
+ ); + } + + 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 */}
@@ -588,7 +802,7 @@ export function ManageIdeasTechPage() {
@@ -701,127 +915,354 @@ export function ManageIdeasTechPage() { + {/* Chart Section */} + + + +
+ +
+ + 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", + )} +
+
+
+
+
+ + +
{/* Details Dialog */} - - - - جزئیات ایده: {selectedIdea?.idea_title} + + + + عنوان ایده: میکروکاتالیزورهای دما بالا - {selectedIdea && ( -
-
-
-
- -

{selectedIdea.full_name || "-"}

+ {selectedIdea &&
+
+ {/* مشخصات ایده پردازان Section */} +
+

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

+
+
+
+ + نام ایده پرداز: +
+ {selectedIdea.full_name || "-"}
- -
- -

{toPersianDigits(selectedIdea.personnel_number) || "-"}

+
+
+ + شماره پرسنلی: +
+ {toPersianDigits(selectedIdea.personnel_number) || "۱۳۰۶۵۸۰۶"}
- -
- -

{selectedIdea.management || "-"}

+
+
+ + مدیریت: +
+ {selectedIdea.management || "مدیریت توسعه"}
- -
- -

{selectedIdea.deputy || "-"}

+
+
+ + معاونت: +
+ {selectedIdea.deputy || "توسعه"}
- -
- -

{selectedIdea.innovation_type || "-"}

-
- -
- -

{selectedIdea.idea_originality || "-"}

-
- -
- -

{selectedIdea.idea_axis || "-"}

+
+
+ + اعضای تیم: +
+ + {selectedIdea.innovator_team_members || "رضا حسین پور, محمد رضا شیاطی, محمد مددی"} +
+
-
-
- -

{selectedIdea.innovator_team_members || "-"}

+ {/* مشخصات ایده Section */} +
+

+ مشخصات ایده +

+
+
+
+ + تاریخ ثبت ایده: +
+ {formatDate(selectedIdea.idea_registration_date) || "-"}
- -
- -

- {selectedIdea.idea_description || "-"} -

+
+
+ + نوع نوآوری: +
+ {selectedIdea.innovation_type || "-"}
- -
- -

- {selectedIdea.idea_current_status_description || "-"} -

+
+
+ + اصالت ایده: +
+ {selectedIdea.idea_originality || "-"}
- -
- -

- {selectedIdea.idea_execution_benefits || "-"} -

+
+
+ + محور ایده: +
+ {selectedIdea.idea_axis || "-"}
+
+
+ {/* نتایج و خروجی ها Section */} +
+

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

+
+
+
+ + درآمد حاصل: +
+ {formatNumber(selectedIdea.increased_revenue) || "-"} + + میلیون ریال -
- -

- {selectedIdea.process_improvements || "-"} -

+ +
+
+
+ + مقاله چاپ شده: +
+ + -
- -

- {formatCurrency(selectedIdea.increased_revenue)} -

+ دانلود + +
+
+
+ + پتنت ثبت شده: +
+ + + دانلود +
- )} +
+ + {/* شرح ایده Section */} +
+

+ شرح ایده +

+
+

+ {selectedIdea.idea_description || + "-" + } +

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

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

+
+

+ {selectedIdea.idea_current_status_description || + "-" + } +

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

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

+
+

+ {selectedIdea.idea_execution_benefits || + "-" + } +

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

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

+
+

+ {selectedIdea.process_improvements || + "-" + } +

+
+
+ +
+
}
diff --git a/app/components/dashboard/project-management/product-innovation-page.tsx b/app/components/dashboard/project-management/product-innovation-page.tsx index 092e091..c1c8679 100644 --- a/app/components/dashboard/project-management/product-innovation-page.tsx +++ b/app/components/dashboard/project-management/product-innovation-page.tsx @@ -616,22 +616,22 @@ export function ProductInnovationPage() { size="sm" onClick={() => { handleProjectDetails(item)}} - className="text-emerald-400 underline underline-offset-4 font-ligth text-base hover:bg-emerald-500/20 p-2 h-auto" + className="text-emerald-400 underline underline-offset-4 font-ligth text-sm hover:bg-emerald-500/20 p-2 h-auto" > جزئیات بیشتر ); case "project_no": return ( - + {String(value)} ); case "title": - return {String(value)}; + return {String(value)}; case "project_status": return ( -
+
); default: - return {String(value) || "-"}; + return {String(value) || "-"}; } }; -- 2.46.0.windows.1 From 39e1ae4e814accf8657c2558a15c8a01b1db5a0e Mon Sep 17 00:00:00 2001 From: Saeed Abadiyan Date: Sat, 4 Oct 2025 02:21:11 +0330 Subject: [PATCH 3/3] add loading in graph ,also fix the graph --- .../mange-ideas-tech-page.tsx | 79 ++++++- app/components/ecosystem/info-panel.tsx | 8 +- app/components/ecosystem/network-graph.tsx | 28 ++- app/routes/ecosystem.tsx | 214 +++++++++++------- 4 files changed, 223 insertions(+), 106 deletions(-) diff --git a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx index 12aba32..e451903 100644 --- a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx +++ b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx @@ -29,7 +29,7 @@ import { } 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 "~/components/ui/label"; +import { Label } from "recharts" import { MetricCard } from "~/components/ui/metric-card"; @@ -605,9 +605,34 @@ export function ManageIdeasTechPage() { 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 */} +
+
+ ))} +
+
); } @@ -921,6 +946,23 @@ style: { textAnchor: "middle" },
+ {loadingStats ? ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ )} - + {loadingStats ? ( +
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : ( + + )}
diff --git a/app/components/ecosystem/info-panel.tsx b/app/components/ecosystem/info-panel.tsx index 89c547d..c8fcf8a 100644 --- a/app/components/ecosystem/info-panel.tsx +++ b/app/components/ecosystem/info-panel.tsx @@ -256,7 +256,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) { {Array.from({ length: 4 }).map((_, i) => (
-
+
{/* Bar Chart Skeleton */} @@ -362,7 +362,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) { {Array.from({ length: 4 }).map((_, i) => (
-
+
diff --git a/app/components/ecosystem/network-graph.tsx b/app/components/ecosystem/network-graph.tsx index ac04955..15b7e44 100644 --- a/app/components/ecosystem/network-graph.tsx +++ b/app/components/ecosystem/network-graph.tsx @@ -43,6 +43,7 @@ export interface CompanyDetails { export interface NetworkGraphProps { onNodeClick?: (node: CompanyDetails) => void; + onLoadingChange?: (loading: boolean) => void; } function parseApiResponse(raw: any): any[] { @@ -58,7 +59,7 @@ function isBrowser(): boolean { return typeof window !== "undefined"; } -export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { +export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps) { const svgRef = useRef(null); const [nodes, setNodes] = useState([]); const [links, setLinks] = useState([]); @@ -441,6 +442,19 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { if (d.isCenter) return; if (onNodeClick && d.stageid) { + // Open dialog immediately with basic info + const basicDetails: CompanyDetails = { + id: d.id, + label: d.label, + category: d.category, + stageid: d.stageid, + fields: [], + }; + onNodeClick(basicDetails); + + // Start loading + onLoadingChange?.(true); + try { const res = await callAPI(d.stageid); const responseData = JSON.parse(res.data); @@ -473,14 +487,10 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { onNodeClick(companyDetails); } catch (error) { console.error("Failed to fetch company details:", error); - const basicDetails: CompanyDetails = { - id: d.id, - label: d.label, - category: d.category, - stageid: d.stageid, - fields: [], - }; - onNodeClick(basicDetails); + // Keep the basic details already shown + } finally { + // Stop loading + onLoadingChange?.(false); } } }); diff --git a/app/routes/ecosystem.tsx b/app/routes/ecosystem.tsx index 696ced1..ff96122 100644 --- a/app/routes/ecosystem.tsx +++ b/app/routes/ecosystem.tsx @@ -56,10 +56,20 @@ function handleValue(val: any): any { export default function EcosystemPage() { const [selectedCompany, setSelectedCompany] = React.useState(null); + const [isDialogLoading, setIsDialogLoading] = React.useState(false); const { token } = useAuth(); const closeDialog = () => { setSelectedCompany(null); + setIsDialogLoading(false); + }; + + const handleNodeClick = (company: CompanyDetails) => { + setSelectedCompany(company); + }; + + const handleLoadingChange = (loading: boolean) => { + setIsDialogLoading(loading); }; // Construct image URL @@ -79,7 +89,7 @@ export default function EcosystemPage() {
- +
@@ -91,7 +101,7 @@ export default function EcosystemPage() { open={!!selectedCompany} onOpenChange={(open) => !open && closeDialog()} > - + معرفی @@ -99,98 +109,136 @@ export default function EcosystemPage() { -
- {/* Right Column - Description */} -
- {/* Company Image */} -
- {selectedCompany?.label || ""} - {selectedCompany?.stageid && token?.accessToken ? ( - {selectedCompany?.label { - // Hide image and show fallback on error - e.currentTarget.style.display = "none"; - if (e.currentTarget.nextSibling) { - ( - e.currentTarget.nextSibling as HTMLElement - ).style.display = "flex"; - } - }} - /> - ) : null} -
- - - + {isDialogLoading ? ( +
+ {/* Right Column - Loading Skeleton */} +
+ {/* Company Image & Title Skeleton */} +
+
+
+
+ {/* Description Skeleton */} +
+
+
+
+
- {selectedCompany?.description ? ( -
-

- {selectedCompany.description} -

-
- ) : ( -
- توضیحات در دسترس نیست -
- )} -
- {/* Left Column - Company Fields */} -
-

- اطلاعات - {selectedCompany?.category} -

- {selectedCompany?.fields && - selectedCompany.fields.length > 0 ? ( + {/* Left Column - Loading Skeleton */} +
+
- {selectedCompany.fields.map((field, index) => ( + {Array.from({ length: 6 }).map((_, index) => (
- - - {field.N}: - - - - {handleValue(field.V)} - {field.U && ({field.U})} - - +
+
+
+
+
))}
- ) : ( -
- اطلاعات تکمیلی در دسترس نیست -
- )} +
-
+ ) : ( +
+ {/* Right Column - Description */} +
+ {/* Company Image */} +
+ {selectedCompany?.label || ""} + {selectedCompany?.stageid && token?.accessToken ? ( + {selectedCompany?.label { + // Hide image and show fallback on error + e.currentTarget.style.display = "none"; + if (e.currentTarget.nextSibling) { + ( + e.currentTarget.nextSibling as HTMLElement + ).style.display = "flex"; + } + }} + /> + ) : null} +
+ + + +
+
+ {selectedCompany?.description ? ( +
+

+ {selectedCompany.description} +

+
+ ) : ( +
+ توضیحات در دسترس نیست +
+ )} +
+ {/* Left Column - Company Fields */} +
+

+ اطلاعات + {selectedCompany?.category} +

+ {selectedCompany?.fields && + selectedCompany.fields.length > 0 ? ( +
+ {selectedCompany.fields.map((field, index) => ( +
+ + + {field.N}: + + + + {handleValue(field.V)} + {field.U && ({field.U})} + + +
+ ))} +
+ ) : ( +
+ اطلاعات تکمیلی در دسترس نیست +
+ )} +
+
+ )} -- 2.46.0.windows.1