diff --git a/app/components/dashboard/project-management/green-innovation-page.tsx b/app/components/dashboard/project-management/green-innovation-page.tsx index 620ff49..93b3a56 100644 --- a/app/components/dashboard/project-management/green-innovation-page.tsx +++ b/app/components/dashboard/project-management/green-innovation-page.tsx @@ -41,8 +41,7 @@ import { UsersIcon, UserIcon, RefreshCw, - Radar, - Cog, + ChevronUp, ChevronDown, } from "lucide-react"; @@ -268,7 +267,7 @@ export function GreenInnovationPage() { try { fetchingRef.current = true; if (reset) { - + setCurrentPage(1); } else { setLoadingMore(true); @@ -395,9 +394,9 @@ export function GreenInnovationPage() { }; }, [loadMore, hasMore, loadingMore]); - useEffect(()=>{ - setLoading(true); - },[]) + useEffect(() => { + setLoading(true); + }, []) const handleSort = (field: string) => { fetchingRef.current = false; setSortConfig((prev) => ({ @@ -453,7 +452,7 @@ export function GreenInnovationPage() { if (typeof payload === "string") { try { payload = JSON.parse(payload); - } catch {} + } catch { } } const parseNum = (v: unknown): any => { if (v == null) return 0; @@ -687,68 +686,68 @@ export function GreenInnovationPage() {
{loading || statsLoading ? // Loading skeleton for stats cards - matching new design - Array.from({ length: 2 }).map((_, index) => ( - - -
-
-
-
-
-
-
-
+ Array.from({ length: 2 }).map((_, index) => ( + + +
+
+
- - - )) +
+
+
+
+
+ + + )) : Object.entries(sustainabilityStats).map(([key, value]) => ( - - -
-
-

- {value.title} -

+ + +
+
+

+ {value.title} +

+
+
+
+ + % {value.percent?.value} + + + {value.percent?.description} +
-
-
- - % {value.percent?.value} - - - {value.percent?.description} - -
- -
- - {value.total?.value} - - - {value.total?.description} - -
+ +
+ + {value.total?.value} + + + {value.total?.description} +
- - - ))} +
+ + + ))}
{/* Process Impacts Chart */} @@ -910,20 +909,20 @@ export function GreenInnovationPage() {
{statsLoading ? Array.from({ length: 10 }).map((_, index) => ( -
- - -
- )) +
+ + +
+ )) : Array.from({ length: 4 }).map((_, index) => ( -
- - استاندارد Iso 2005 -
- ))} +
+ + استاندارد Iso 2005 +
+ ))}
@@ -1118,9 +1117,9 @@ export function GreenInnovationPage() { {selectedProjectDetails?.start_date ? moment( - selectedProjectDetails?.start_date, - "YYYY-MM-DD" - ).format("YYYY/MM/DD") + selectedProjectDetails?.start_date, + "YYYY-MM-DD" + ).format("YYYY/MM/DD") : "-"}
@@ -1133,9 +1132,9 @@ export function GreenInnovationPage() { {selectedProjectDetails?.done_date ? moment( - selectedProjectDetails?.done_date, - "YYYY-MM-DD" - ).format("YYYY/MM/DD") + selectedProjectDetails?.done_date, + "YYYY-MM-DD" + ).format("YYYY/MM/DD") : "-"}
diff --git a/app/components/dashboard/project-management/innovation-built-inside-page.tsx b/app/components/dashboard/project-management/innovation-built-inside-page.tsx new file mode 100644 index 0000000..13c5ac7 --- /dev/null +++ b/app/components/dashboard/project-management/innovation-built-inside-page.tsx @@ -0,0 +1,1058 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import { Card, CardContent } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { Badge } from "~/components/ui/badge"; +import { Checkbox } from "~/components/ui/checkbox"; +import moment from "moment-jalaali"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; + +import apiService from "~/lib/api"; +import toast from "react-hot-toast"; +import { + FilterIcon, + Key, + Sparkle, + Zap, + Flame, + Building2, + PickaxeIcon, + UsersIcon, + UserIcon, + RefreshCw, + ChevronUp, + ChevronDown, +} from "lucide-react"; +import DashboardLayout from "../layout"; + +moment.loadPersian({ usePersianDigits: true }); +interface GreenInnovationData { + WorkflowID: string; + approved_budget: string; + done_date: string | null; + observer: string; + project_description: string; + project_id: string; + project_no: string; + project_rating: string; + project_status: string; + start_date: string; + title: string; +} + +interface SortConfig { + field: string; + direction: "asc" | "desc"; +} + +interface StateItem { + id: string; + title: string; + percent: { + value: number; + description: string; + }; + total: { + value: number; + description: string; + }; +} +interface BottleNeckItem { + resolveBottleNeck: { + label: string; + value: string; + description?: string; + }; + increaseCapacity: { + label: string; + value: string; + description?: string; + unit?: string; + increasePercent: number + }; + increaseIncome: { + label: string; + value: string; + description?: string; + increasePercent: number + unit?: string; + }; +} + + +interface StatsCard { + currencySaving: StateItem; + investmentAmount: StateItem; +} + +interface InnovationStats { + average_project_score: number | null; + count_innovation_construction_inside_projects: number; + foreign_currency_saving: number; + foreign_currency_saving_percent: number; + high_level_technology_count: number; + increased_capacity_after_innovation: number; + increased_capacity_after_innovation_percent: number; + increased_income_after_innovation: number; + increased_income_after_innovation_percent: number; + investment_amount: number; + investment_amount_percent: number; + resolved_bottleneck_count: number; +} + +interface Params { + icon: any; + label: string; + value: number; + suffix: string; + percent: number; +} +interface RecycleParams { + water: Params; + food: Params; + power: Params; + oil: Params; +} + +interface stateCounter { + totalProjects: number; +} + +interface ChartDataItem { + name: string; + pv: any; // actual value + amt: number; // max value or target +} + +enum projectStatus { + propozal = "پروپوزال", + contract = "پیشنویس قرارداد", + inprogress = "در حال انجام", + stop = "متوقف شده", + mafasa = "مرحله مفاصا", + finish = "پایان یافته", +} + +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 InnovationBuiltInsidePage() { + 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 [actualTotalCount, setActualTotalCount] = useState(0); + const [statsLoading, setStatsLoading] = useState(false); + const [stats, setStats] = useState(); + const [sortConfig, setSortConfig] = useState({ + field: "start_date", + direction: "asc", + }); + const [tblAvarage, setTblAvarage] = useState(0); + const [selectedProjects, setSelectedProjects] = useState>( + new Set() + ); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [selectedProjectDetails, setSelectedProjectDetails] = + useState(null); + + const [innovationMetric, setInnovationMetric] = useState({ + + }) + // const [recycleParams, setRecycleParams] = useState({ + // water: { + // icon: , + // label: "آب", + // value: 0, + // suffix: "لیتر", + // percent: 0, + // }, + // food: { + // icon: , + // label: "خوراک", + // value: 0, + // suffix: "تن", + // percent: 0, + // }, + + // power: { + // icon: , + // label: "برق", + // value: 0, + // suffix: "میلیون مگاوات", + // percent: 0, + // }, + // oil: { + // icon: , + // label: "سوخت", + // value: 0, + // suffix: "متر مربع", + // percent: 0, + // }, + // }); + const [sustainabilityStats, setSustainabilityStats] = useState({ + currencySaving: { + id: "reduce-pollution", + title: "صرفه جویی ارزی", + total: { + value: 10.45, + description: "میلیون ریال کاهش یافته", + }, + + percent: { + value: 10, + description: "درصد نسبت به کل", + }, + }, + investmentAmount: { + id: "reduce-junkfull", + title: "میزان سرمایه گذاری ", + total: { + value: 10, + description: "میلیون ریال", + }, + percent: { + value: 10, + description: "درصد به کل", + }, + }, + }); + + const [bottleNeck, setBottleNeck] = useState({ + resolveBottleNeck: { + label: 'تعدادگلوگاه رفع شده', + value: '0', + description: '' + }, + increaseCapacity: { + label: 'ظرفیت تولید اضافه شده', + value: '0', + description: 'درصد افزایش ظرفیت تولید', + increasePercent: 0, + unit: 'تن' + }, + increaseIncome: { + label: 'میزان افزایش درآمد', + value: '0', + description: 'درصد افزایش درآمد', + increasePercent: 0, + unit: "میلیون ریال" + } + }) + + + const [countOfHighTech, setCountOfHighTech] = useState(0) + + const observerRef = useRef(null); + const fetchingRef = useRef(false); + + const handleSelectProject = (projectNo: string) => { + const newSelected = new Set(selectedProjects); + if (newSelected.has(projectNo)) { + newSelected.delete(projectNo); + } else { + newSelected.add(projectNo); + } + setSelectedProjects(newSelected); + }; + + const handleProjectDetails = (project: GreenInnovationData) => { + setSelectedProjectDetails(project); + setDetailsDialogOpen(true); + }; + + const formatNumber = (value: string | number) => { + if (!value) return "0"; + const numericValue = typeof value === "string" ? parseFloat(value) : value; + if (isNaN(numericValue)) return "0"; + return new Intl.NumberFormat("fa-IR").format(numericValue); + }; + + const fetchProjects = async (reset = false) => { + if (fetchingRef.current) { + return; + } + + try { + fetchingRef.current = true; + if (reset) { + setCurrentPage(1); + } else { + setLoadingMore(true); + } + + const pageToFetch = reset ? 1 : currentPage; + const response = await apiService.select({ + ProcessName: "project", + OutputFields: [ + "project_id", + "project_no", + "title", + "project_status", + "project_rating", + "project_description", + "start_date", + "done_date", + "approved_budget", + "observer" + ], + Sorts: [[sortConfig.field, sortConfig.direction]], + Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]], + Pagination: { PageNumber: pageToFetch, PageSize: pageSize }, + }); + 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) { + setProjects(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 (!loadingMore && hasMore && !loading) { + setCurrentPage((prev) => prev + 1); + } + }, [loadingMore, hasMore, loading]); + + useEffect(() => { + fetchProjects(true); + fetchTotalCount(); + fetchStats(); + }, [sortConfig, selectedProjects]); + + useEffect(() => { + if (currentPage > 1) { + fetchProjects(false); + } + }, [currentPage]); + + useEffect(() => { + const scrollContainer = document.querySelector(".overflow-auto"); + + const handleScroll = () => { + if (!scrollContainer || !hasMore || loadingMore) return; + + const { scrollTop, scrollHeight, clientHeight } = scrollContainer; + const scrollPercentage = (scrollTop + clientHeight) / scrollHeight; + + if (scrollPercentage >= 0.9) { + loadMore(); + } + }; + + if (scrollContainer) { + scrollContainer.addEventListener("scroll", handleScroll); + } + + return () => { + if (scrollContainer) { + scrollContainer.removeEventListener("scroll", handleScroll); + } + }; + }, [loadMore, hasMore, loadingMore]); + + useEffect(() => { + setLoading(true); + }, []); + const handleSort = (field: string) => { + fetchingRef.current = false; + 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: "project", + OutputFields: ["count(project_no)"], + Conditions: [["type_of_innovation", "=", "نوآوری سبز"]], + }); + 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; + // Keep stats in sync if backend stats not yet loaded + 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_construction_inside_function: { + project_ids: + selectedProjects.size > 0 + ? Array.from(selectedProjects).join(" , ") + : "", + }, + }); + let payload: any = raw?.data; + if (typeof payload === "string") { + try { + payload = JSON.parse(payload); + } catch { } + } + const parseNum = (v: unknown): any => { + 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 data: Array = JSON.parse( + payload?.innovation_construction_inside_function + ); + const stats = data[0]; + const normalized: any = { + currencySaving: { + value: formatNumber(parseNum(stats?.foreign_currency_saving)), + percent: formatNumber(parseNum(stats?.foreign_currency_saving_percent)), + }, + + investmentAmount: { + value: formatNumber(parseNum(stats?.investment_amount)), + percent: formatNumber(parseNum(stats?.investment_amount_percent)), + }, + + technology: { + value: formatNumber(parseNum(stats?.high_level_technology_count)), + }, + + income: { + value: formatNumber(parseNum(stats.increased_income_after_innovation)), + percent: formatNumber(parseNum(stats.increased_income_after_innovation_percent)), + }, + + capacity: { + value: formatNumber(parseNum(stats.increased_capacity_after_innovation)), + percent: formatNumber(parseNum(stats.increased_capacity_after_innovation_percent)), + }, + + resolveBottleNeck: { + value: formatNumber(parseNum(stats.resolved_bottleneck_count)), + }, + countOfHighTech: formatNumber(stats.high_level_technology_count), + avarage: stats.average_project_score, + countInnovationGreenProjects: stats.count_innovation_construction_inside_projects, + }; + setActualTotalCount(normalized.countInnovationGreenProjects); + setTblAvarage(normalized.avarage); + setPageData(normalized); + } catch (error) { + console.error("Error fetching stats:", error); + } finally { + setStatsLoading(false); + } + }; + + const setPageData = (normalized: any) => { + setSustainabilityStats((prev) => ({ + ...prev, + currencySaving: { + ...prev.currencySaving, + total: { ...prev.currencySaving.total, value: normalized.currencySaving.value }, + percent: { + ...prev.currencySaving.percent, + value: normalized.currencySaving.percent, + }, + }, + investmentAmount: { + ...prev.investmentAmount, + total: { ...prev.investmentAmount.total, value: normalized.investmentAmount.value }, + percent: { ...prev.investmentAmount.percent, value: normalized.investmentAmount.percent }, + }, + })); + + setBottleNeck(prev => ({ + ...prev, + + increaseIncome: { + ...prev.increaseIncome, + value: normalized.income.value, + increasePercent: normalized.income.percent + }, + increaseCapacity: { + ...prev.increaseCapacity, + value: normalized.capacity.value, + increasePercent: normalized.capacity.percent + }, + resolveBottleNeck: { + ...prev.resolveBottleNeck, + value: normalized.resolveBottleNeck.value, + }, + // average: normalized.avarage, + // countInnovationGreenProjects: normalized.countInnovationGreenProjects, + })); + setCountOfHighTech(normalized.countOfHighTech) + }; + + const renderCellContent = (item: GreenInnovationData, column: any) => { + const value = item[column.key as keyof GreenInnovationData]; + + switch (column.key) { + case "select": + return ( + handleSelectProject(item.project_id)} + className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer" + /> + ); + 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) || "-"}; + } + }; + + 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 [chartData, setChartData] = useState>([ + // { name: recycleParams.water.label, pv: 70, amt: 80 }, + // { name: recycleParams.power.label, pv: 45, amt: 60 }, + // { name: recycleParams.oil.label, pv: 90, amt: 75 }, + // { name: recycleParams.food.label, pv: 30, amt: 50 }, + // ]); + + return ( + +
+ {/* Stats Cards */} +
+
+ {loading || statsLoading + ? // Loading skeleton for stats cards - matching new design + Array.from({ length: 2 }).map((_, index) => ( + + +
+
+
+
+
+
+
+
+
+ + + )) + : Object.entries(sustainabilityStats).map(([key, value]) => ( + + +
+
+

+ {value.title} +

+
+
+
+ + % {value.percent?.value} + + + {value.percent?.description} + +
+ +
+ + {value.total?.value} + + + {value.total?.description} + +
+
+
+
+
+ ))} + + + +
+
+

