feat: compelete page

This commit is contained in:
mehrdad_adabi 2025-08-29 22:02:44 +03:30 committed by Saeed Abadiyan
parent 768fe2226b
commit 20963d0ea3
2 changed files with 500 additions and 438 deletions

View File

@ -3,9 +3,7 @@ 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 { CustomBarChart } from "~/components/ui/custom-bar-chart";
import moment from "moment-jalaali";
import type { BarChartData } from "~/components/ui/custom-bar-chart";
import {
Table,
TableBody,
@ -26,12 +24,7 @@ import {
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
Rectangle,
Legend,
} from "recharts";
import apiService from "~/lib/api";
@ -48,20 +41,26 @@ import {
UsersIcon,
UserIcon,
RefreshCw,
Radar,
Cog,
ChevronUp,
ChevronDown,
} from "lucide-react";
import DashboardLayout from "../layout";
moment.loadPersian({ usePersianDigits: true });
interface ProcessInnovationData {
project_no: string;
title: string;
project_status: string;
project_rating: string;
reduce_prevention_production_stops: string;
throat_removal: string;
amount_currency_reduction: string;
Reduce_rate_failure: string;
interface GreenInnovationData {
WorkflowID: string;
approved_budget: string;
done_date: string | null;
observer: string;
project_description: string;
project_id: string;
project_no: string;
project_rating: string;
project_status: string;
start_date: string;
title: string;
}
interface SortConfig {
@ -69,7 +68,7 @@ interface SortConfig {
direction: "asc" | "desc";
}
interface StatsCard {
interface StateItem {
id: string;
title: string;
percent: {
@ -82,24 +81,51 @@ interface StatsCard {
};
}
interface StatsCard {
pollution: StateItem;
waste: StateItem;
}
interface InnovationStats {
totalProjects: number;
averageScore: number;
productionStopsPreventionSum: number; // مجموع جلوگیری از توقفات تولید
bottleneckRemovalCount: number; // تعداد رفع گلوگاه
currencyReductionSum: number; // مجموع کاهش ارز بری (میلیون ریال)
frequentFailuresReductionSum: number; // مجموع کاهش خرابی های پرتکرار
percentProductionStops: number; // درصد مقایسه‌ای جلوگیری از توقفات تولید
percentBottleneckRemoval: number; // درصد مقایسه‌ای رفع گلوگاه
percentCurrencyReduction: number; // درصد مقایسه‌ای کاهش ارز بری
percentFailuresReduction: number; // درصد مقایسه‌ای کاهش خرابی‌های پرتکرار
electricity_recovery_reduction: number;
electricity_recovery_reduction_percent: number;
feed_recovery_reduction: number;
feed_recovery_reduction_percent: number;
fuel_recovery_reduction: number;
fuel_recovery_reduction_percent: number;
pollution_reduction: number;
pollution_reduction_percent: number;
waste_reduction: number;
waste_reduction_percent: number;
water_recovery_reduction: number;
water_recovery_reduction_percent: number;
}
interface GreenInnovationState {
water: {
value: any;
percent: number;
};
food: {
value: any;
percent: number;
};
power: {
value: any;
percent: number;
};
oil: {
value: any;
percent: number;
};
}
interface Params {
icon: any;
label: string;
value: string;
value: number;
suffix: string;
percent: number;
}
interface RecycleParams {
water: Params;
@ -108,6 +134,16 @@ interface RecycleParams {
oil: Params;
}
interface stateCounter {
totalProjects: number;
}
interface ChartDataItem {
name: string;
pv: any; // actual value
amt: number; // max value or target
}
const columns = [
{ key: "select", label: "", sortable: false, width: "50px" },
{ key: "project_no", label: "شماره پروژه", sortable: true, width: "140px" },
@ -128,7 +164,7 @@ const columns = [
];
export function GreenInnovationPage() {
const [projects, setProjects] = useState<ProcessInnovationData[]>([]);
const [projects, setProjects] = useState<GreenInnovationData[]>([]);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
@ -137,18 +173,7 @@ export function GreenInnovationPage() {
const [totalCount, setTotalCount] = useState(0);
const [actualTotalCount, setActualTotalCount] = useState(0);
const [statsLoading, setStatsLoading] = useState(false);
const [stats, setStats] = useState<InnovationStats>({
totalProjects: 0,
averageScore: 0,
productionStopsPreventionSum: 0,
bottleneckRemovalCount: 0,
currencyReductionSum: 0,
frequentFailuresReductionSum: 0,
percentProductionStops: 0,
percentBottleneckRemoval: 0,
percentCurrencyReduction: 0,
percentFailuresReduction: 0,
});
const [stats, setStats] = useState<stateCounter>();
const [sortConfig, setSortConfig] = useState<SortConfig>({
field: "start_date",
direction: "asc",
@ -158,88 +183,52 @@ export function GreenInnovationPage() {
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
useState<ProcessInnovationData | null>(null);
useState<GreenInnovationData | null>(null);
const [recycleParams, setRecycleParams] = useState<RecycleParams>({
water: {
icon: <Key className="text-emerald-400" size={"18px"} />,
label: "آب",
value: "1,520",
value: 0,
suffix: "لیتر",
percent: 0,
},
food: {
icon: <Sparkle className="text-emerald-400" size={"18px"} />,
label: "خوراک",
value: "520",
value: 0,
suffix: "تن",
percent: 0,
},
oil: {
icon: <Flame className="text-emerald-400" size={"18px"} />,
label: "سوخت",
value: "250",
value: 0,
suffix: "متر مربع",
percent: 0,
},
power: {
icon: <Zap className="text-emerald-400" size={"18px"} />,
label: "برق",
value: "650",
value: 0,
suffix: "میلیون مگاوات",
percent: 0,
},
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
// Selection handlers
const handleSelectAll = () => {
if (selectedProjects.size === projects.length) {
setSelectedProjects(new Set());
} else {
setSelectedProjects(new Set(projects.map((p) => p.project_no)));
}
};
const handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) {
newSelected.delete(projectNo);
} else {
newSelected.add(projectNo);
}
setSelectedProjects(newSelected);
};
const handleProjectDetails = (project: ProcessInnovationData) => {
console.log(project);
setSelectedProjectDetails(project);
setDetailsDialogOpen(true);
};
const formatNumber = (value: string | number) => {
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};
// Stats cards data - computed from projects data
const statsCards: StatsCard[] = [
{
const [sustainabilityStats, setSustainabilityStats] = useState<StatsCard>({
pollution: {
id: "reduce-pollution",
title: "کاهش آلایندگی",
total: {
value: 10.45,
description: "میلیون ریال",
},
// formatNumber(
// stats.productionStopsPreventionSum.toFixed?.(1) ??
// stats.productionStopsPreventionSum
// ),
percent: {
value: 10,
description: "درصد به کل درآمد",
},
},
{
waste: {
id: "reduce-junkfull",
title: "کاهش ضایعات",
total: {
@ -251,29 +240,31 @@ export function GreenInnovationPage() {
description: "درصد به کل درآمد",
},
},
});
const observerRef = useRef<HTMLDivElement>(null);
const fetchingRef = useRef(false);
// {
// 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 handleSelectProject = (projectNo: string) => {
const newSelected = new Set(selectedProjects);
if (newSelected.has(projectNo)) {
newSelected.delete(projectNo);
} else {
newSelected.add(projectNo);
}
setSelectedProjects(newSelected);
};
const handleProjectDetails = (project: GreenInnovationData) => {
setSelectedProjectDetails(project);
setDetailsDialogOpen(true);
};
const formatNumber = (value: string | number) => {
if (!value) return "0";
const numericValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numericValue)) return "0";
return new Intl.NumberFormat("fa-IR").format(numericValue);
};
const fetchProjects = async (reset = false) => {
if (fetchingRef.current) {
@ -291,30 +282,24 @@ export function GreenInnovationPage() {
}
const pageToFetch = reset ? 1 : currentPage;
const response = await apiService.select({
ProcessName: "project",
OutputFields: [
"project_id",
"project_no",
"title",
"project_status",
"project_rating",
"throat_removal",
"reduce_prevention_production_stops",
"amount_currency_reduction",
"Reduce_rate_failure",
"project_description",
"start_date",
"done_date",
"approved_budget",
"observer",
],
Sorts: [["start_date", "asc"]],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
Sorts: [[sortConfig.field, sortConfig.direction]],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
Pagination: { PageNumber: pageToFetch, PageSize: pageSize },
});
console.log(JSON.parse(response.data));
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
@ -384,7 +369,7 @@ export function GreenInnovationPage() {
fetchProjects(true);
fetchTotalCount();
fetchStats();
}, [sortConfig]);
}, [sortConfig, selectedProjects]);
useEffect(() => {
if (currentPage > 1) {
@ -434,9 +419,8 @@ export function GreenInnovationPage() {
const response = await apiService.select({
ProcessName: "project",
OutputFields: ["count(project_no)"],
Conditions: [["type_of_innovation", "=", "نوآوری در فرآیند"]],
Conditions: [["type_of_innovation", "=", "نوآوری سبز"]],
});
if (response.state === 0) {
const dataString = response.data;
if (dataString && typeof dataString === "string") {
@ -463,17 +447,20 @@ export function GreenInnovationPage() {
try {
setStatsLoading(true);
const raw = await apiService.call<any>({
innovation_process_function: {},
innovation_green_function: {
project_ids:
selectedProjects.size > 0
? Array.from(selectedProjects).join(" , ")
: "",
},
});
let payload: any = raw?.data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch {}
}
const parseNum = (v: unknown): number => {
const parseNum = (v: unknown): any => {
if (v == null) return 0;
if (typeof v === "number") return v;
if (typeof v === "string") {
@ -484,30 +471,43 @@ export function GreenInnovationPage() {
return 0;
};
const normalized: InnovationStats = {
totalProjects: parseNum(payload?.count_innovation_process_projects),
averageScore: parseNum(payload?.average_project_score),
productionStopsPreventionSum: parseNum(
payload?.sum_stopping_production
),
bottleneckRemovalCount: parseNum(payload?.count_throat_removal),
currencyReductionSum: parseNum(payload?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum(
payload?.sum_reducing_breakdowns
),
percentProductionStops: parseNum(
payload?.percent_sum_stopping_production
),
percentBottleneckRemoval: parseNum(payload?.percent_throat_removal),
percentCurrencyReduction: parseNum(
payload?.percent_reduction_value_currency
),
percentFailuresReduction: parseNum(
payload?.percent_reducing_breakdowns
),
};
const data: Array<InnovationStats> = JSON.parse(
payload?.innovation_green_function
);
const stats = data[0];
setStats(normalized);
const normalized: any = {
food: {
value: formatNumber(parseNum(stats?.feed_recovery_reduction)),
percent: parseNum(stats?.feed_recovery_reduction_percent),
},
oil: {
value: formatNumber(parseNum(stats?.fuel_recovery_reduction)),
percent: parseNum(stats?.fuel_recovery_reduction_percent),
},
power: {
value: formatNumber(parseNum(stats?.electricity_recovery_reduction)),
percent: parseNum(stats?.electricity_recovery_reduction_percent),
},
water: {
value: formatNumber(parseNum(stats.water_recovery_reduction)),
percent: parseNum(stats.water_recovery_reduction_percent),
},
pollution: {
value: formatNumber(parseNum(stats.pollution_reduction)),
percent: formatNumber(parseNum(stats.pollution_reduction_percent)),
},
waste: {
value: formatNumber(parseNum(stats.waste_reduction)),
percent: formatNumber(parseNum(stats.waste_reduction_percent)),
},
};
setPageData(normalized);
} catch (error) {
console.error("Error fetching stats:", error);
} finally {
@ -515,65 +515,80 @@ export function GreenInnovationPage() {
}
};
// const handleRefresh = () => {
// fetchingRef.current = false;
// setCurrentPage(1);
// setProjects([]);
// setHasMore(true);
// fetchProjects(true);
// fetchTotalCount();
// fetchStats();
// };
const setPageData = (normalized: any) => {
setSustainabilityStats((prev) => ({
...prev,
pollution: {
...prev.pollution,
total: { ...prev.pollution.total, value: normalized.pollution.value },
percent: {
...prev.pollution.percent,
value: normalized.pollution.percent,
},
},
waste: {
...prev.waste,
total: { ...prev.waste.total, value: normalized.waste.value },
percent: { ...prev.waste.percent, value: normalized.waste.percent },
},
}));
// const formatCurrency = (amount: string | number) => {
// if (!amount) return "0 ریال";
// const numericAmount =
// typeof amount === "string"
// ? parseFloat(amount.replace(/,/g, ""))
// : amount;
// if (isNaN(numericAmount)) return "0 ریال";
// return new Intl.NumberFormat("fa-IR").format(numericAmount) + " ریال";
// };
setRecycleParams((prev) => ({
...prev,
water: {
...prev.water,
value: normalized.water.value,
percent: normalized.water.percent,
},
food: {
...prev.food,
value: normalized.food.value,
percent: normalized.food.percent,
},
oil: {
...prev.oil,
value: normalized.oil.value,
percent: normalized.oil.percent,
},
power: {
...prev.power,
value: normalized.power.value,
percent: normalized.power.percent,
},
}));
setChartData([
{
name: recycleParams.water.label,
pv: Math.max(0, normalized.water.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.power.label,
pv: Math.max(0, normalized.power.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.oil.label,
pv: Math.max(0, normalized.oil.percent).toFixed(2),
amt: 100,
},
{
name: recycleParams.food.label,
pv: Math.max(0, normalized.food.percent).toFixed(2),
amt: 100,
},
]);
};
// const formatPercentage = (value: string | number) => {
// if (!value) return "0%";
// const numericValue = typeof value === "string" ? parseFloat(value) : value;
// if (isNaN(numericValue)) return "0%";
// return `${numericValue.toFixed(1)}%`;
// };
// const getStatusColor = (status: string) => {
// switch (status?.toLowerCase()) {
// case "فعال":
// return "#3AEA83";
// case "متوقف":
// return "#F76276";
// case "تکمیل شده":
// return "#32CD32";
// default:
// return "#6B7280";
// }
// };
// const 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 value = item[column.key as keyof ProcessInnovationData];
const renderCellContent = (item: GreenInnovationData, column: any) => {
const value = item[column.key as keyof GreenInnovationData];
switch (column.key) {
case "select":
return (
<Checkbox
checked={selectedProjects.has(item.project_no)}
onCheckedChange={() => handleSelectProject(item.project_no)}
checked={selectedProjects.has(item.project_id)}
onCheckedChange={() => handleSelectProject(item.project_id)}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
);
@ -633,12 +648,12 @@ export function GreenInnovationPage() {
}
};
const data = [
const [chartData, setChartData] = useState<Array<ChartDataItem>>([
{ name: recycleParams.water.label, pv: 70, amt: 80 },
{ name: recycleParams.power.label, pv: 45, amt: 60 },
{ name: recycleParams.oil.label, pv: 90, amt: 75 },
{ name: recycleParams.food.label, pv: 30, amt: 50 },
];
]);
return (
<DashboardLayout title="نوآوری سبز">
@ -654,18 +669,15 @@ export function GreenInnovationPage() {
key={`skeleton-${index}`}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden"
>
<CardContent className="p-2">
<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">
<CardContent className="p-0 h-48">
<div className="flex flex-col gap-2 h-full">
<div className="border-b-2 border-gray-500/20 p-2.5">
<div
className="h-6 bg-gray-600 rounded animate-pulse"
style={{ width: "60%" }}
/>
<div className="p-3 bg-emerald-500/20 rounded-full w-fit">
<div className="w-6 h-6 bg-gray-600 rounded animate-pulse" />
</div>
</div>
<div className="flex items-center justify-center flex-col p-1">
<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%" }}
@ -679,34 +691,34 @@ export function GreenInnovationPage() {
</CardContent>
</Card>
))
: statsCards.map((card) => (
: Object.entries(sustainabilityStats).map(([key, value]) => (
<Card
key={card.id}
key={key}
className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm border-gray-700/50"
>
<CardContent className="p-0 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 ">
<h3 className="text-lg font-bold text-white font-persian p-4">
{card.title}
{value.title}
</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">
% {card.percent.value}
<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">
{card.percent.description}
{value.percent?.description}
</span>
</div>
<b className="block w-0.5 h-12 bg-gray-600 rotate-45" />
<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">
{card.total.value}
<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">
{card.total.description}
{value.total?.description}
</span>
</div>
</div>
@ -717,6 +729,51 @@ export function GreenInnovationPage() {
</div>
{/* Process Impacts Chart */}
{statsLoading ? (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden">
<CardContent className="p-0 h-full">
<div className="border-b-2 border-gray-500/20">
<div className="w-full p-4 px-6">
<div className="h-6 bg-gray-600 rounded animate-pulse w-1/2" />
</div>
</div>
<div className="content grid gap-6 h-max p-8 box-border items-center justify-between sm:grid-cols-1 xl:grid-cols-[30%_70%]">
<div className="params flex flex-col gap-3.5">
{[...Array(3)].map((_, paramIndex) => (
<div
key={paramIndex}
className="param flex flex-row justify-between items-center"
>
<div className="flex flex-row gap-2 items-center">
<div className="h-6 w-6 bg-gray-600 rounded-full animate-pulse" />
<div className="h-4 bg-gray-600 rounded animate-pulse w-24" />
</div>
<div className="flex flex-row gap-1.5 items-center">
<div className="h-4 bg-gray-600 rounded animate-pulse w-12" />
<div className="h-4 bg-gray-600 rounded animate-pulse w-8" />
</div>
</div>
))}
</div>
<div className="h-72 w-full flex items-end justify-between px-6">
{[...Array(8)].map((_, barIndex) => (
<div
key={barIndex}
className="bg-gray-600 rounded-t animate-pulse"
style={{
width: "14px",
height: `${30 + barIndex * 8}%`, // ارتفاع تصادفی یا ترتیب مشخص
}}
/>
))}
</div>
</div>
</CardContent>
</Card>
) : (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden">
<CardContent className="p-0 h-full overflow-hidden">
<div className="border-b-2 border-gray-500/20">
@ -731,15 +788,17 @@ export function GreenInnovationPage() {
<div className="param flex flex-row justify-between items-center">
<div className="flex flex-row gap-2">
{el[1].icon}
<span className="font-normal text-sm">
<span className="font-normal text-sm font-persian">
{el[1].label}:
</span>
</div>
<div className="flex flex-row gap-1.5 items-center">
<span className="text-sm font-normal">
<span className="text-sm font-normal font-persian">
{el[1].value}
</span>
<span className="text-sm">{el[1].suffix}</span>
<span className="text-sm font-persian">
{el[1].suffix}
</span>
</div>
</div>
);
@ -751,7 +810,7 @@ export function GreenInnovationPage() {
<BarChart
width={500}
height={300}
data={data}
data={chartData}
margin={{
top: 5,
right: 30,
@ -762,12 +821,11 @@ export function GreenInnovationPage() {
<CartesianGrid
stroke="#374151"
strokeDasharray="0"
vertical={false} // خط‌های عمودی (از محور X) بمونه
// horizontal={false} // خط‌های افقی (از محور Y) حذف میشه
vertical={false}
/>
<XAxis
dataKey="name"
axisLine={false} // خط اصلی محور X محو میشه
axisLine={false}
tickLine={false}
tick={{
fill: "#fff",
@ -784,9 +842,7 @@ export function GreenInnovationPage() {
tick={{
fill: "#99a1af", // رنگ متن
fontSize: 14, // سایز فونت
// fontWeight: "bold", // ضخامت
dx: -30, // جابجایی افقی (اعداد نزدیک‌تر یا دورتر از محور)
// dy:-1
}}
tickFormatter={(val) => `${val}%`}
/>
@ -799,7 +855,7 @@ export function GreenInnovationPage() {
position: "top",
fill: "#fff",
fontWeight: "bold",
formatter: (value) => `${value}%`
formatter: (value) => `${value}%`,
}}
/>
</BarChart>
@ -808,18 +864,39 @@ export function GreenInnovationPage() {
</div>
</CardContent>
</Card>
)}
<Card className="w-1/3 bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl overflow-hidden">
<CardContent className="p-0">
<div className="border-b-2 border-gray-500/20">
<div className="flex flex-row justify-between w-full p-4 px-6">
{statsLoading ? (
<>
<span className="h-4 w-28 bg-gray-500/40 rounded animate-pulse"></span>
<span className="h-5 w-5 bg-gray-500/40 rounded-full animate-pulse"></span>
</>
) : (
<>
<span>استاندارد ها و مقررات</span>
<TrendingUp />
</>
)}
</div>
</div>
<div className="flex flex-col gap-3 p-4 max-h-[22rem] overflow-y-scroll">
{Array.from({ length: 10 }, (index) => {
return (
<div
className={`flex flex-col gap-3 p-4 max-h-[22rem] ${
statsLoading ? "overflow-y-hidden" : "overflow-y-scroll"
}`}
>
{statsLoading
? Array.from({ length: 10 }).map((_, index) => (
<div key={index} className="flex gap-2 items-center">
<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>
))
: Array.from({ length: 10 }).map((_, index) => (
<div key={`${index}-1`} className="flex gap-2">
<LoaderCircle
size={"18px"}
@ -827,8 +904,7 @@ export function GreenInnovationPage() {
/>
<span>استاندارد Iso 2005</span>
</div>
);
})}
))}
</div>
</CardContent>
</Card>
@ -847,18 +923,7 @@ export function GreenInnovationPage() {
className="text-right font-persian whitespace-nowrap text-gray-200 font-medium sticky top-0 z-20 bg-[#3F415A]"
style={{ width: column.width }}
>
{column.key === "select" ? (
<div className="flex items-center justify-center">
<Checkbox
checked={
selectedProjects.size === projects.length &&
projects.length > 0
}
onCheckedChange={handleSelectAll}
className="data-[state=checked]:bg-emerald-600 data-[state=checked]:border-emerald-600"
/>
</div>
) : column.sortable ? (
{column.sortable ? (
<button
onClick={() => handleSort(column.key)}
className="flex items-center gap-2"
@ -950,29 +1015,6 @@ export function GreenInnovationPage() {
</div>
</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 */}
<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">
@ -980,7 +1022,7 @@ export function GreenInnovationPage() {
<div className="text-base text-gray-401 mb-1">
{" "}
کل پروژه ها :{" "}
{formatNumber(stats.totalProjects || actualTotalCount)}
{formatNumber(stats?.totalProjects || actualTotalCount)}
</div>
</div>
{/* Project number column - empty */}
@ -997,8 +1039,8 @@ export function GreenInnovationPage() {
</div>
<div className="font-bold">
{formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ??
((stats?.averageScore ?? 0) as number).toFixed?.(1) ??
stats?.averageScore ??
0
)}
</div>
@ -1022,7 +1064,7 @@ export function GreenInnovationPage() {
{/* Project Description */}
<div className="flex-[4] border-l-2 border-gray-600">
<h2 className="font-bold">{selectedProjectDetails?.title}</h2>
<p className="text-gray-300 font-persian px-2 mt-2">
<p className="text-gray-300 font-persian px-1 mt-2">
{selectedProjectDetails?.project_description || "-"}
</p>
</div>
@ -1086,6 +1128,26 @@ export function GreenInnovationPage() {
{selectedProjectDetails?.observer || "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<Radar className="h-4 text-green-500" />
حوزه کاری :
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.observer || "-"}
</span>
</div>
<div className="flex items-center justify-between">
<h4 className="font-medium text-gray-300 font-persian mb-2 flex items-center gap-1">
<Cog className="h-4 text-green-500" />
صنعت :
</h4>
<span className="text-white font-bold font-persian">
{selectedProjectDetails?.observer || "-"}
</span>
</div>
</div>
</div>
</DialogContent>

View File

@ -121,7 +121,7 @@ export function ProcessInnovationPage() {
direction: "asc",
});
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set(),
new Set()
);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] =
@ -167,7 +167,7 @@ export function ProcessInnovationPage() {
title: "جلوگیری از توقفات تولید",
value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum,
stats.productionStopsPreventionSum
),
description: "تن افزایش یافته",
icon: <CirclePause />,
@ -186,7 +186,7 @@ export function ProcessInnovationPage() {
id: "currency-reduction",
title: "کاهش ارز بری",
value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum,
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
),
description: "دلار کاهش یافته",
icon: <DollarSign />,
@ -197,7 +197,7 @@ export function ProcessInnovationPage() {
title: "کاهش خرابی های پرتکرار",
value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum,
stats.frequentFailuresReductionSum
),
description: "مجموع درصد کاهش خرابی",
icon: <Wrench />,
@ -400,7 +400,7 @@ export function ProcessInnovationPage() {
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch { }
} catch {}
}
const parseNum = (v: unknown): number => {
@ -418,22 +418,22 @@ export function ProcessInnovationPage() {
totalProjects: parseNum(payload?.count_innovation_process_projects),
averageScore: parseNum(payload?.average_project_score),
productionStopsPreventionSum: parseNum(
payload?.sum_stopping_production,
payload?.sum_stopping_production
),
bottleneckRemovalCount: parseNum(payload?.count_throat_removal),
currencyReductionSum: parseNum(payload?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum(
payload?.sum_reducing_breakdowns,
payload?.sum_reducing_breakdowns
),
percentProductionStops: parseNum(
payload?.percent_sum_stopping_production,
payload?.percent_sum_stopping_production
),
percentBottleneckRemoval: parseNum(payload?.percent_throat_removal),
percentCurrencyReduction: parseNum(
payload?.percent_reduction_value_currency,
payload?.percent_reduction_value_currency
),
percentFailuresReduction: parseNum(
payload?.percent_reducing_breakdowns,
payload?.percent_reducing_breakdowns
),
};
@ -639,7 +639,7 @@ export function ProcessInnovationPage() {
{/* Process Impacts Chart */}
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] backdrop-blur-sm rounded-2xl w-full overflow-hidden">
<CardContent >
<CardContent>
<CustomBarChart
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
loading={statsLoading}
@ -841,7 +841,7 @@ export function ProcessInnovationPage() {
{formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ??
0,
0
)}
</div>
</div>
@ -882,7 +882,7 @@ export function ProcessInnovationPage() {
{selectedProjectDetails?.start_date
? moment(
selectedProjectDetails?.start_date,
"YYYY-MM-DD",
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</span>
@ -897,7 +897,7 @@ export function ProcessInnovationPage() {
{selectedProjectDetails?.done_date
? moment(
selectedProjectDetails?.done_date,
"YYYY-MM-DD",
"YYYY-MM-DD"
).format("YYYY/MM/DD")
: "-"}
</span>
@ -913,9 +913,9 @@ export function ProcessInnovationPage() {
Number(
selectedProjectDetails?.approved_budget.replaceAll(
",",
"",
),
),
""
)
)
) || "-"}
</span>
</div>