dashboard_popup #8

Merged
Saeed0920 merged 3 commits from dashboard_popup into main 2025-09-09 02:23:55 +03:30
9 changed files with 1479 additions and 630 deletions
Showing only changes of commit d69a7c1e05 - Show all commits

View File

@ -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 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 { import {
Table, Table,
TableBody, TableBody,
@ -12,39 +26,24 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } 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 { import {
LoaderCircle,
TrendingUp,
Key,
Sparkle,
Zap,
Flame,
Building2, Building2,
PickaxeIcon,
UsersIcon,
UserIcon,
RefreshCw,
ChevronUp,
ChevronDown, ChevronDown,
ChevronUp,
Flame,
Key,
LoaderCircle,
PickaxeIcon,
RefreshCw,
Sparkle,
TrendingUp,
UserIcon,
UsersIcon,
Zap,
} from "lucide-react"; } from "lucide-react";
import toast from "react-hot-toast";
import apiService from "~/lib/api";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
@ -100,7 +99,7 @@ interface InnovationStats {
water_recovery_reduction_percent: number; water_recovery_reduction_percent: number;
average_project_score: number; average_project_score: number;
count_innovation_green_projects: number; count_innovation_green_projects: number;
standard_regulations: string standard_regulations: string;
} }
interface Params { interface Params {
@ -174,7 +173,9 @@ export function GreenInnovationPage() {
const [selectedProjects, setSelectedProjects] = useState<Set<string>>( const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set() new Set()
); );
const [standartRegulation, setStandardRegulation] = useState<Array<string>>([]) const [standartRegulation, setStandardRegulation] = useState<Array<string>>(
[]
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] = const [selectedProjectDetails, setSelectedProjectDetails] =
useState<GreenInnovationData | null>(null); useState<GreenInnovationData | null>(null);
@ -255,7 +256,6 @@ export function GreenInnovationPage() {
}; };
const formatNumber = (value: string | number) => { const formatNumber = (value: string | number) => {
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value; const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0"; if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue); return new Intl.NumberFormat("fa-IR").format(numericValue);
@ -269,7 +269,6 @@ export function GreenInnovationPage() {
try { try {
fetchingRef.current = true; fetchingRef.current = true;
if (reset) { if (reset) {
setCurrentPage(1); setCurrentPage(1);
} else { } else {
setLoadingMore(true); setLoadingMore(true);
@ -362,8 +361,11 @@ export function GreenInnovationPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
}, [sortConfig]);
useEffect(() => {
fetchStats(); fetchStats();
}, [sortConfig, selectedProjects]); }, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -398,7 +400,7 @@ export function GreenInnovationPage() {
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
}, []) }, []);
const handleSort = (field: string) => { const handleSort = (field: string) => {
fetchingRef.current = false; fetchingRef.current = false;
setSortConfig((prev) => ({ setSortConfig((prev) => ({
@ -454,15 +456,16 @@ export function GreenInnovationPage() {
if (typeof payload === "string") { if (typeof payload === "string") {
try { try {
payload = JSON.parse(payload); payload = JSON.parse(payload);
} catch { } } catch {}
} }
const parseNum = (v: unknown): any => { const parseNum = (v: unknown): any => {
const convertNumber = typeof v === "number" ? Math.max(0, v) : 0;
if (v == null) return 0; if (v == null) return 0;
if (typeof v === "number") return v; if (typeof v === "number") return convertNumber;
if (typeof v === "string") { if (typeof v === "string") {
const cleaned = v.replace(/,/g, "").trim(); const cleaned = v.replace(/,/g, "").trim();
const n = parseFloat(cleaned); const n = parseFloat(cleaned);
return isNaN(n) ? 0 : n; return isNaN(n) ? 0 : convertNumber;
} }
return 0; return 0;
}; };
@ -503,9 +506,11 @@ export function GreenInnovationPage() {
}, },
avarage: stats.average_project_score, avarage: stats.average_project_score,
countInnovationGreenProjects: stats.count_innovation_green_projects, 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); setActualTotalCount(normalized.countInnovationGreenProjects);
setTblAvarage(normalized.avarage); setTblAvarage(normalized.avarage);
setPageData(normalized); setPageData(normalized);
@ -689,68 +694,68 @@ export function GreenInnovationPage() {
<div className="flex flex-col justify-between w-1/2"> <div className="flex flex-col justify-between w-1/2">
{loading || statsLoading {loading || statsLoading
? // Loading skeleton for stats cards - matching new design ? // Loading skeleton for stats cards - matching new design
Array.from({ length: 2 }).map((_, index) => ( Array.from({ length: 2 }).map((_, index) => (
<Card <Card
key={`skeleton-${index}`} key={`skeleton-${index}`}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden" className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden"
> >
<CardContent className="p-0 h-[11.5rem]"> <CardContent className="p-0 h-[11.5rem]">
<div className="flex flex-col gap-2 h-full"> <div className="flex flex-col gap-2 h-full">
<div className="border-b-2 border-gray-500/20 p-2.5"> <div className="border-b-2 border-gray-500/20 p-2.5">
<div <div
className="h-6 bg-gray-600 rounded animate-pulse" className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }} style={{ width: "60%" }}
/> />
</div>
<div className="flex items-center justify-center flex-col p-2.5 mt-4">
<div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div> </div>
<div className="flex items-center justify-center flex-col p-2.5 mt-4"> </CardContent>
<div </Card>
className="h-8 bg-gray-600 rounded mb-1 animate-pulse" ))
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div>
</CardContent>
</Card>
))
: Object.entries(sustainabilityStats).map(([key, value]) => ( : Object.entries(sustainabilityStats).map(([key, value]) => (
<Card <Card
key={key} key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50" className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
> >
<CardContent className="p-0 h-full"> <CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full"> <div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 "> <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
<h3 className="text-lg font-bold text-white font-persian p-4"> <h3 className="text-lg font-bold text-white font-persian p-4">
{value.title} {value.title}
</h3> </h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div> </div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" /> <div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian"> <span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
{value.total?.value} % {value.percent?.value}
</span> </span>
<span className="text-sm text-gray-400 font-persian"> <span className="text-sm text-gray-400 font-persian">
{value.total?.description} {value.percent?.description}
</span> </span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-400 mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div> </div>
</div> </div>
</div> </CardContent>
</CardContent> </Card>
</Card> ))}
))}
</div> </div>
{/* Process Impacts Chart */} {/* Process Impacts Chart */}
@ -880,7 +885,8 @@ export function GreenInnovationPage() {
position: "top", position: "top",
fill: "#fff", fill: "#fff",
fontWeight: "bold", fontWeight: "bold",
formatter: (value: any) => `${formatNumber(value)}%`, formatter: (value: any) =>
`${formatNumber(value)}%`,
}} }}
/> />
</BarChart> </BarChart>
@ -912,21 +918,27 @@ export function GreenInnovationPage() {
<div className="flex flex-col gap-3 p-4 overflow-y-scroll h-[20rem]"> <div className="flex flex-col gap-3 p-4 overflow-y-scroll h-[20rem]">
{statsLoading {statsLoading
? Array.from({ length: 10 }).map((_, index) => ( ? Array.from({ length: 10 }).map((_, index) => (
<div key={`skeleton-${index}`} className="flex gap-2 items-center"> <div
<span className="h-4 w-4 bg-gray-500/40 rounded-full animate-pulse"></span> key={`skeleton-${index}`}
<span className="h-3 w-32 bg-gray-500/40 rounded animate-pulse"></span> className="flex gap-2 items-center"
</div> >
)) <span className="h-4 w-4 bg-gray-500/40 rounded-full animate-pulse"></span>
<span className="h-3 w-32 bg-gray-500/40 rounded animate-pulse"></span>
</div>
))
: standartRegulation.map((item, index) => ( : standartRegulation.map((item, index) => (
<div key={`${item}-${index}-1`} className="flex flex-row flex-1 gap-2"> <div
<LoaderCircle key={`${item}-${index}-1`}
size={"20px"} className="flex flex-row gap-2"
height={"20px"} >
className="text-emerald-400 w-5 h-5 shrink-0" <LoaderCircle
/> size={"20px"}
<span className="text-sm truncate">{item}</span> height={"20px"}
</div> className="text-emerald-400 w-5 h-5 shrink-0"
))} />
<span className="text-sm truncate">{item}</span>
</div>
))}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -1121,9 +1133,9 @@ export function GreenInnovationPage() {
<span className="text-white font-bold font-persian"> <span className="text-white font-bold font-persian">
{selectedProjectDetails?.start_date {selectedProjectDetails?.start_date
? moment( ? moment(
selectedProjectDetails?.start_date, selectedProjectDetails?.start_date,
"YYYY-MM-DD" "YYYY-MM-DD"
).format("YYYY/MM/DD") ).format("YYYY/MM/DD")
: "-"} : "-"}
</span> </span>
</div> </div>
@ -1136,9 +1148,9 @@ export function GreenInnovationPage() {
<span className="text-white font-bold font-persian"> <span className="text-white font-bold font-persian">
{selectedProjectDetails?.done_date {selectedProjectDetails?.done_date
? moment( ? moment(
selectedProjectDetails?.done_date, selectedProjectDetails?.done_date,
"YYYY-MM-DD" "YYYY-MM-DD"
).format("YYYY/MM/DD") ).format("YYYY/MM/DD")
: "-"} : "-"}
</span> </span>
</div> </div>

View File

@ -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 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 { import {
Table, Table,
TableBody, TableBody,
@ -12,28 +18,29 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } 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 toast from "react-hot-toast";
import { import {
FilterIcon, Customized,
RefreshCw, Line,
ChevronUp, LineChart,
ChevronDown, ReferenceLine,
Handshake, ResponsiveContainer,
CodeXml, XAxis,
SquareUser, } from "recharts";
Users, import apiService from "~/lib/api";
User,
} from "lucide-react";
import DashboardLayout from "../layout"; import DashboardLayout from "../layout";
import { LineChart, CartesianGrid, Legend, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, Customized } from "recharts";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
@ -43,7 +50,7 @@ interface innovationBuiltInDate {
done_date: string | null; done_date: string | null;
observer: string; observer: string;
project_description: string; project_description: string;
project_id: number | null; project_id: number | string;
project_no: string; project_no: string;
project_rating: string; project_rating: string;
project_status: string; project_status: string;
@ -52,7 +59,7 @@ interface innovationBuiltInDate {
} }
interface DialogInfo { interface DialogInfo {
WorkflowID: number WorkflowID: number;
collaboration_model: string; collaboration_model: string;
complexity_level: string; complexity_level: string;
developer_team_role: string; developer_team_role: string;
@ -66,10 +73,9 @@ interface DialogInfo {
role_company_staff: string | null; role_company_staff: string | null;
technology_maturity_level: string; technology_maturity_level: string;
title: string; title: string;
technology_params?: Array<TechnologyParameter> technology_params?: Array<TechnologyParameter>;
} }
interface SortConfig { interface SortConfig {
field: string; field: string;
direction: "asc" | "desc"; direction: "asc" | "desc";
@ -98,18 +104,17 @@ interface BottleNeckItem {
value: string; value: string;
description?: string; description?: string;
unit?: string; unit?: string;
increasePercent: number increasePercent: number;
}; };
increaseIncome: { increaseIncome: {
label: string; label: string;
value: string; value: string;
description?: string; description?: string;
increasePercent: number increasePercent: number;
unit?: string; unit?: string;
}; };
} }
interface StatsCard { interface StatsCard {
currencySaving: StateItem; currencySaving: StateItem;
investmentAmount: StateItem; investmentAmount: StateItem;
@ -166,11 +171,10 @@ const columns = [
]; ];
const dialogChartData = [ const dialogChartData = [
{ name: 'مرحه پیدایش', value: 10 }, { name: "مرحه پیدایش", value: 10 },
{ name: 'مرحله رشد', value: 14 }, { name: "مرحله رشد", value: 14 },
{ name: 'مرحله بلوغ', value: 25 }, { name: "مرحله بلوغ", value: 25 },
{ name:'مرحله افول', value: 15 }, { name: "مرحله افول", value: 15 },
]; ];
export function InnovationBuiltInsidePage() { export function InnovationBuiltInsidePage() {
@ -189,15 +193,13 @@ export function InnovationBuiltInsidePage() {
direction: "asc", direction: "asc",
}); });
const [tblAvarage, setTblAvarage] = useState<number>(0); const [tblAvarage, setTblAvarage] = useState<number>(0);
const [selectedProjects, setSelectedProjects] = useState<Set<string>>( const [selectedProjects, setSelectedProjects] = useState<
new Set() Set<string | number>
); >(new Set());
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] = const [selectedProjectDetails, setSelectedProjectDetails] =
useState<DialogInfo>(); useState<DialogInfo>();
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({ const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
currencySaving: { currencySaving: {
id: "reduce-pollution", id: "reduce-pollution",
@ -228,35 +230,34 @@ export function InnovationBuiltInsidePage() {
const [bottleNeck, setBottleNeck] = useState<BottleNeckItem>({ const [bottleNeck, setBottleNeck] = useState<BottleNeckItem>({
resolveBottleNeck: { resolveBottleNeck: {
label: 'تعدادگلوگاه رفع شده', label: "تعدادگلوگاه رفع شده",
value: '0', value: "0",
description: '' description: "",
}, },
increaseCapacity: { increaseCapacity: {
label: 'ظرفیت تولید اضافه شده', label: "ظرفیت تولید اضافه شده",
value: '0', value: "0",
description: 'درصد افزایش ظرفیت تولید', description: "درصد افزایش ظرفیت تولید",
increasePercent: 0, increasePercent: 0,
unit: 'تن' unit: "تن",
}, },
increaseIncome: { increaseIncome: {
label: 'میزان افزایش درآمد', label: "میزان افزایش درآمد",
value: '0', value: "0",
description: 'درصد افزایش درآمد', description: "درصد افزایش درآمد",
increasePercent: 0, increasePercent: 0,
unit: "میلیون ریال" unit: "میلیون ریال",
} },
}) });
const [showDialogItems, setShowDialogItems] = useState<boolean>(false);
const [showDialogItems, setShowDialogItems] = useState<boolean>(false) const [countOfHighTech, setCountOfHighTech] = useState(0);
const [countOfHighTech, setCountOfHighTech] = useState(0)
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
const handleSelectProject = (projectNo: string) => { const handleSelectProject = (projectNo: string | number) => {
const newSelected = new Set(selectedProjects); const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) { if (newSelected.has(projectNo)) {
newSelected.delete(projectNo); newSelected.delete(projectNo);
@ -267,18 +268,16 @@ export function InnovationBuiltInsidePage() {
}; };
const handleProjectDetails = async (project: DialogInfo) => { const handleProjectDetails = async (project: DialogInfo) => {
setShowDialogItems(true) setShowDialogItems(true);
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setSelectedProjectDetails(project); setSelectedProjectDetails(project);
await fetchDialogTbl(project.WorkflowID) await fetchDialogTbl(project.WorkflowID);
setTimeout(() => { setTimeout(() => {
setShowDialogItems(false) setShowDialogItems(false);
calculateProgressBar(+project.project_rating) calculateProgressBar(+project.project_rating);
}, 500); }, 500);
}; };
const formatNumber = (value: string | number) => { const formatNumber = (value: string | number) => {
if (!value) return "0"; if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value; const numericValue = typeof value === "string" ? parseFloat(value) : value;
@ -315,7 +314,7 @@ export function InnovationBuiltInsidePage() {
"role_company_staff", "role_company_staff",
"number_employees_involved", "number_employees_involved",
"participants_full_name", "participants_full_name",
"technology_maturity_level" "technology_maturity_level",
], ],
Sorts: [[sortConfig.field, sortConfig.direction]], Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]], Conditions: [["type_of_innovation", "=", "نوآوری ساخت داخل"]],
@ -380,7 +379,6 @@ export function InnovationBuiltInsidePage() {
} }
}; };
const fetchDialogTbl = async (tblId: number) => { const fetchDialogTbl = async (tblId: number) => {
try { try {
const response = await apiService.select({ const response = await apiService.select({
@ -388,7 +386,7 @@ export function InnovationBuiltInsidePage() {
OutputFields: [ OutputFields: [
"technology_parameter_title", "technology_parameter_title",
"domestic_technology_parameter_value", "domestic_technology_parameter_value",
"foreign_technology_parameter_value" "foreign_technology_parameter_value",
], ],
Conditions: [["project_id", "=", tblId]], Conditions: [["project_id", "=", tblId]],
}); });
@ -398,7 +396,7 @@ export function InnovationBuiltInsidePage() {
const parsedData = JSON.parse(dataString); const parsedData = JSON.parse(dataString);
setSelectedProjectDetails((prev: any) => ({ setSelectedProjectDetails((prev: any) => ({
...prev, ...prev,
technology_params: parsedData technology_params: parsedData,
})); }));
} }
} }
@ -409,7 +407,9 @@ export function InnovationBuiltInsidePage() {
}; };
const calculateProgressBar = (val: number) => { const calculateProgressBar = (val: number) => {
const pointer = document.getElementsByClassName('progressBarPointer')[0] as HTMLElement; const pointer = document.getElementsByClassName(
"progressBarPointer"
)[0] as HTMLElement;
if (!pointer) return; if (!pointer) return;
const leftValue = val !== 0 ? `calc(${val}% - 100px)` : `calc(0% - 40px)`; const leftValue = val !== 0 ? `calc(${val}% - 100px)` : `calc(0% - 40px)`;
@ -430,7 +430,7 @@ export function InnovationBuiltInsidePage() {
useEffect(() => { useEffect(() => {
fetchStats(); fetchStats();
}, [selectedProjects]) }, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
@ -478,10 +478,9 @@ export function InnovationBuiltInsidePage() {
setHasMore(true); setHasMore(true);
}; };
const fetchStats = async () => { const fetchStats = async () => {
try { try {
detailsDialogOpen detailsDialogOpen;
setStatsLoading(true); setStatsLoading(true);
const raw = await apiService.call<any>({ const raw = await apiService.call<any>({
innovation_construction_inside_function: { innovation_construction_inside_function: {
@ -495,7 +494,7 @@ export function InnovationBuiltInsidePage() {
if (typeof payload === "string") { if (typeof payload === "string") {
try { try {
payload = JSON.parse(payload); payload = JSON.parse(payload);
} catch { } } catch {}
} }
const parseNum = (v: unknown): any => { const parseNum = (v: unknown): any => {
if (v == null) return 0; if (v == null) return 0;
@ -515,7 +514,9 @@ export function InnovationBuiltInsidePage() {
const normalized: any = { const normalized: any = {
currencySaving: { currencySaving: {
value: formatNumber(parseNum(stats?.foreign_currency_saving)), value: formatNumber(parseNum(stats?.foreign_currency_saving)),
percent: formatNumber(parseNum(stats?.foreign_currency_saving_percent)), percent: formatNumber(
parseNum(stats?.foreign_currency_saving_percent)
),
}, },
investmentAmount: { investmentAmount: {
@ -528,13 +529,21 @@ export function InnovationBuiltInsidePage() {
}, },
income: { income: {
value: formatNumber(parseNum(stats.increased_income_after_innovation)), value: formatNumber(
percent: formatNumber(parseNum(stats.increased_income_after_innovation_percent)), parseNum(stats.increased_income_after_innovation)
),
percent: formatNumber(
parseNum(stats.increased_income_after_innovation_percent)
),
}, },
capacity: { capacity: {
value: formatNumber(parseNum(stats.increased_capacity_after_innovation)), value: formatNumber(
percent: formatNumber(parseNum(stats.increased_capacity_after_innovation_percent)), parseNum(stats.increased_capacity_after_innovation)
),
percent: formatNumber(
parseNum(stats.increased_capacity_after_innovation_percent)
),
}, },
resolveBottleNeck: { resolveBottleNeck: {
@ -542,7 +551,8 @@ export function InnovationBuiltInsidePage() {
}, },
countOfHighTech: formatNumber(stats.high_level_technology_count), countOfHighTech: formatNumber(stats.high_level_technology_count),
avarage: stats.average_project_score, avarage: stats.average_project_score,
countInnovationGreenProjects: stats.count_innovation_construction_inside_projects, countInnovationGreenProjects:
stats.count_innovation_construction_inside_projects,
}; };
setActualTotalCount(normalized.countInnovationGreenProjects); setActualTotalCount(normalized.countInnovationGreenProjects);
setTblAvarage(normalized.avarage); setTblAvarage(normalized.avarage);
@ -559,7 +569,10 @@ export function InnovationBuiltInsidePage() {
...prev, ...prev,
currencySaving: { currencySaving: {
...prev.currencySaving, ...prev.currencySaving,
total: { ...prev.currencySaving.total, value: normalized.currencySaving.value }, total: {
...prev.currencySaving.total,
value: normalized.currencySaving.value,
},
percent: { percent: {
...prev.currencySaving.percent, ...prev.currencySaving.percent,
value: normalized.currencySaving.percent, value: normalized.currencySaving.percent,
@ -567,23 +580,29 @@ export function InnovationBuiltInsidePage() {
}, },
investmentAmount: { investmentAmount: {
...prev.investmentAmount, ...prev.investmentAmount,
total: { ...prev.investmentAmount.total, value: normalized.investmentAmount.value }, total: {
percent: { ...prev.investmentAmount.percent, value: normalized.investmentAmount.percent }, ...prev.investmentAmount.total,
value: normalized.investmentAmount.value,
},
percent: {
...prev.investmentAmount.percent,
value: normalized.investmentAmount.percent,
},
}, },
})); }));
setBottleNeck(prev => ({ setBottleNeck((prev) => ({
...prev, ...prev,
increaseIncome: { increaseIncome: {
...prev.increaseIncome, ...prev.increaseIncome,
value: normalized.income.value, value: normalized.income.value,
increasePercent: normalized.income.percent increasePercent: normalized.income.percent,
}, },
increaseCapacity: { increaseCapacity: {
...prev.increaseCapacity, ...prev.increaseCapacity,
value: normalized.capacity.value, value: normalized.capacity.value,
increasePercent: normalized.capacity.percent increasePercent: normalized.capacity.percent,
}, },
resolveBottleNeck: { resolveBottleNeck: {
...prev.resolveBottleNeck, ...prev.resolveBottleNeck,
@ -592,7 +611,7 @@ export function InnovationBuiltInsidePage() {
// average: normalized.avarage, // average: normalized.avarage,
// countInnovationGreenProjects: normalized.countInnovationGreenProjects, // countInnovationGreenProjects: normalized.countInnovationGreenProjects,
})); }));
setCountOfHighTech(normalized.countOfHighTech) setCountOfHighTech(normalized.countOfHighTech);
}; };
const renderCellContent = (item: innovationBuiltInDate, column: any) => { const renderCellContent = (item: innovationBuiltInDate, column: any) => {
@ -602,8 +621,8 @@ export function InnovationBuiltInsidePage() {
case "select": case "select":
return ( return (
<Checkbox <Checkbox
checked={selectedProjects.has(item.project_id)} checked={selectedProjects.has(item?.project_id!)}
onCheckedChange={() => handleSelectProject(item.project_id)} onCheckedChange={() => handleSelectProject(item?.project_id)}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600 cursor-pointer" 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 el;
}; };
return ( return (
<DashboardLayout title="نوآوری ساخت داخل"> <DashboardLayout title="نوآوری ساخت داخل">
<div className="p-6 space-y-4 flex justify-between gap-8 sm:flex-col xl:flex-row"> <div className="p-6 space-y-4 flex justify-between gap-8 sm:flex-col xl:flex-row">
@ -697,72 +715,71 @@ export function InnovationBuiltInsidePage() {
<div className="flex flex-col justify-between w-full gap-6"> <div className="flex flex-col justify-between w-full gap-6">
{statsLoading {statsLoading
? // Loading skeleton for stats cards - matching new design ? // Loading skeleton for stats cards - matching new design
Array.from({ length: 2 }).map((_, index) => ( Array.from({ length: 2 }).map((_, index) => (
<Card <Card
key={`skeleton-${index}`} key={`skeleton-${index}`}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden" className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-lg overflow-hidden"
> >
<CardContent className="p-0 h-[11.5rem]"> <CardContent className="p-0 h-[11.5rem]">
<div className="flex flex-col gap-2 h-full"> <div className="flex flex-col gap-2 h-full">
<div className="border-b-2 border-gray-500/20 p-2.5"> <div className="border-b-2 border-gray-500/20 p-2.5">
<div <div
className="h-6 bg-gray-600 rounded animate-pulse" className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }} style={{ width: "60%" }}
/> />
</div>
<div className="flex items-center justify-center flex-col p-2.5 mt-4">
<div
className="h-8 bg-gray-600 rounded mb-1 animate-pulse"
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div> </div>
<div className="flex items-center justify-center flex-col p-2.5 mt-4"> </CardContent>
<div </Card>
className="h-8 bg-gray-600 rounded mb-1 animate-pulse" ))
style={{ width: "40%" }}
/>
<div
className="h-4 bg-gray-600 rounded animate-pulse"
style={{ width: "80%" }}
/>
</div>
</div>
</CardContent>
</Card>
))
: Object.entries(sustainabilityStats).map(([key, value]) => ( : Object.entries(sustainabilityStats).map(([key, value]) => (
<Card <Card
key={key} key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50" className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"
> >
<CardContent className="p-0 h-full"> <CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full"> <div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 "> <div className="flex justify-between items-center border-b-2 border-gray-500/20 ">
<h3 className="text-lg font-semibold text-white p-4"> <h3 className="text-lg font-semibold text-white p-4">
{value.title} {value.title}
</h3> </h3>
</div>
<div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
% {value.percent?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.percent?.description}
</span>
</div> </div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" /> <div className="flex items-center justify-between p-6 flex-row-reverse">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian"> <span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
{value.total?.value} % {value.percent?.value}
</span> </span>
<span className="text-sm text-gray-400 font-persian"> <span className="text-sm text-gray-400 font-persian">
{value.total?.description} {value.percent?.description}
</span> </span>
</div>
<b className="block w-0.5 h-8 bg-gray-600 rotate-45" />
<div className="flex flex-col">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
{value.total?.value}
</span>
<span className="text-sm text-gray-400 font-persian">
{value.total?.description}
</span>
</div>
</div> </div>
</div> </div>
</div> </CardContent>
</CardContent> </Card>
</Card> ))}
))}
{statsLoading ? (
{ <div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse">
statsLoading ? <div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse">
<div className="p-0 h-full"> <div className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full"> <div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5"> <div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5">
@ -788,7 +805,9 @@ export function InnovationBuiltInsidePage() {
</div> </div>
</div> </div>
</div> </div>
</div> : <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50"> </div>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] rounded-lg backdrop-blur-sm border-gray-700/50">
<CardContent className="p-0 h-full"> <CardContent className="p-0 h-full">
<div className="flex flex-col justify-between gap-2 h-full"> <div className="flex flex-col justify-between gap-2 h-full">
<div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5"> <div className="flex justify-between items-center border-b-2 border-gray-500/20 p-4 px-5">
@ -798,58 +817,57 @@ export function InnovationBuiltInsidePage() {
<FilterIcon /> <FilterIcon />
</div> </div>
<div className="flex items-center justify-between p-6 px-14 flex-col gap-4"> <div className="flex items-center justify-between p-6 px-14 flex-col gap-4">
{ {Object.entries(bottleNeck).map(([key, value]) => {
Object.entries(bottleNeck).map(([key, value]) => { return (
return <div key={`bottle-neck-${key}`} className="flex flex-row-reverse w-full justify-between"> <div
key={`bottle-neck-${key}`}
className="flex flex-row-reverse w-full justify-between"
>
<div className="flex flex-col text-left text-gray-400"> <div className="flex flex-col text-left text-gray-400">
<span className="text-3xl font-bold text-emerald-500 mb-1 font-persian"> <span className="text-3xl font-bold text-emerald-500 mb-1 font-persian">
{value.value} {value.value}
</span> </span>
{ {value.unit && (
value.unit && <span className="text-sm font-persian"> <span className="text-sm font-persian">
{value.unit} {value.unit}
</span> </span>
} )}
</div> </div>
<div className="text-sm font-persian flex flex-col gap-1"> <div className="text-sm font-persian flex flex-col gap-1">
<span className="text-lg">{value.label}</span> <span className="text-lg">{value.label}</span>
<div className="text-emerald-500 flex flex-row-reverse gap-1"> <div className="text-emerald-500 flex flex-row-reverse gap-1">
<span> <span>
{value.description && value.description}
{
value.description && value.description
}
</span>
<span>
{value.increasePercent}
</span> </span>
<span>{value.increasePercent}</span>
</div> </div>
</div> </div>
</div> </div>
}) );
} })}
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
} )}
{ {statsLoading ? (
statsLoading ? <div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse mt-4"> <div className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border border-gray-700/50 animate-pulse mt-4">
<div className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto"> <div className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto">
<div className="h-6 w-32 bg-gray-600/40 rounded-md"></div> <div className="h-6 w-32 bg-gray-600/40 rounded-md"></div>
<div className="h-8 w-16 bg-gray-600/40 rounded-md"></div> <div className="h-8 w-16 bg-gray-600/40 rounded-md"></div>
</div> </div>
</div> : <Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border-gray-700/50"> </div>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] h-30 rounded-lg backdrop-blur-sm border-gray-700/50">
<CardContent className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto"> <CardContent className="h-full flex flex-row justify-between p-6 items-center w-4/5 m-auto">
<span className="text-lg">تعداد فناوری سطح بالا</span> <span className="text-lg">تعداد فناوری سطح بالا</span>
<span className="text-emerald-500 text-3xl font-bold font-persian">{countOfHighTech}</span> <span className="text-emerald-500 text-3xl font-bold font-persian">
{countOfHighTech}
</span>
</CardContent> </CardContent>
</Card> </Card>
} )}
</div> </div>
</div> </div>
@ -960,13 +978,13 @@ export function InnovationBuiltInsidePage() {
<div className="p-2 px-4 bg-gray-700/50"> <div className="p-2 px-4 bg-gray-700/50">
<div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row"> <div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
<div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full"> <div className="text-center items-center xl:w-full pr-6 sm:w-full">
<div className="text-base text-gray-401 mb-1"> <div className="text-base text-gray-401 mb-1">
کل پروژه ها :{formatNumber(actualTotalCount)} کل پروژه ها :{formatNumber(actualTotalCount)}
</div> </div>
</div> </div>
<div className="flex items-center flex-row gap-20 status justify-center xl:w-2/3 sm:w-full"> <div className="flex items-center flex-row gap-5 status justify-start xl:w-full sm:w-full ">
<div className="flex flex-row-reverse"> <div className="flex flex-row-reverse">
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span> <span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
<span className="block w-7 h-2.5 bg-purple-500 "></span> <span className="block w-7 h-2.5 bg-purple-500 "></span>
@ -984,8 +1002,6 @@ export function InnovationBuiltInsidePage() {
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
</div> </div>
@ -999,32 +1015,30 @@ export function InnovationBuiltInsidePage() {
</DialogHeader> </DialogHeader>
<div className="flex justify-between text-right px-4"> <div className="flex justify-between text-right px-4">
<div className="flex-[4] border-l-2 border-gray-600 pl-8"> <div className="flex-[4] border-l-2 border-gray-600 pl-8">
{showDialogItems ? (
{ <div className="animate-pulse flex flex-col gap-2">
showDialogItems ? <div className="animate-pulse flex flex-col gap-2">
<div className="flex flex-col gap-2 mt-2"> <div className="flex flex-col gap-2 mt-2">
<div className="h-4 w-full bg-gray-600 rounded"></div> <div className="h-4 w-full bg-gray-600 rounded"></div>
<div className="h-4 w-5/6 bg-gray-600 rounded"></div> <div className="h-4 w-5/6 bg-gray-600 rounded"></div>
<div className="h-4 w-11/12 bg-gray-600 rounded"></div> <div className="h-4 w-11/12 bg-gray-600 rounded"></div>
</div> </div>
</div> : <div> </div>
) : (
<div>
<h2 className="font-bold">{selectedProjectDetails?.title}</h2> <h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian mt-2 text-justify"> <p className="text-gray-300 font-persian mt-2 text-justify">
{selectedProjectDetails?.project_description || "-"} {selectedProjectDetails?.project_description || "-"}
</p> </p>
</div> </div>
} )}
<h2 className="font-bold my-6">دانش فنی محصول جدید</h2> <h2 className="font-bold my-6">دانش فنی محصول جدید</h2>
{ {showDialogItems ? (
showDialogItems ? <div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full animate-pulse"> <div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full animate-pulse">
<div className="w-max relative"> <div className="w-max relative">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center"> <div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-gray-500 w-28 h-10 p-4 text-center flex items-center justify-center"> <div className="bg-gray-500 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
</div> <div className="bg-gray-400 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
<div className="bg-gray-400 w-28 h-10 p-4 text-center flex items-center justify-center"> <div className="bg-gray-300 w-28 h-10 p-4 text-center flex items-center justify-center"></div>
</div>
<div className="bg-gray-300 w-28 h-10 p-4 text-center flex items-center justify-center">
</div>
</div> </div>
<div className="progressBarPointer absolute z-[200] top-0.5 left-0.5"> <div className="progressBarPointer absolute z-[200] top-0.5 left-0.5">
@ -1036,7 +1050,9 @@ export function InnovationBuiltInsidePage() {
</div> </div>
</div> </div>
</div> </div>
</div> : <div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full "> </div>
) : (
<div className="newProductTechKnowledge flex flex-col gap-4 h-max w-full ">
<div className="w-max relative transition-all duration-300"> <div className="w-max relative transition-all duration-300">
<div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center"> <div className="range flex flex-row-reverse rounded-xl overflow-hidden justify-center">
<div className="bg-emerald-700 w-28 h-10 p-4 text-center text-sm flex items-center justify-center"> <div className="bg-emerald-700 w-28 h-10 p-4 text-center text-sm flex items-center justify-center">
@ -1049,20 +1065,23 @@ export function InnovationBuiltInsidePage() {
<span className="text-gray-700">سطح بالا</span> <span className="text-gray-700">سطح بالا</span>
</div> </div>
</div> </div>
<div className='progressBarPointer absolute z-[200] top-0.5 transition-all duration-300'> <div className="progressBarPointer absolute z-[200] top-0.5 transition-all duration-300">
<div className="flex flex-col justify-center items-center"> <div className="flex flex-col justify-center items-center">
<span className="block w-0.5 h-14 bg-white"></span> <span className="block w-0.5 h-14 bg-white"></span>
<span className="text-white border border-white p-1 px-2 text-xs rounded-lg"> سطح تکنولوژی</span> <span className="text-white border border-white p-1 px-2 text-xs rounded-lg">
{" "}
سطح تکنولوژی
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
} )}
<div className="flex flex-col gap-5 mt-20"> <div className="flex flex-col gap-5 mt-20">
<h2 className="font-bold">مشارکت در پروژه</h2> <h2 className="font-bold">مشارکت در پروژه</h2>
{ {showDialogItems ? (
showDialogItems ? <div className="space-y-3"> <div className="space-y-3">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div key={i} className="flex flex-row gap-2 items-center"> <div key={i} className="flex flex-row gap-2 items-center">
<div className="h-5 w-5 bg-gray-500 rounded-full"></div> <div className="h-5 w-5 bg-gray-500 rounded-full"></div>
@ -1072,34 +1091,45 @@ export function InnovationBuiltInsidePage() {
</div> </div>
</div> </div>
))} ))}
</div> : <div className="flex flex-col gap-5"> </div>
) : (
<div className="flex flex-col gap-5">
<div className="content flex flex-col gap-3 w-5/6"> <div className="content flex flex-col gap-3 w-5/6">
<div className="flex flex-row gap-2 w-full"> <div className="flex flex-row gap-2 w-full">
<Handshake className="text-emerald-500 w-5" /> <Handshake className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<span>مدل همکاری:</span> <span>مدل همکاری:</span>
<span>{selectedProjectDetails?.collaboration_model}</span> <span>
{selectedProjectDetails?.collaboration_model}
</span>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2 w-full"> <div className="flex flex-row gap-2 w-full">
<CodeXml className="text-emerald-500 w-5" /> <CodeXml className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<span>نقش تیم توسعه دهنده:</span> <span>نقش تیم توسعه دهنده:</span>
<span>{selectedProjectDetails?.developer_team_role}</span> <span>
{selectedProjectDetails?.developer_team_role}
</span>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2 w-full"> <div className="flex flex-row gap-2 w-full">
<SquareUser className="text-emerald-500 w-5" /> <SquareUser className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<span>نقش کارکنان شرکت: </span> <span>نقش کارکنان شرکت: </span>
<span>{selectedProjectDetails?.role_company_staff ?? '-'}</span> <span>
{selectedProjectDetails?.role_company_staff ?? "-"}
</span>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2 w-full"> <div className="flex flex-row gap-2 w-full">
<Users className="text-emerald-500 w-5" /> <Users className="text-emerald-500 w-5" />
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<span>تعداد کارکنان درگیر: </span> <span>تعداد کارکنان درگیر: </span>
<span className="persian-font">{selectedProjectDetails?.number_employees_involved ?? 0}</span> <span className="persian-font">
{selectedProjectDetails?.number_employees_involved ??
0}
</span>
</div> </div>
</div> </div>
</div> </div>
@ -1108,59 +1138,76 @@ export function InnovationBuiltInsidePage() {
<User className="text-emerald-500 w-5" /> <User className="text-emerald-500 w-5" />
<div className="flex flex-col justify-between w-full gap-1"> <div className="flex flex-col justify-between w-full gap-1">
<span>لیست کارکنان : </span> <span>لیست کارکنان : </span>
<span className="pr-1">{selectedProjectDetails?.participants_full_name}</span> <span className="pr-1">
{selectedProjectDetails?.participants_full_name}
</span>
</div> </div>
</div> </div>
</div> </div>
} )}
</div> </div>
</div> </div>
{/* Project Details */} {/* Project Details */}
<div className="px-4 pl-0 flex flex-col gap-4 w-1/2"> <div className="px-4 pl-0 flex flex-col gap-4 w-1/2">
{ {showDialogItems ? (
showDialogItems ? <div className="flex flex-col gap-2 animate-pulse"> <div className="flex flex-col gap-2 animate-pulse">
{[...Array(5)].map((_, rowIndex) => ( {[...Array(5)].map((_, rowIndex) => (
<div key={`skeleton-${rowIndex}`}> <div key={`skeleton-${rowIndex}`}>
<div className="h-6 bg-gray-700 rounded"></div> <div className="h-6 bg-gray-700 rounded"></div>
</div> </div>
))} ))}
</div> : <div className="tbl rounded-xl overflow-hidden"> </div>
) : (
<div className="tbl rounded-xl overflow-hidden">
<div className="grid grid-cols-3 bg-[#3F415A] text-white"> <div className="grid grid-cols-3 bg-[#3F415A] text-white">
<span className="text-md text-center p-3">شاخص مقایسه با نمونه خارجی</span> <span className="text-md text-center p-3">
شاخص مقایسه با نمونه خارجی
</span>
<span className="text-md text-center p-3">نمونه داخلی</span> <span className="text-md text-center p-3">نمونه داخلی</span>
<span className="text-md text-center p-3">نمونه خارجی</span> <span className="text-md text-center p-3">نمونه خارجی</span>
</div> </div>
<div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]"> <div className="flex flex-col divide-y divide-gray-600 overflow-auto max-h-[6rem]">
{ {selectedProjectDetails?.technology_params?.map(
selectedProjectDetails?.technology_params?.map((el, index) => { (el, index) => {
return <div className="grid grid-cols-3 py-3" key={`technology-${index}-${el.WorkflowID}`}> return (
<span className="text-center">{el.technology_parameter_title}</span> <div
<div className="flex flex-row items-center gap-1 justify-center"> className="grid grid-cols-3 py-3"
<span>{formatNumber(el.domestic_technology_parameter_value)}</span> key={`technology-${index}-${el.WorkflowID}`}
<span className="text-sm text-gray-400">میلیون لیتر</span> >
<span className="text-center">
{el.technology_parameter_title}
</span>
<div className="flex flex-row items-center gap-1 justify-center">
<span>
{formatNumber(
el.domestic_technology_parameter_value
)}
</span>
<span className="text-sm text-gray-400">
میلیون لیتر
</span>
</div>
<div className="flex flex-row items-center gap-1 justify-center">
<span>
{formatNumber(
el.foreign_technology_parameter_value
)}
</span>
<span className="text-sm text-gray-400">
میلیون لیتر
</span>
</div>
</div> </div>
<div className="flex flex-row items-center gap-1 justify-center"> );
<span>{formatNumber(el.foreign_technology_parameter_value)}</span> }
<span className="text-sm text-gray-400">میلیون لیتر</span> )}
</div>
</div>
})
}
</div> </div>
</div> </div>
} )}
<div style={{ width: '100%', height: 400 }}> <div style={{ width: "100%", height: 400 }}>
{ {showDialogItems ? (
showDialogItems ? <div className="relative h-96 w-full bg-gray-700 rounded-lg overflow-hidden animate-pulse"> <div className="relative h-96 w-full bg-gray-700 rounded-lg overflow-hidden animate-pulse">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div <div
key={i} key={i}
@ -1177,14 +1224,16 @@ export function InnovationBuiltInsidePage() {
key={i} key={i}
className="absolute bg-gray-500 rounded-full" className="absolute bg-gray-500 rounded-full"
style={{ style={{
width: '10px', width: "10px",
height: '10px', height: "10px",
left: `${10 + i * 18}%`, left: `${10 + i * 18}%`,
top: `${30 + (i % 2) * 20}%`, top: `${30 + (i % 2) * 20}%`,
}} }}
/> />
))} ))}
</div> : <ResponsiveContainer width="100%" height={420}> </div>
) : (
<ResponsiveContainer width="100%" height={420}>
<LineChart <LineChart
data={dialogChartData} data={dialogChartData}
margin={{ top: 20, right: 20, left: 20, bottom: 80 }} margin={{ top: 20, right: 20, left: 20, bottom: 80 }}
@ -1220,19 +1269,27 @@ export function InnovationBuiltInsidePage() {
const xAxis: any = xAxes[0]; const xAxis: any = xAxes[0];
const ticks = xAxis?.ticks || []; const ticks = xAxis?.ticks || [];
const value = selectedProjectDetails?.technology_maturity_level; const value =
selectedProjectDetails?.technology_maturity_level;
const xFromScale = typeof xAxis?.scale === "function" ? xAxis.scale(value) : undefined; const xFromScale =
typeof xAxis?.scale === "function"
? xAxis.scale(value)
: undefined;
const tick = ticks.find( const tick = ticks.find(
(t: any) => (t: any) =>
t && 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 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 rectWidth = 140;
const rectHeight = 28; const rectHeight = 28;
@ -1267,14 +1324,13 @@ export function InnovationBuiltInsidePage() {
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
} )}
</div> </div>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</DashboardLayout > </DashboardLayout>
); );
} }

View File

@ -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<ProjectData[]>([]);
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<SortConfig>({
field: "idea_title",
direction: "asc",
});
const observerRef = useRef<HTMLDivElement>(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<string, string> = {
"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<string, string> = {
"تحقیق و توسعه": "#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 <span className="text-gray-300">-</span>;
}
const color = days > 0 ? "#3AEA83" : days < 0 ? "#F76276" : undefined;
return (
<span
dir="ltr"
className="font-medium flex justify-end gap-1 items-center"
style={{ color }}
>
<span>روز</span> {toPersianDigits(days)}
</span>
);
}
// case "strategic_theme":
// case "value_technology_and_innovation":
// case "type_of_innovation":
// case "innovation":
// return (
// <span className="inline-flex items-center justify-end flex-row-reverse gap-2 w-full">
// <span className="text-gray-300">{String(value) || "-"}</span>
// <span
// style={{
// backgroundColor: `${column.key === "strategic_theme" ? "#6D53FB" : column.key === "value_technology_and_innovation" ? "#A757FF" : column.key === "type_of_innovation" ? "#E884CE" : "#C3BF8B"}`,
// }}
// className="inline-block w-2 h-2 rounded-full bg-emerald-400"
// />
// </span>
// );
case "idea_income":
return (
<span className="font-medium text-emerald-400">
{formatCurrency(String(value))}
</span>
);
case "personnel_number":
// case "idea_originality":
return (
<span className="text-gray-300">{formatNumber(value as any)} </span>
);
case "idea_registration_date":
return (
<span className="text-gray-300">{formatDate(String(value))}</span>
);
case "project_no":
return (
<Badge
variant="outline"
className="font-mono text-emerald-400 border-emerald-500/50"
>
{String(value)}
</Badge>
);
case "idea_title":
return <span className="font-medium text-white">{String(value)}</span>;
case "idea_status":
return (
<Badge
variant="outline"
className="font-medium border-2"
style={{
color: getImportanceColor(String(value)),
borderColor: getImportanceColor(String(value)),
backgroundColor: `${getImportanceColor(String(value))}20`,
}}
>
{String(value)}
</Badge>
);
default:
return (
<span className="text-gray-300">
{(value && String(value)) || "-"}
</span>
);
}
};
const totalPages = Math.ceil(totalCount / pageSize);
return (
<DashboardLayout title="مدیریت ایده های فناوری و نوآوری">
<div className="p-6 space-y-6">
{/* Data Table */}
<Card className="bg-transparent backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="relative">
<Table containerClassName="overflow-auto custom-scrollbar max-h-[calc(100vh-200px)]">
<TableHeader className="sticky top-0 z-50 bg-[#3F415A]">
<TableRow className="bg-[#3F415A]">
{columns.map((column) => (
<TableHead
key={column.key}
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium bg-[#3F415A] sticky top-0 z-20"
style={{ width: column.width }}
>
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
>
<span>{column.label}</span>
{sortConfig.field === column.key ? (
sortConfig.direction === "asc" ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)
) : (
<div className="w-4 h-4" />
)}
</button>
) : (
column.label
)}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
// Skeleton loading rows (compact)
Array.from({ length: 20 }).map((_, index) => (
<TableRow
key={`skeleton-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 bg-gray-600 rounded-full animate-pulse" />
<div
className="h-2.5 bg-gray-600 rounded animate-pulse"
style={{ width: `${Math.random() * 60 + 40}%` }}
/>
</div>
</TableCell>
))}
</TableRow>
))
) : projects.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-8"
>
<span className="text-gray-400 font-persian">
هیچ پروژهای یافت نشد
</span>
</TableCell>
</TableRow>
) : (
projects.map((project, index) => (
<TableRow
key={`${project.project_no}-${index}`}
className="text-sm leading-tight h-8"
>
{columns.map((column) => (
<TableCell
key={column.key}
className="text-right whitespace-nowrap border-emerald-500/20 py-1 px-2"
>
{renderCellContent(project, column)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Infinite scroll trigger */}
<div ref={observerRef} className="h-auto">
{loadingMore && (
<div className="flex items-center justify-center py-1">
<div className="flex items-center gap-2">
<RefreshCw className="w-4 h-4 animate-spin text-emerald-400" />
<span className="font-persian text-gray-300 text-xs"></span>
</div>
</div>
)}
</div>
</CardContent>
{/* Footer */}
<div className="p-4 bg-gray-700/50">
<div className="flex items-center justify-between text-sm text-gray-300 font-persian">
<span>کل پروژهها: {formatNumber(actualTotalCount)}</span>
</div>
</div>
</Card>
</div>
</DashboardLayout>
);
}

View File

@ -1,12 +1,30 @@
import { useState, useEffect, useCallback, useRef } from "react"; import {
import { DashboardLayout } from "../layout"; Building2,
import { Card, CardContent } from "~/components/ui/card"; ChevronDown,
import { Button } from "~/components/ui/button"; 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 { 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 { Checkbox } from "~/components/ui/checkbox";
import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import moment from "moment-jalaali"; import {
import type { BarChartData } from "~/components/ui/custom-bar-chart"; Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { import {
Table, Table,
TableBody, TableBody,
@ -15,29 +33,14 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "~/components/ui/table"; } 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 apiService from "~/lib/api";
import toast from "react-hot-toast"; import { formatNumber } from "~/lib/utils";
import { Funnel, Wrench, CirclePause, DollarSign } from "lucide-react"; import { DashboardLayout } from "../layout";
moment.loadPersian({ usePersianDigits: true }); moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData { interface ProcessInnovationData {
project_no: string; project_no: string;
project_id: string;
title: string; title: string;
project_status: string; project_status: string;
project_rating: string; project_rating: string;
@ -48,18 +51,31 @@ interface ProcessInnovationData {
observer: string; 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 { interface SortConfig {
field: string; field: string;
direction: "asc" | "desc"; direction: "asc" | "desc";
} }
interface StatsCard { enum projectStatus {
id: string; propozal = "پروپوزال",
title: string; contract = "پیشنویس قرارداد",
value: string; inprogress = "در حال انجام",
description: string; stop = "متوقف شده",
icon: React.ReactNode; mafasa = "مرحله مفاصا",
color: string; finish = "پایان یافته",
} }
interface InnovationStats { interface InnovationStats {
@ -69,10 +85,10 @@ interface InnovationStats {
bottleneckRemovalCount: number; // تعداد رفع گلوگاه bottleneckRemovalCount: number; // تعداد رفع گلوگاه
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال) currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
percentProductionStops: number; // درصد مقایسه‌ای جلوگیری از توقفات تولید percentProductionStops: number | string; // درصد مقایسه‌ای جلوگیری از توقفات تولید
percentBottleneckRemoval: number; // درصد مقایسه‌ای رفع گلوگاه percentBottleneckRemoval: number | string; // درصد مقایسه‌ای رفع گلوگاه
percentCurrencyReduction: number; // درصد مقایسه‌ای کاهش ارز بری percentCurrencyReduction: number | string; // درصد مقایسه‌ای کاهش ارز بری
percentFailuresReduction: number; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار percentFailuresReduction: number | string; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار
} }
const columns = [ const columns = [
@ -126,6 +142,50 @@ export function ProcessInnovationPage() {
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] = const [selectedProjectDetails, setSelectedProjectDetails] =
useState<ProcessInnovationData | null>(null); useState<ProcessInnovationData | null>(null);
const [stateCard, setStateCard] = useState({
productionstopsprevention: {
id: "productionstopsprevention",
title: "جلوگیری از توقفات تولید",
value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum
),
description: "تن افزایش یافته",
icon: <CirclePause />,
color: "text-emerald-400",
},
bottleneckremoval: {
id: "bottleneckremoval",
title: "رفع گلوگاه",
value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه",
icon: <Funnel />,
color: "text-emerald-400",
},
currencyreduction: {
id: "currencyreduction",
title: "کاهش ارز بری",
value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
),
description: "دلار کاهش یافته",
icon: <DollarSign />,
color: "text-emerald-400",
},
frequentfailuresreduction: {
id: "frequentfailuresreduction",
title: "کاهش خرابی های پرتکرار",
value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum
),
description: "مجموع درصد کاهش خرابی",
icon: <Wrench />,
color: "text-emerald-400",
},
});
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false); const fetchingRef = useRef(false);
@ -153,58 +213,7 @@ export function ProcessInnovationPage() {
setDetailsDialogOpen(true); 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 // 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: <CirclePause />,
color: "text-emerald-400",
},
{
id: "bottleneck-removal",
title: "رفع گلوگاه",
value: formatNumber(stats.bottleneckRemovalCount),
description: "تعداد رفع گلوگاه",
icon: <Funnel />,
color: "text-emerald-400",
},
{
id: "currency-reduction",
title: "کاهش ارز بری",
value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
),
description: "دلار کاهش یافته",
icon: <DollarSign />,
color: "text-emerald-400",
},
{
id: "frequent-failures-reduction",
title: "کاهش خرابی های پرتکرار",
value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum
),
description: "مجموع درصد کاهش خرابی",
icon: <Wrench />,
color: "text-emerald-400",
},
];
const fetchProjects = async (reset = false) => { const fetchProjects = async (reset = false) => {
if (fetchingRef.current) { if (fetchingRef.current) {
return; return;
@ -226,6 +235,7 @@ export function ProcessInnovationPage() {
ProcessName: "project", ProcessName: "project",
OutputFields: [ OutputFields: [
"project_no", "project_no",
"project_id",
"title", "title",
"project_status", "project_status",
"project_rating", "project_rating",
@ -313,9 +323,12 @@ export function ProcessInnovationPage() {
useEffect(() => { useEffect(() => {
fetchProjects(true); fetchProjects(true);
fetchTotalCount(); fetchTotalCount();
fetchStats();
}, [sortConfig]); }, [sortConfig]);
useEffect(() => {
fetchStats();
}, [selectedProjects]);
useEffect(() => { useEffect(() => {
if (currentPage > 1) { if (currentPage > 1) {
fetchProjects(false); fetchProjects(false);
@ -393,7 +406,12 @@ export function ProcessInnovationPage() {
try { try {
setStatsLoading(true); setStatsLoading(true);
const raw = await apiService.call<any>({ const raw = await apiService.call<any>({
innovation_process_function: {}, innovation_process_function: {
project_ids:
selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
},
}); });
let payload: any = raw?.data; let payload: any = raw?.data;
@ -403,40 +421,53 @@ export function ProcessInnovationPage() {
} catch {} } 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 (v == null) return 0;
if (typeof v === "number") return v; if (typeof v === "number") return convertNumber;
if (typeof v === "string") { if (typeof v === "string") {
const cleaned = v.replace(/,/g, "").trim(); const cleaned = v.replace(/,/g, "").trim();
const n = parseFloat(cleaned); const n = parseFloat(cleaned);
return isNaN(n) ? 0 : n; return isNaN(n) ? 0 : convertNumber;
} }
return 0; return 0;
}; };
const data: Array<ProjectStats> = JSON.parse(
payload?.innovation_process_function
);
const stats = data[0];
const normalized: InnovationStats = { const normalized: InnovationStats = {
totalProjects: parseNum(payload?.count_innovation_process_projects), totalProjects: parseNum(stats?.count_innovation_process_projects),
averageScore: parseNum(payload?.average_project_score), averageScore: parseNum(stats?.average_project_score),
productionStopsPreventionSum: parseNum( productionStopsPreventionSum: parseNum(stats?.sum_stopping_production),
payload?.sum_stopping_production bottleneckRemovalCount: parseNum(stats?.count_throat_removal),
), currencyReductionSum: parseNum(stats?.sum_reduction_value_currency),
bottleneckRemovalCount: parseNum(payload?.count_throat_removal), frequentFailuresReductionSum: parseNum(stats?.sum_reducing_breakdowns),
currencyReductionSum: parseNum(payload?.sum_reduction_value_currency), percentProductionStops: stats?.percent_sum_stopping_production,
frequentFailuresReductionSum: parseNum( percentBottleneckRemoval: stats?.percent_throat_removal,
payload?.sum_reducing_breakdowns percentCurrencyReduction: stats?.percent_reduction_value_currency,
), percentFailuresReduction: stats?.percent_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
),
}; };
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); setStats(normalized);
} catch (error) { } catch (error) {
console.error("Error fetching stats:", 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) => { const formatCurrency = (amount: string | number) => {
if (!amount) return "0 ریال"; if (!amount) return "0 ریال";
const numericAmount = const numericAmount =
@ -465,34 +486,28 @@ export function ProcessInnovationPage() {
return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال"; return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
}; };
const formatPercentage = (value: string | number) => { const statusColor = (status: projectStatus): any => {
if (!value) return "0%"; let el = null;
const numericValue = typeof value === "string" ? parseFloat(value) : value; switch (status) {
if (isNaN(numericValue)) return "0%"; case projectStatus.contract:
return `${numericValue.toFixed(1)}%`; el = "teal";
}; break;
case projectStatus.finish:
const getStatusColor = (status: string) => { el = "info";
switch (status?.toLowerCase()) { break;
case "فعال": case projectStatus.stop:
return "#3AEA83"; el = "warning";
case "متوقف": break;
return "#F76276"; case projectStatus.inprogress:
case "تکمیل شده": el = "teal";
return "#32CD32"; break;
default: case projectStatus.mafasa:
return "#6B7280"; el = "destructive";
break;
case projectStatus.propozal:
el = "info";
} }
}; return el;
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";
}; };
const renderCellContent = (item: ProcessInnovationData, column: any) => { const renderCellContent = (item: ProcessInnovationData, column: any) => {
@ -502,8 +517,8 @@ export function ProcessInnovationPage() {
case "select": case "select":
return ( return (
<Checkbox <Checkbox
checked={selectedProjects.has(item.project_no)} checked={selectedProjects.has(item.project_id)}
onCheckedChange={() => handleSelectProject(item.project_no)} onCheckedChange={() => handleSelectProject(item.project_id)}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600" className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/> />
); );
@ -534,15 +549,16 @@ export function ProcessInnovationPage() {
return <span className="font-medium text-white">{String(value)}</span>; return <span className="font-medium text-white">{String(value)}</span>;
case "project_status": case "project_status":
return ( return (
<Badge <div className="flex items-center gap-1">
variant="outline" <Badge
className="font-medium border-2" variant={statusColor(value)}
style={{ className="font-medium border-2 p-0 block w-2 h-2 rounded-full"
border: "none", style={{
}} border: "none",
> }}
></Badge>
{String(value)} {String(value)}
</Badge> </div>
); );
case "project_rating": case "project_rating":
return ( return (
@ -603,7 +619,7 @@ export function ProcessInnovationPage() {
</CardContent> </CardContent>
</Card> </Card>
)) ))
: statsCards.map((card) => ( : Object.entries(stateCard).map(([key, card]) => (
<Card <Card
key={card.id} key={card.id}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50" className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
@ -792,61 +808,32 @@ export function ProcessInnovationPage() {
</div> </div>
</CardContent> </CardContent>
{/* Selection Summary */}
{/* {selectedProjects.size > 0 && (
<div className="px-4 py-3 bg-emerald-500/10 border-t border-emerald-500/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
<span className="text-emerald-400 font-medium font-persian">
{selectedProjects.size} پروژه انتخاب شده
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelectedProjects(new Set())}
className="border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/20 hover:text-emerald-300"
>
لغو انتخاب
</Button>
</div>
</div>
</div>
)} */}
{/* Footer */} {/* Footer */}
<div className="p-2 px-4 bg-gray-700/50"> <div className="p-2 px-4 bg-gray-700/50">
<div className="grid grid-cols-6 gap-4 text-sm text-gray-300 font-persian"> <div className="flex gap-4 text-sm text-gray-300 font-persian justify-between sm:flex-col xl:flex-row">
<div className="text-center gap-2 items-center flex"> <div className="text-center gap-2 items-center xl:w-1/3 pr-36 sm:w-full">
<div className="text-base text-gray-401 mb-1"> <div className="text-base text-gray-401 mb-1">
{" "} کل پروژه ها :{formatNumber(actualTotalCount)}
کل پروژه ها :{" "}
{formatNumber(stats.totalProjects || actualTotalCount)}
</div>
</div>
{/* Project number column - empty */}
<div></div>
{/* Title column - empty */}
<div></div>
{/* Project status column - empty */}
<div></div>
{/* Project rating column - show average */}
<div className="flex justify-center items-center gap-2">
<div className="text-base text-gray-400 mb-1">
{" "}
میانگین امتیاز :
</div>
<div className="font-bold">
{formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ??
0
)}
</div> </div>
</div> </div>
{/* Details column - show total count */} <div className="flex items-center flex-row gap-20 status justify-center xl:w-2/3 sm:w-full">
<div className="flex flex-row-reverse">
<span className="block w-7 h-2.5 bg-violet-500 rounded-tl-xl rounded-bl-xl"></span>
<span className="block w-7 h-2.5 bg-purple-500 "></span>
<span className="block w-7 h-2.5 bg-cyan-300 "></span>
<span className="block w-7 h-2.5 bg-pink-400 rounded-tr-xl rounded-br-xl"></span>
</div>
<div className="flex justify-center items-center gap-2">
<div className="text-base text-gray-400 mb-1">میانگین :</div>
<div className="font-bold">
{formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ?? 0
)}
</div>
</div>
</div>
</div> </div>
</div> </div>
</Card> </Card>

View File

@ -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 React, { useState } from "react";
import { Link, useLocation } from "react-router"; 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 { useAuth } from "~/contexts/auth-context";
import { import { cn } from "~/lib/utils";
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";
interface SidebarProps { interface SidebarProps {
isCollapsed?: boolean; isCollapsed?: boolean;
@ -113,7 +98,7 @@ const menuItems: MenuItem[] = [
id: "ideas", id: "ideas",
label: "ایده‌های فناوری و نوآوری", label: "ایده‌های فناوری و نوآوری",
icon: Lightbulb, icon: Lightbulb,
href: "/dashboard/ideas", href: "/dashboard/manage-ideas-tech",
}, },
{ {
id: "top-innovations", id: "top-innovations",
@ -162,7 +147,7 @@ export function Sidebar({
menuItems.forEach((item) => { menuItems.forEach((item) => {
if (item.children) { if (item.children) {
const hasActiveChild = item.children.some( const hasActiveChild = item.children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
); );
if (hasActiveChild) { if (hasActiveChild) {
newExpandedItems.push(item.id); newExpandedItems.push(item.id);
@ -183,7 +168,7 @@ export function Sidebar({
const item = menuItems.find((menuItem) => menuItem.id === itemId); const item = menuItems.find((menuItem) => menuItem.id === itemId);
if (item?.children) { if (item?.children) {
const hasActiveChild = item.children.some( 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 // Don't collapse if a child is active
if (hasActiveChild) { if (hasActiveChild) {
@ -201,7 +186,7 @@ export function Sidebar({
if (href && location.pathname === href) return true; if (href && location.pathname === href) return true;
if (children) { if (children) {
return children.some( return children.some(
(child) => child.href && location.pathname === child.href, (child) => child.href && location.pathname === child.href
); );
} }
return false; return false;
@ -213,7 +198,7 @@ export function Sidebar({
expandedItems.includes(item.id) || expandedItems.includes(item.id) ||
(item.children && (item.children &&
item.children.some( 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; const hasChildren = item.children && item.children.length > 0;
@ -265,15 +250,14 @@ return (
? " text-emerald-400 border-r-2 border-emerald-400" ? " 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", : "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", isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
"hover:bg-red-500/10 hover:text-red-400",
)} )}
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<ItemIcon <ItemIcon
className={cn( className={cn(
"w-5 h-5 flex-shrink-0", "w-5 h-5 flex-shrink-0",
isActive ? "text-emerald-400" : "text-current", isActive ? "text-emerald-400" : "text-current"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (
@ -294,7 +278,7 @@ return (
<ChevronDown <ChevronDown
className={cn( className={cn(
"w-4 h-4 transition-transform duration-200", "w-4 h-4 transition-transform duration-200",
isExpanded ? "rotate-180" : "rotate-0", isExpanded ? "rotate-180" : "rotate-0"
)} )}
/> />
)} )}
@ -309,9 +293,9 @@ return (
// Disable pointer cursor when child is active (cannot collapse) // Disable pointer cursor when child is active (cannot collapse)
item.children && item.children &&
item.children.some( 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} onClick={handleClick}
> >
@ -323,15 +307,14 @@ return (
? " text-emerald-400 border-r-2 border-emerald-400" ? " 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", : "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", isCollapsed && level === 0 && "justify-center px-2",
item.id === "logout" && item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400"
"hover:bg-red-500/10 hover:text-red-400",
)} )}
> >
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<ItemIcon <ItemIcon
className={cn( className={cn(
"w-5 h-5 flex-shrink-0", "w-5 h-5 flex-shrink-0",
isActive ? "text-emerald-400" : "text-current", isActive ? "text-emerald-400" : "text-current"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (
@ -357,10 +340,10 @@ return (
item.children && item.children &&
item.children.some( item.children.some(
(child) => (child) =>
child.href && location.pathname === child.href, child.href && location.pathname === child.href
) )
? "text-emerald-400" ? "text-emerald-400"
: "text-current", : "text-current"
)} )}
/> />
)} )}
@ -395,7 +378,7 @@ return (
className={cn( className={cn(
" backdrop-blur-sm h-full flex flex-col transition-all duration-300", " backdrop-blur-sm h-full flex flex-col transition-all duration-300",
isCollapsed ? "w-16" : "w-64", isCollapsed ? "w-16" : "w-64",
className, className
)} )}
> >
{/* Header */} {/* Header */}
@ -459,7 +442,7 @@ return (
<ChevronRight <ChevronRight
className={cn( className={cn(
"w-4 h-4 text-gray-400 transition-transform duration-200", "w-4 h-4 text-gray-400 transition-transform duration-200",
isCollapsed ? "rotate-180" : "rotate-0", isCollapsed ? "rotate-180" : "rotate-0"
)} )}
/> />
{!isCollapsed && ( {!isCollapsed && (

View File

@ -1,4 +1,3 @@
import * as React from "react";
import { formatNumber } from "~/lib/utils"; import { formatNumber } from "~/lib/utils";
export interface BarChartData { export interface BarChartData {
@ -80,7 +79,7 @@ export function CustomBarChart({
{data.map((item, index) => { {data.map((item, index) => {
const percentage = const percentage =
globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0; globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
const displayValue = item.value.toFixed(1); const displayValue: any = item.value;
return ( return (
<div key={index} className="flex items-center gap-3"> <div key={index} className="flex items-center gap-3">

View File

@ -6,7 +6,7 @@ export function cn(...inputs: ClassValue[]) {
} }
export const formatNumber = (value: string | number) => { export const formatNumber = (value: string | number) => {
if (!value) return "0"; // if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value; const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0"; if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue); return new Intl.NumberFormat("fa-IR").format(numericValue);

View File

@ -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 [ export default [
route("login", "routes/login.tsx"), route("login", "routes/login.tsx"),
@ -21,6 +21,7 @@ export default [
"routes/digital-innovation-page.tsx" "routes/digital-innovation-page.tsx"
), ),
route("dashboard/ecosystem", "routes/ecosystem.tsx"), route("dashboard/ecosystem", "routes/ecosystem.tsx"),
route("dashboard/manage-ideas-tech", "routes/manage-ideas-tech-page.tsx"),
route("404", "routes/404.tsx"), route("404", "routes/404.tsx"),
route("unauthorized", "routes/unauthorized.tsx"), route("unauthorized", "routes/unauthorized.tsx"),
route("*", "routes/$.tsx"), // Catch-all route for 404s route("*", "routes/$.tsx"), // Catch-all route for 404s

View File

@ -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 (
<ProtectedRoute requireAuth={true}>
<ManageIdeasTechPage />
</ProtectedRoute>
);
}