+ بر طرف کردن گلوگاه +

+ +
+
+ { + Object.entries(bottleNeck).map(([key, value]) => { + return
+
+ + {value.value} + + { + value.unit && + {value.unit} + + } + +
+
+ {value.label} +
+ + + { + value.description && value.description + } + + + {value.increasePercent} + +
+
+
+ }) + } +
+
+
+
+ + + تعداد فناوری سطح بالا + {countOfHighTech} + + +
+
+ + {/* Data Table */} + + +
+ + + + {columns.map((column) => ( + + {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 && ( +
+
+ + +
+
+ )} +
+
+ +
+
+
+
+ کل پروژه ها :{formatNumber(actualTotalCount)} +
+
+ +
+
+ + + + +
+
+
میانگین :‌
+
+ {formatNumber( + ((tblAvarage ?? 0) as number).toFixed?.(1) ?? 0 + )} +
+
+
+
+
+ +
+
+ + {/* Project Details Dialog */} + + + + + شرح پروژه + + +
+ {/* Project Description */} +
+

{selectedProjectDetails?.title}

+

+ {selectedProjectDetails?.project_description || "-"} +

+
+ + {/* Project Details */} +
+
جزئیات پروژه
+ +
+

+ + زمان شروع: +

+ + {selectedProjectDetails?.start_date + ? moment( + selectedProjectDetails?.start_date, + "YYYY-MM-DD" + ).format("YYYY/MM/DD") + : "-"} + +
+ +
+

+ + زمان پایان: +

+ + {selectedProjectDetails?.done_date + ? moment( + selectedProjectDetails?.done_date, + "YYYY-MM-DD" + ).format("YYYY/MM/DD") + : "-"} + +
+ +
+

+ + هزینه برآورد شده: +

+ + {formatNumber( + Number( + selectedProjectDetails?.approved_budget.replaceAll( + ",", + "" + ) + ) + ) || "-"} + +
+
+

+ + نفر مرتبط: +

+ + {selectedProjectDetails?.observer || "-"} + +
+ + {/*
+

+ + حوزه کاری : +

+ + {selectedProjectDetails?.observer || "-"} + +
*/} + + {/*
+

+ + صنعت : +

+ + {selectedProjectDetails?.observer || "-"} + +
*/} +
+
+
+
+ + ); +} + +export default InnovationBuiltInsidePage; diff --git a/app/routes.ts b/app/routes.ts index 77f0299..05a0524 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -6,15 +6,19 @@ export default [ route("dashboard/project-management", "routes/project-management.tsx"), route( "dashboard/innovation-basket/process-innovation", - "routes/innovation-basket.process-innovation.tsx", + "routes/innovation-basket.process-innovation.tsx" ), route( "dashboard/innovation-basket/green-innovation", - "routes/green-innovation.tsx", + "routes/green-innovation.tsx" + ), + route( + "/dashboard/innovation-basket/internal-innovation", + "routes/innovation-built-insider-page.tsx" ), route( "/dashboard/innovation-basket/digital-innovation", - "routes/digital-innovation-page.tsx", + "routes/digital-innovation-page.tsx" ), route("dashboard/ecosystem", "routes/ecosystem.tsx"), route("404", "routes/404.tsx"), diff --git a/app/routes/innovation-built-insider-page.tsx b/app/routes/innovation-built-insider-page.tsx new file mode 100644 index 0000000..7b6c663 --- /dev/null +++ b/app/routes/innovation-built-insider-page.tsx @@ -0,0 +1,17 @@ +import { ProtectedRoute } from "~/components/auth/protected-route"; +import InnovationBuiltInsidePage from "~/components/dashboard/project-management/innovation-built-inside-page"; + +export function meta() { + return [ + { title: "نوآوری در فرآیند - سیستم مدیریت فناوری و نوآوری" }, + { name: "description", content: "مدیریت پروژه‌های نوآوری در فرآیند" }, + ]; +} + +export default function InnovationBuiltInside() { + return ( + + + + ); +}