Merge branch 'main' of https://git.pelekan.org/Saeed0920/inogen into dashboard_popup
This commit is contained in:
commit
d69a7c1e05
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
17
app/routes/manage-ideas-tech-page.tsx
Normal file
17
app/routes/manage-ideas-tech-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user