From e36fbf9874f0da23142eafbcc32108b875e31e6c Mon Sep 17 00:00:00 2001 From: mehrdad_adabi Date: Sun, 7 Sep 2025 19:47:13 +0330 Subject: [PATCH 1/2] fix: report bugs --- .../green-innovation-page.tsx | 256 +++++---- .../innovation-built-inside-page.tsx | 542 ++++++++++-------- .../process-innovation-page.tsx | 405 +++++++------ app/components/ui/custom-bar-chart.tsx | 3 +- app/lib/utils.ts | 2 +- 5 files changed, 631 insertions(+), 577 deletions(-) diff --git a/app/components/dashboard/project-management/green-innovation-page.tsx b/app/components/dashboard/project-management/green-innovation-page.tsx index 27b90f9..cfc9d05 100644 --- a/app/components/dashboard/project-management/green-innovation-page.tsx +++ b/app/components/dashboard/project-management/green-innovation-page.tsx @@ -1,9 +1,23 @@ -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 { useCallback, useEffect, useRef, useState } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + XAxis, + YAxis, +} from "recharts"; +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 { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; import { Table, TableBody, @@ -12,39 +26,24 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "~/components/ui/dialog"; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - ResponsiveContainer, -} from "recharts"; -import apiService from "~/lib/api"; -import toast from "react-hot-toast"; import { - LoaderCircle, - TrendingUp, - Key, - Sparkle, - Zap, - Flame, Building2, - PickaxeIcon, - UsersIcon, - UserIcon, - RefreshCw, - - ChevronUp, ChevronDown, + ChevronUp, + Flame, + Key, + LoaderCircle, + PickaxeIcon, + RefreshCw, + Sparkle, + TrendingUp, + UserIcon, + UsersIcon, + Zap, } from "lucide-react"; +import toast from "react-hot-toast"; +import apiService from "~/lib/api"; import DashboardLayout from "../layout"; moment.loadPersian({ usePersianDigits: true }); @@ -100,7 +99,7 @@ interface InnovationStats { water_recovery_reduction_percent: number; average_project_score: number; count_innovation_green_projects: number; - standard_regulations: string + standard_regulations: string; } interface Params { @@ -174,7 +173,9 @@ export function GreenInnovationPage() { const [selectedProjects, setSelectedProjects] = useState>( new Set() ); - const [standartRegulation, setStandardRegulation] = useState>([]) + const [standartRegulation, setStandardRegulation] = useState>( + [] + ); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [selectedProjectDetails, setSelectedProjectDetails] = useState(null); @@ -255,7 +256,6 @@ export function GreenInnovationPage() { }; 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); @@ -269,7 +269,6 @@ export function GreenInnovationPage() { try { fetchingRef.current = true; if (reset) { - setCurrentPage(1); } else { setLoadingMore(true); @@ -362,8 +361,11 @@ export function GreenInnovationPage() { useEffect(() => { fetchProjects(true); fetchTotalCount(); + }, [sortConfig]); + + useEffect(() => { fetchStats(); - }, [sortConfig, selectedProjects]); + }, [selectedProjects]); useEffect(() => { if (currentPage > 1) { @@ -398,7 +400,7 @@ export function GreenInnovationPage() { useEffect(() => { setLoading(true); - }, []) + }, []); const handleSort = (field: string) => { fetchingRef.current = false; setSortConfig((prev) => ({ @@ -454,15 +456,16 @@ export function GreenInnovationPage() { if (typeof payload === "string") { try { payload = JSON.parse(payload); - } catch { } + } catch {} } const parseNum = (v: unknown): any => { + const convertNumber = typeof v === "number" ? Math.max(0, v) : 0; if (v == null) return 0; - if (typeof v === "number") return v; + if (typeof v === "number") return convertNumber; if (typeof v === "string") { const cleaned = v.replace(/,/g, "").trim(); const n = parseFloat(cleaned); - return isNaN(n) ? 0 : n; + return isNaN(n) ? 0 : convertNumber; } return 0; }; @@ -503,9 +506,11 @@ export function GreenInnovationPage() { }, avarage: stats.average_project_score, countInnovationGreenProjects: stats.count_innovation_green_projects, - standardRegulation: stats.standard_regulations.replace('\r', '').split('\n') + standardRegulation: stats.standard_regulations + .replace("\r", "") + .split("\n"), }; - setStandardRegulation(normalized.standardRegulation) + setStandardRegulation(normalized.standardRegulation); setActualTotalCount(normalized.countInnovationGreenProjects); setTblAvarage(normalized.avarage); setPageData(normalized); @@ -689,68 +694,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.percent?.value} - - - {value.percent?.description} - + + +
+
+

+ {value.title} +

- -
- - {value.total?.value} - - - {value.total?.description} - +
+
+ + % {value.percent?.value} + + + {value.percent?.description} + +
+ +
+ + {value.total?.value} + + + {value.total?.description} + +
-
-
-
- ))} + + + ))}
{/* Process Impacts Chart */} @@ -880,7 +885,8 @@ export function GreenInnovationPage() { position: "top", fill: "#fff", fontWeight: "bold", - formatter: (value: any) => `${formatNumber(value)}%`, + formatter: (value: any) => + `${formatNumber(value)}%`, }} /> @@ -912,21 +918,27 @@ export function GreenInnovationPage() {
{statsLoading ? Array.from({ length: 10 }).map((_, index) => ( -
- - -
- )) +
+ + +
+ )) : standartRegulation.map((item, index) => ( -
- - {item} -
- ))} +
+ + {item} +
+ ))}
@@ -1121,9 +1133,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") : "-"}
@@ -1136,9 +1148,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 index 0c75312..6d7f46f 100644 --- a/app/components/dashboard/project-management/innovation-built-inside-page.tsx +++ b/app/components/dashboard/project-management/innovation-built-inside-page.tsx @@ -1,9 +1,15 @@ -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 { useCallback, useEffect, useRef, useState } from "react"; +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 { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; import { Table, TableBody, @@ -12,28 +18,29 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "~/components/ui/dialog"; -import apiService from "~/lib/api"; +import { + ChevronDown, + ChevronUp, + CodeXml, + FilterIcon, + Handshake, + RefreshCw, + SquareUser, + User, + Users, +} from "lucide-react"; import toast from "react-hot-toast"; import { - FilterIcon, - RefreshCw, - ChevronUp, - ChevronDown, - Handshake, - CodeXml, - SquareUser, - Users, - User, -} from "lucide-react"; + Customized, + Line, + LineChart, + ReferenceLine, + ResponsiveContainer, + XAxis, +} from "recharts"; +import apiService from "~/lib/api"; import DashboardLayout from "../layout"; -import { LineChart, CartesianGrid, Legend, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, Customized } from "recharts"; moment.loadPersian({ usePersianDigits: true }); @@ -43,7 +50,7 @@ interface innovationBuiltInDate { done_date: string | null; observer: string; project_description: string; - project_id: number | null; + project_id: number | string; project_no: string; project_rating: string; project_status: string; @@ -52,7 +59,7 @@ interface innovationBuiltInDate { } interface DialogInfo { - WorkflowID: number + WorkflowID: number; collaboration_model: string; complexity_level: string; developer_team_role: string; @@ -66,10 +73,9 @@ interface DialogInfo { role_company_staff: string | null; technology_maturity_level: string; title: string; - technology_params?: Array + technology_params?: Array; } - interface SortConfig { field: string; direction: "asc" | "desc"; @@ -98,18 +104,17 @@ interface BottleNeckItem { value: string; description?: string; unit?: string; - increasePercent: number + increasePercent: number; }; increaseIncome: { label: string; value: string; description?: string; - increasePercent: number + increasePercent: number; unit?: string; }; } - interface StatsCard { currencySaving: StateItem; investmentAmount: StateItem; @@ -166,11 +171,10 @@ const columns = [ ]; const dialogChartData = [ - { name: 'مرحه پیدایش', value: 10 }, - { name: 'مرحله رشد', value: 14 }, - { name: 'مرحله بلوغ', value: 25 }, - { name:'مرحله افول', value: 15 }, - + { name: "مرحه پیدایش", value: 10 }, + { name: "مرحله رشد", value: 14 }, + { name: "مرحله بلوغ", value: 25 }, + { name: "مرحله افول", value: 15 }, ]; export function InnovationBuiltInsidePage() { @@ -189,15 +193,13 @@ export function InnovationBuiltInsidePage() { direction: "asc", }); const [tblAvarage, setTblAvarage] = useState(0); - const [selectedProjects, setSelectedProjects] = useState>( - new Set() - ); + const [selectedProjects, setSelectedProjects] = useState< + Set + >(new Set()); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [selectedProjectDetails, setSelectedProjectDetails] = useState(); - - const [sustainabilityStats, setSustainabilityStats] = useState({ currencySaving: { id: "reduce-pollution", @@ -228,35 +230,34 @@ export function InnovationBuiltInsidePage() { const [bottleNeck, setBottleNeck] = useState({ resolveBottleNeck: { - label: 'تعدادگلوگاه رفع شده', - value: '0', - description: '' + label: "تعدادگلوگاه رفع شده", + value: "0", + description: "", }, increaseCapacity: { - label: 'ظرفیت تولید اضافه شده', - value: '0', - description: 'درصد افزایش ظرفیت تولید', + label: "ظرفیت تولید اضافه شده", + value: "0", + description: "درصد افزایش ظرفیت تولید", increasePercent: 0, - unit: 'تن' + unit: "تن", }, increaseIncome: { - label: 'میزان افزایش درآمد', - value: '0', - description: 'درصد افزایش درآمد', + label: "میزان افزایش درآمد", + value: "0", + description: "درصد افزایش درآمد", increasePercent: 0, - unit: "میلیون ریال" - } - }) + unit: "میلیون ریال", + }, + }); + const [showDialogItems, setShowDialogItems] = useState(false); - const [showDialogItems, setShowDialogItems] = useState(false) - - const [countOfHighTech, setCountOfHighTech] = useState(0) + const [countOfHighTech, setCountOfHighTech] = useState(0); const observerRef = useRef(null); const fetchingRef = useRef(false); - const handleSelectProject = (projectNo: string) => { + const handleSelectProject = (projectNo: string | number) => { const newSelected = new Set(selectedProjects); if (newSelected.has(projectNo)) { newSelected.delete(projectNo); @@ -267,18 +268,16 @@ export function InnovationBuiltInsidePage() { }; const handleProjectDetails = async (project: DialogInfo) => { - setShowDialogItems(true) + setShowDialogItems(true); setDetailsDialogOpen(true); setSelectedProjectDetails(project); - await fetchDialogTbl(project.WorkflowID) + await fetchDialogTbl(project.WorkflowID); setTimeout(() => { - setShowDialogItems(false) - calculateProgressBar(+project.project_rating) + setShowDialogItems(false); + calculateProgressBar(+project.project_rating); }, 500); }; - - const formatNumber = (value: string | number) => { if (!value) return "0"; const numericValue = typeof value === "string" ? parseFloat(value) : value; @@ -315,7 +314,7 @@ export function InnovationBuiltInsidePage() { "role_company_staff", "number_employees_involved", "participants_full_name", - "technology_maturity_level" + "technology_maturity_level", ], Sorts: [[sortConfig.field, sortConfig.direction]], Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]], @@ -380,7 +379,6 @@ export function InnovationBuiltInsidePage() { } }; - const fetchDialogTbl = async (tblId: number) => { try { const response = await apiService.select({ @@ -388,7 +386,7 @@ export function InnovationBuiltInsidePage() { OutputFields: [ "technology_parameter_title", "domestic_technology_parameter_value", - "foreign_technology_parameter_value" + "foreign_technology_parameter_value", ], Conditions: [["project_id", "=", tblId]], }); @@ -398,7 +396,7 @@ export function InnovationBuiltInsidePage() { const parsedData = JSON.parse(dataString); setSelectedProjectDetails((prev: any) => ({ ...prev, - technology_params: parsedData + technology_params: parsedData, })); } } @@ -409,7 +407,9 @@ export function InnovationBuiltInsidePage() { }; const calculateProgressBar = (val: number) => { - const pointer = document.getElementsByClassName('progressBarPointer')[0] as HTMLElement; + const pointer = document.getElementsByClassName( + "progressBarPointer" + )[0] as HTMLElement; if (!pointer) return; const leftValue = val !== 0 ? `calc(${val}% - 100px)` : `calc(0% - 40px)`; @@ -430,7 +430,7 @@ export function InnovationBuiltInsidePage() { useEffect(() => { fetchStats(); - }, [selectedProjects]) + }, [selectedProjects]); useEffect(() => { if (currentPage > 1) { @@ -478,10 +478,9 @@ export function InnovationBuiltInsidePage() { setHasMore(true); }; - const fetchStats = async () => { try { - detailsDialogOpen + detailsDialogOpen; setStatsLoading(true); const raw = await apiService.call({ innovation_construction_inside_function: { @@ -495,7 +494,7 @@ export function InnovationBuiltInsidePage() { if (typeof payload === "string") { try { payload = JSON.parse(payload); - } catch { } + } catch {} } const parseNum = (v: unknown): any => { if (v == null) return 0; @@ -515,7 +514,9 @@ export function InnovationBuiltInsidePage() { const normalized: any = { currencySaving: { value: formatNumber(parseNum(stats?.foreign_currency_saving)), - percent: formatNumber(parseNum(stats?.foreign_currency_saving_percent)), + percent: formatNumber( + parseNum(stats?.foreign_currency_saving_percent) + ), }, investmentAmount: { @@ -528,13 +529,21 @@ export function InnovationBuiltInsidePage() { }, income: { - value: formatNumber(parseNum(stats.increased_income_after_innovation)), - percent: formatNumber(parseNum(stats.increased_income_after_innovation_percent)), + 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)), + value: formatNumber( + parseNum(stats.increased_capacity_after_innovation) + ), + percent: formatNumber( + parseNum(stats.increased_capacity_after_innovation_percent) + ), }, resolveBottleNeck: { @@ -542,7 +551,8 @@ export function InnovationBuiltInsidePage() { }, countOfHighTech: formatNumber(stats.high_level_technology_count), avarage: stats.average_project_score, - countInnovationGreenProjects: stats.count_innovation_construction_inside_projects, + countInnovationGreenProjects: + stats.count_innovation_construction_inside_projects, }; setActualTotalCount(normalized.countInnovationGreenProjects); setTblAvarage(normalized.avarage); @@ -559,7 +569,10 @@ export function InnovationBuiltInsidePage() { ...prev, currencySaving: { ...prev.currencySaving, - total: { ...prev.currencySaving.total, value: normalized.currencySaving.value }, + total: { + ...prev.currencySaving.total, + value: normalized.currencySaving.value, + }, percent: { ...prev.currencySaving.percent, value: normalized.currencySaving.percent, @@ -567,23 +580,29 @@ export function InnovationBuiltInsidePage() { }, investmentAmount: { ...prev.investmentAmount, - total: { ...prev.investmentAmount.total, value: normalized.investmentAmount.value }, - percent: { ...prev.investmentAmount.percent, value: normalized.investmentAmount.percent }, + total: { + ...prev.investmentAmount.total, + value: normalized.investmentAmount.value, + }, + percent: { + ...prev.investmentAmount.percent, + value: normalized.investmentAmount.percent, + }, }, })); - setBottleNeck(prev => ({ + setBottleNeck((prev) => ({ ...prev, increaseIncome: { ...prev.increaseIncome, value: normalized.income.value, - increasePercent: normalized.income.percent + increasePercent: normalized.income.percent, }, increaseCapacity: { ...prev.increaseCapacity, value: normalized.capacity.value, - increasePercent: normalized.capacity.percent + increasePercent: normalized.capacity.percent, }, resolveBottleNeck: { ...prev.resolveBottleNeck, @@ -592,7 +611,7 @@ export function InnovationBuiltInsidePage() { // average: normalized.avarage, // countInnovationGreenProjects: normalized.countInnovationGreenProjects, })); - setCountOfHighTech(normalized.countOfHighTech) + setCountOfHighTech(normalized.countOfHighTech); }; const renderCellContent = (item: innovationBuiltInDate, column: any) => { @@ -602,8 +621,8 @@ export function InnovationBuiltInsidePage() { case "select": return ( handleSelectProject(item.project_id)} + checked={selectedProjects.has(item?.project_id!)} + onCheckedChange={() => handleSelectProject(item?.project_id)} className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer" /> ); @@ -688,7 +707,6 @@ export function InnovationBuiltInsidePage() { return el; }; - return (
@@ -697,72 +715,71 @@ export function InnovationBuiltInsidePage() {
{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.percent?.value} - - - {value.percent?.description} - + + +
+
+

+ {value.title} +

- -
- - {value.total?.value} - - - {value.total?.description} - +
+
+ + % {value.percent?.value} + + + {value.percent?.description} + +
+ +
+ + {value.total?.value} + + + {value.total?.description} + +
-
-
-
- ))} + + + ))} - - { - statsLoading ?
+ {statsLoading ? ( +
@@ -788,7 +805,9 @@ export function InnovationBuiltInsidePage() {
-
: +
+ ) : ( +
@@ -798,58 +817,57 @@ export function InnovationBuiltInsidePage() {
- { - Object.entries(bottleNeck).map(([key, value]) => { - return
+ {Object.entries(bottleNeck).map(([key, value]) => { + return ( +
{value.value} - { - value.unit && + {value.unit && ( + {value.unit} - } - + )}
{value.label}
- - { - value.description && value.description - } - - - {value.increasePercent} + {value.description && value.description} + {value.increasePercent}
- }) - } + ); + })}
- } + )} - { - statsLoading ?
+ {statsLoading ? ( +
-
: +
+ ) : ( + تعداد فناوری سطح بالا - {countOfHighTech} + + {countOfHighTech} + - } - - + )}
@@ -960,13 +978,13 @@ export function InnovationBuiltInsidePage() {
-
+
کل پروژه ها :{formatNumber(actualTotalCount)}
-
+
@@ -984,8 +1002,6 @@ export function InnovationBuiltInsidePage() {
- -
@@ -999,32 +1015,30 @@ export function InnovationBuiltInsidePage() {
- - { - showDialogItems ?
+ {showDialogItems ? ( +
-
:
+
+ ) : ( +

{selectedProjectDetails?.title}

{selectedProjectDetails?.project_description || "-"}

- } + )}

دانش فنی محصول جدید

- { - showDialogItems ?
+ {showDialogItems ? ( +
-
-
-
-
-
-
+
+
+
@@ -1036,7 +1050,9 @@ export function InnovationBuiltInsidePage() {
-
:
+
+ ) : ( +
@@ -1049,20 +1065,23 @@ export function InnovationBuiltInsidePage() { سطح بالا
-
+
- سطح تکنولوژی + + {" "} + سطح تکنولوژی +
- } + )}

مشارکت در پروژه

- { - showDialogItems ?
+ {showDialogItems ? ( +
{[...Array(4)].map((_, i) => (
@@ -1072,34 +1091,45 @@ export function InnovationBuiltInsidePage() {
))} -
:
+
+ ) : ( +
مدل همکاری: - {selectedProjectDetails?.collaboration_model} + + {selectedProjectDetails?.collaboration_model} +
نقش تیم توسعه دهنده: - {selectedProjectDetails?.developer_team_role} + + {selectedProjectDetails?.developer_team_role} +
نقش کارکنان شرکت: - {selectedProjectDetails?.role_company_staff ?? '-'} + + {selectedProjectDetails?.role_company_staff ?? "-"} +
تعداد کارکنان درگیر: - {selectedProjectDetails?.number_employees_involved ?? 0} + + {selectedProjectDetails?.number_employees_involved ?? + 0} +
@@ -1108,59 +1138,76 @@ export function InnovationBuiltInsidePage() {
لیست کارکنان : - {selectedProjectDetails?.participants_full_name} + + {selectedProjectDetails?.participants_full_name} +
-
- } - - - - - + )}
{/* Project Details */}
- { - showDialogItems ?
+ {showDialogItems ? ( +
{[...Array(5)].map((_, rowIndex) => (
))} -
:
+
+ ) : ( +
- شاخص مقایسه با نمونه خارجی + + شاخص مقایسه با نمونه خارجی + نمونه داخلی نمونه خارجی
- { - selectedProjectDetails?.technology_params?.map((el, index) => { - return
- {el.technology_parameter_title} -
- {formatNumber(el.domestic_technology_parameter_value)} - میلیون لیتر + {selectedProjectDetails?.technology_params?.map( + (el, index) => { + return ( +
+ + {el.technology_parameter_title} + +
+ + {formatNumber( + el.domestic_technology_parameter_value + )} + + + میلیون لیتر + +
+
+ + {formatNumber( + el.foreign_technology_parameter_value + )} + + + میلیون لیتر + +
-
- {formatNumber(el.foreign_technology_parameter_value)} - میلیون لیتر -
-
- - }) - - } + ); + } + )}
- } -
- { - showDialogItems ?
+ )} +
+ {showDialogItems ? ( +
{[...Array(4)].map((_, i) => (
))} -
: +
+ ) : ( + t && - (t.value === value || (t.payload && t.payload.value === value)) + (t.value === value || + (t.payload && t.payload.value === value)) ); const axisOffsetX = xAxis?.x ?? 0; - const x = (xFromScale ?? tick?.coordinate ?? width / 2) + axisOffsetX - 15; + const x = + (xFromScale ?? tick?.coordinate ?? width / 2) + + axisOffsetX - + 15; const rectWidth = 140; const rectHeight = 28; @@ -1267,14 +1324,13 @@ export function InnovationBuiltInsidePage() { /> - } + )}
-
- + ); } diff --git a/app/components/dashboard/project-management/process-innovation-page.tsx b/app/components/dashboard/project-management/process-innovation-page.tsx index d2d484d..1512902 100644 --- a/app/components/dashboard/project-management/process-innovation-page.tsx +++ b/app/components/dashboard/project-management/process-innovation-page.tsx @@ -1,12 +1,30 @@ -import { useState, useEffect, useCallback, useRef } from "react"; -import { DashboardLayout } from "../layout"; -import { Card, CardContent } from "~/components/ui/card"; -import { Button } from "~/components/ui/button"; +import { + Building2, + ChevronDown, + ChevronUp, + CirclePause, + DollarSign, + Funnel, + PickaxeIcon, + RefreshCw, + UserIcon, + UsersIcon, + Wrench, +} 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 moment from "moment-jalaali"; -import type { BarChartData } from "~/components/ui/custom-bar-chart"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; import { Table, TableBody, @@ -15,29 +33,14 @@ import { TableHeader, TableRow, } from "~/components/ui/table"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "~/components/ui/dialog"; -import { - ChevronUp, - ChevronDown, - RefreshCw, - ExternalLink, - Building2, - PickaxeIcon, - UserIcon, - UsersIcon, -} from "lucide-react"; import apiService from "~/lib/api"; -import toast from "react-hot-toast"; -import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react"; +import { formatNumber } from "~/lib/utils"; +import { DashboardLayout } from "../layout"; moment.loadPersian({ usePersianDigits: true }); interface ProcessInnovationData { project_no: string; + project_id: string; title: string; project_status: string; project_rating: string; @@ -48,18 +51,31 @@ interface ProcessInnovationData { observer: string; } +interface ProjectStats { + average_project_score: string; + count_innovation_process_projects: number; + count_throat_removal: number; + percent_reducing_breakdowns: string; + percent_reduction_value_currency: string; + percent_sum_stopping_production: string; + percent_throat_removal: string; + sum_reducing_breakdowns: number; + sum_reduction_value_currency: number; + sum_stopping_production: number; +} + interface SortConfig { field: string; direction: "asc" | "desc"; } -interface StatsCard { - id: string; - title: string; - value: string; - description: string; - icon: React.ReactNode; - color: string; +enum projectStatus { + propozal = "پروپوزال", + contract = "پیشنویس قرارداد", + inprogress = "در حال انجام", + stop = "متوقف شده", + mafasa = "مرحله مفاصا", + finish = "پایان یافته", } interface InnovationStats { @@ -69,10 +85,10 @@ interface InnovationStats { bottleneckRemovalCount: number; // تعداد رفع گلوگاه currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال) frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار - percentProductionStops: number; // درصد مقایسه‌ای جلوگیری از توقفات تولید - percentBottleneckRemoval: number; // درصد مقایسه‌ای رفع گلوگاه - percentCurrencyReduction: number; // درصد مقایسه‌ای کاهش ارز بری - percentFailuresReduction: number; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار + percentProductionStops: number | string; // درصد مقایسه‌ای جلوگیری از توقفات تولید + percentBottleneckRemoval: number | string; // درصد مقایسه‌ای رفع گلوگاه + percentCurrencyReduction: number | string; // درصد مقایسه‌ای کاهش ارز بری + percentFailuresReduction: number | string; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار } const columns = [ @@ -126,6 +142,50 @@ export function ProcessInnovationPage() { const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [selectedProjectDetails, setSelectedProjectDetails] = useState(null); + + const [stateCard, setStateCard] = useState({ + productionstopsprevention: { + id: "productionstopsprevention", + title: "جلوگیری از توقفات تولید", + value: formatNumber( + stats.productionStopsPreventionSum.toFixed?.(1) ?? + stats.productionStopsPreventionSum + ), + description: "تن افزایش یافته", + icon: , + color: "text-emerald-400", + }, + bottleneckremoval: { + id: "bottleneckremoval", + title: "رفع گلوگاه", + value: formatNumber(stats.bottleneckRemovalCount), + description: "تعداد رفع گلوگاه", + icon: , + color: "text-emerald-400", + }, + currencyreduction: { + id: "currencyreduction", + title: "کاهش ارز بری", + value: formatNumber( + stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum + ), + description: "دلار کاهش یافته", + icon: , + color: "text-emerald-400", + }, + frequentfailuresreduction: { + id: "frequentfailuresreduction", + title: "کاهش خرابی های پرتکرار", + value: formatNumber( + stats.frequentFailuresReductionSum.toFixed?.(1) ?? + stats.frequentFailuresReductionSum + ), + description: "مجموع درصد کاهش خرابی", + icon: , + color: "text-emerald-400", + }, + }); + const observerRef = useRef(null); const fetchingRef = useRef(false); @@ -153,58 +213,7 @@ export function ProcessInnovationPage() { 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); - }; - // Stats cards data - computed from projects data - const statsCards: StatsCard[] = [ - { - id: "production-stops-prevention", - title: "جلوگیری از توقفات تولید", - value: formatNumber( - stats.productionStopsPreventionSum.toFixed?.(1) ?? - stats.productionStopsPreventionSum - ), - description: "تن افزایش یافته", - icon: , - color: "text-emerald-400", - }, - { - id: "bottleneck-removal", - title: "رفع گلوگاه", - value: formatNumber(stats.bottleneckRemovalCount), - description: "تعداد رفع گلوگاه", - icon: , - color: "text-emerald-400", - }, - - { - id: "currency-reduction", - title: "کاهش ارز بری", - value: formatNumber( - stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum - ), - description: "دلار کاهش یافته", - icon: , - color: "text-emerald-400", - }, - { - id: "frequent-failures-reduction", - title: "کاهش خرابی های پرتکرار", - value: formatNumber( - stats.frequentFailuresReductionSum.toFixed?.(1) ?? - stats.frequentFailuresReductionSum - ), - description: "مجموع درصد کاهش خرابی", - icon: , - color: "text-emerald-400", - }, - ]; - const fetchProjects = async (reset = false) => { if (fetchingRef.current) { return; @@ -226,6 +235,7 @@ export function ProcessInnovationPage() { ProcessName: "project", OutputFields: [ "project_no", + "project_id", "title", "project_status", "project_rating", @@ -313,9 +323,12 @@ export function ProcessInnovationPage() { useEffect(() => { fetchProjects(true); fetchTotalCount(); - fetchStats(); }, [sortConfig]); + useEffect(() => { + fetchStats(); + }, [selectedProjects]); + useEffect(() => { if (currentPage > 1) { fetchProjects(false); @@ -393,7 +406,12 @@ export function ProcessInnovationPage() { try { setStatsLoading(true); const raw = await apiService.call({ - innovation_process_function: {}, + innovation_process_function: { + project_ids: + selectedProjects.size > 0 + ? Array.from(selectedProjects).join(" , ") + : "", + }, }); let payload: any = raw?.data; @@ -403,40 +421,53 @@ export function ProcessInnovationPage() { } catch {} } - const parseNum = (v: unknown): number => { + const parseNum = (v: unknown): any => { + const convertNumber = typeof v === "number" ? Math.max(0, v) : 0; if (v == null) return 0; - if (typeof v === "number") return v; + if (typeof v === "number") return convertNumber; if (typeof v === "string") { const cleaned = v.replace(/,/g, "").trim(); const n = parseFloat(cleaned); - return isNaN(n) ? 0 : n; + return isNaN(n) ? 0 : convertNumber; } return 0; }; + const data: Array = JSON.parse( + payload?.innovation_process_function + ); + const stats = data[0]; const normalized: InnovationStats = { - totalProjects: parseNum(payload?.count_innovation_process_projects), - averageScore: parseNum(payload?.average_project_score), - productionStopsPreventionSum: parseNum( - payload?.sum_stopping_production - ), - bottleneckRemovalCount: parseNum(payload?.count_throat_removal), - currencyReductionSum: parseNum(payload?.sum_reduction_value_currency), - frequentFailuresReductionSum: parseNum( - payload?.sum_reducing_breakdowns - ), - percentProductionStops: parseNum( - payload?.percent_sum_stopping_production - ), - percentBottleneckRemoval: parseNum(payload?.percent_throat_removal), - percentCurrencyReduction: parseNum( - payload?.percent_reduction_value_currency - ), - percentFailuresReduction: parseNum( - payload?.percent_reducing_breakdowns - ), + totalProjects: parseNum(stats?.count_innovation_process_projects), + averageScore: parseNum(stats?.average_project_score), + productionStopsPreventionSum: parseNum(stats?.sum_stopping_production), + bottleneckRemovalCount: parseNum(stats?.count_throat_removal), + currencyReductionSum: parseNum(stats?.sum_reduction_value_currency), + frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns), + percentProductionStops: stats?.percent_sum_stopping_production, + percentBottleneckRemoval: stats?.percent_throat_removal, + percentCurrencyReduction: stats?.percent_reduction_value_currency, + percentFailuresReduction: stats?.percent_reducing_breakdowns, }; - + setStateCard((prev) => ({ + ...prev, + bottleneckremoval: { + ...prev.bottleneckremoval, + value: formatNumber(normalized.bottleneckRemovalCount), + }, + productionstopsprevention: { + ...prev.productionstopsprevention, + value: formatNumber(normalized.productionStopsPreventionSum), + }, + frequentfailuresreduction: { + ...prev.frequentfailuresreduction, + value: formatNumber(normalized.frequentFailuresReductionSum), + }, + currencyreduction: { + ...prev.currencyreduction, + value: formatNumber(normalized.currencyReductionSum), + }, + })); setStats(normalized); } catch (error) { console.error("Error fetching stats:", error); @@ -445,16 +476,6 @@ export function ProcessInnovationPage() { } }; - const handleRefresh = () => { - fetchingRef.current = false; - setCurrentPage(1); - setProjects([]); - setHasMore(true); - fetchProjects(true); - fetchTotalCount(); - fetchStats(); - }; - const formatCurrency = (amount: string | number) => { if (!amount) return "0 ریال"; const numericAmount = @@ -465,34 +486,28 @@ export function ProcessInnovationPage() { return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; }; - const formatPercentage = (value: string | number) => { - if (!value) return "0%"; - const numericValue = typeof value === "string" ? parseFloat(value) : value; - if (isNaN(numericValue)) return "0%"; - return `${numericValue.toFixed(1)}%`; - }; - - const getStatusColor = (status: string) => { - switch (status?.toLowerCase()) { - case "فعال": - return "#3AEA83"; - case "متوقف": - return "#F76276"; - case "تکمیل شده": - return "#32CD32"; - default: - return "#6B7280"; + 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"; } - }; - - const getRatingColor = (rating: string) => { - const ratingNum = parseFloat(rating); - if (isNaN(ratingNum)) return "#6B7280"; - - if (ratingNum >= 8) return "#3AEA83"; - if (ratingNum >= 6) return "#69C8EA"; - if (ratingNum >= 4) return "#FFD700"; - return "#F76276"; + return el; }; const renderCellContent = (item: ProcessInnovationData, column: any) => { @@ -502,8 +517,8 @@ export function ProcessInnovationPage() { case "select": return ( handleSelectProject(item.project_no)} + checked={selectedProjects.has(item.project_id)} + onCheckedChange={() => handleSelectProject(item.project_id)} className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" /> ); @@ -534,15 +549,16 @@ export function ProcessInnovationPage() { return {String(value)}; case "project_status": return ( - +
+ {String(value)} - +
); case "project_rating": return ( @@ -603,7 +619,7 @@ export function ProcessInnovationPage() { )) - : statsCards.map((card) => ( + : Object.entries(stateCard).map(([key, card]) => ( - {/* Selection Summary */} - {/* {selectedProjects.size > 0 && ( -
-
-
-
- - {selectedProjects.size} پروژه انتخاب شده - -
-
- -
-
-
- )} */} {/* Footer */} +
-
-
+
+
- {" "} - کل پروژه ها :{" "} - {formatNumber(stats.totalProjects || actualTotalCount)} -
-
- {/* Project number column - empty */} -
- {/* Title column - empty */} -
- {/* Project status column - empty */} -
- {/* Project rating column - show average */} -
-
- {" "} - میانگین امتیاز :‌ -
-
- {formatNumber( - ((stats.averageScore ?? 0) as number).toFixed?.(1) ?? - stats.averageScore ?? - 0 - )} + کل پروژه ها :{formatNumber(actualTotalCount)}
- {/* Details column - show total count */} +
+
+ + + + +
+
+
میانگین :‌
+
+ {formatNumber( + ((stats.averageScore ?? 0) as number).toFixed?.(1) ?? 0 + )} +
+
+
diff --git a/app/components/ui/custom-bar-chart.tsx b/app/components/ui/custom-bar-chart.tsx index 6ba5f18..dfc053d 100644 --- a/app/components/ui/custom-bar-chart.tsx +++ b/app/components/ui/custom-bar-chart.tsx @@ -1,4 +1,3 @@ -import * as React from "react"; import { formatNumber } from "~/lib/utils"; export interface BarChartData { @@ -80,7 +79,7 @@ export function CustomBarChart({ {data.map((item, index) => { const percentage = globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0; - const displayValue = item.value.toFixed(1); + const displayValue: any = item.value; return (
diff --git a/app/lib/utils.ts b/app/lib/utils.ts index b771a69..2cee588 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) { } export const formatNumber = (value: string | number) => { - if (!value) return "0"; + // 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); From cc163a19f0aad93d34e2154a16ad6bdfb2bfaa69 Mon Sep 17 00:00:00 2001 From: mehrdad_adabi Date: Mon, 8 Sep 2025 06:53:51 +0330 Subject: [PATCH 2/2] feat: create ideas and tech page --- .../mange-ideas-tech-page.tsx | 794 ++++++++++++++++++ app/components/dashboard/sidebar.tsx | 87 +- app/routes.ts | 3 +- app/routes/manage-ideas-tech-page.tsx | 17 + 4 files changed, 848 insertions(+), 53 deletions(-) create mode 100644 app/components/dashboard/project-management/mange-ideas-tech-page.tsx create mode 100644 app/routes/manage-ideas-tech-page.tsx diff --git a/app/components/dashboard/project-management/mange-ideas-tech-page.tsx b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx new file mode 100644 index 0000000..0d9bc8d --- /dev/null +++ b/app/components/dashboard/project-management/mange-ideas-tech-page.tsx @@ -0,0 +1,794 @@ +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 { 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_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: "idea_status", + label: "وضعیت ایده", + sortable: true, + width: "260px", + }, + { + 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", + }, +]; + +// idea_income , idea_registration_date , idea_originality , personnel_number + +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_no)"], + 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].project_no_count || 0); + } + } catch (parseError) { + console.error("Error parsing count data:", parseError); + } + } + } + } catch (error) { + console.error("Error fetching total count:", error); + } + }; + + const formatCurrency = (amount: string | number) => { + if (!amount) return "0 ریال"; + // Remove commas and convert to number + const numericAmount = + typeof amount === "string" + ? parseFloat(amount.replace(/,/g, "")) + : amount; + if (isNaN(numericAmount)) return "0 ریال"; + return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; + }; + + // const formatNumber = (value: string | number) => { + // if (value === undefined || value === null || value === "") return "0"; + // const numericValue = typeof value === "string" ? Number(value) : value; + // if (Number.isNaN(numericValue)) return "0"; + // return new Intl.NumberFormat("fa-IR").format(numericValue as number); + // }; + + 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 "#3AEA83"; // سبز + case "در حال بررسی": + return "#69C8EA"; // آبی + case "رد شده": + return "#F76276"; // قرمز + case "اجرا شده": + return "#FBBF24"; // زرد/نارنجی + default: + return "#6B7280"; // خاکستری پیش‌فرض + } + }; + + // 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 ? "#3AEA83" : days < 0 ? "#F76276" : undefined; + return ( + + روز {toPersianDigits(days)} + + ); + } + // case "strategic_theme": + // case "value_technology_and_innovation": + // case "type_of_innovation": + // case "innovation": + // return ( + // + // {String(value) || "-"} + // + // + // ); + case "idea_income": + return ( + + {formatCurrency(String(value))} + + ); + case "personnel_number": + // case "idea_originality": + return ( + {formatNumber(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)} +
+
+
+
+
+ ); +} diff --git a/app/components/dashboard/sidebar.tsx b/app/components/dashboard/sidebar.tsx index 717f204..43a0878 100644 --- a/app/components/dashboard/sidebar.tsx +++ b/app/components/dashboard/sidebar.tsx @@ -1,40 +1,25 @@ +import { + Box, + Building2, + ChevronDown, + ChevronRight, + FolderKanban, + GalleryVerticalEnd, + Globe, + LayoutDashboard, + Leaf, + Lightbulb, + LogOut, + MonitorSmartphone, + Package, + Settings, + Star, + Workflow, +} from "lucide-react"; import React, { useState } from "react"; import { Link, useLocation } from "react-router"; -import { cn } from "~/lib/utils"; -import { InogenLogo } from "~/components/ui/brand-logo"; import { useAuth } from "~/contexts/auth-context"; -import { - GalleryVerticalEnd, - LayoutDashboard, - FolderOpen, - Users, - BarChart3, - Settings, - ChevronLeft, - ChevronDown, - FileText, - Calendar, - Bell, - User, - Database, - Shield, - HelpCircle, - LogOut, - ChevronRight, - Refrigerator, -} from "lucide-react"; -import { - FolderKanban, - Box, - Package, - Workflow, - MonitorSmartphone, - Leaf, - Building2, - Globe, - Lightbulb, - Star, -} from "lucide-react"; +import { cn } from "~/lib/utils"; interface SidebarProps { isCollapsed?: boolean; @@ -111,7 +96,7 @@ const menuItems: MenuItem[] = [ id: "ideas", label: "ایده‌های فناوری و نوآوری", icon: Lightbulb, - href: "/dashboard/ideas", + href: "/dashboard/manage-ideas-tech", }, { id: "top-innovations", @@ -153,7 +138,7 @@ export function Sidebar({ menuItems.forEach((item) => { if (item.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href, + (child) => child.href && location.pathname === child.href ); if (hasActiveChild) { newExpandedItems.push(item.id); @@ -174,7 +159,7 @@ export function Sidebar({ const item = menuItems.find((menuItem) => menuItem.id === itemId); if (item?.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href, + (child) => child.href && location.pathname === child.href ); // Don't collapse if a child is active if (hasActiveChild) { @@ -192,7 +177,7 @@ export function Sidebar({ if (href && location.pathname === href) return true; if (children) { return children.some( - (child) => child.href && location.pathname === child.href, + (child) => child.href && location.pathname === child.href ); } return false; @@ -204,7 +189,7 @@ export function Sidebar({ expandedItems.includes(item.id) || (item.children && item.children.some( - (child) => child.href && location.pathname === child.href, + (child) => child.href && location.pathname === child.href )); const hasChildren = item.children && item.children.length > 0; @@ -230,15 +215,14 @@ export function Sidebar({ ? " text-emerald-400 border-r-2 border-emerald-400" : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", isCollapsed && level === 0 && "justify-center px-2", - item.id === "logout" && - "hover:bg-red-500/10 hover:text-red-400", + item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400" )} >
{!isCollapsed && ( @@ -259,7 +243,7 @@ export function Sidebar({ )} @@ -274,9 +258,9 @@ export function Sidebar({ // Disable pointer cursor when child is active (cannot collapse) item.children && item.children.some( - (child) => child.href && location.pathname === child.href, + (child) => child.href && location.pathname === child.href ) && - "cursor-not-allowed", + "cursor-not-allowed" )} onClick={handleClick} > @@ -288,15 +272,14 @@ export function Sidebar({ ? " text-emerald-400 border-r-2 border-emerald-400" : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", isCollapsed && level === 0 && "justify-center px-2", - item.id === "logout" && - "hover:bg-red-500/10 hover:text-red-400", + item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400" )} >
{!isCollapsed && ( @@ -322,10 +305,10 @@ export function Sidebar({ item.children && item.children.some( (child) => - child.href && location.pathname === child.href, + child.href && location.pathname === child.href ) ? "text-emerald-400" - : "text-current", + : "text-current" )} /> )} @@ -360,7 +343,7 @@ export function Sidebar({ className={cn( " backdrop-blur-sm h-full flex flex-col transition-all duration-300", isCollapsed ? "w-16" : "w-64", - className, + className )} > {/* Header */} @@ -424,7 +407,7 @@ export function Sidebar({ {!isCollapsed && ( diff --git a/app/routes.ts b/app/routes.ts index 05a0524..7eff565 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -1,4 +1,4 @@ -import { type RouteConfig, index, route } from "@react-router/dev/routes"; +import { type RouteConfig, route } from "@react-router/dev/routes"; export default [ route("login", "routes/login.tsx"), @@ -21,6 +21,7 @@ export default [ "routes/digital-innovation-page.tsx" ), route("dashboard/ecosystem", "routes/ecosystem.tsx"), + route("dashboard/manage-ideas-tech", "routes/manage-ideas-tech-page.tsx"), route("404", "routes/404.tsx"), route("unauthorized", "routes/unauthorized.tsx"), route("*", "routes/$.tsx"), // Catch-all route for 404s diff --git a/app/routes/manage-ideas-tech-page.tsx b/app/routes/manage-ideas-tech-page.tsx new file mode 100644 index 0000000..f6473d9 --- /dev/null +++ b/app/routes/manage-ideas-tech-page.tsx @@ -0,0 +1,17 @@ +import { ProtectedRoute } from "~/components/auth/protected-route"; +import { ManageIdeasTechPage } from "~/components/dashboard/project-management/mange-ideas-tech-page"; + +export function meta() { + return [ + { title: "مدیریت فنواری و ایده ها" }, + { name: "description", content: "مدیریت پروژه‌های فناوری و نوآوری" }, + ]; +} + +export default function ManageIdeasTech() { + return ( + + + + ); +}