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);