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

View File

@ -121,7 +121,7 @@ export function ProcessInnovationPage() {
direction: "asc", direction: "asc",
}); });
const [selectedProjects, setSelectedProjects] = useState<Set<string>>( const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set(), new Set()
); );
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [selectedProjectDetails, setSelectedProjectDetails] = const [selectedProjectDetails, setSelectedProjectDetails] =
@ -167,7 +167,7 @@ export function ProcessInnovationPage() {
title: "جلوگیری از توقفات تولید", title: "جلوگیری از توقفات تولید",
value: formatNumber( value: formatNumber(
stats.productionStopsPreventionSum.toFixed?.(1) ?? stats.productionStopsPreventionSum.toFixed?.(1) ??
stats.productionStopsPreventionSum, stats.productionStopsPreventionSum
), ),
description: "تن افزایش یافته", description: "تن افزایش یافته",
icon: <CirclePause />, icon: <CirclePause />,
@ -186,7 +186,7 @@ export function ProcessInnovationPage() {
id: "currency-reduction", id: "currency-reduction",
title: "کاهش ارز بری", title: "کاهش ارز بری",
value: formatNumber( value: formatNumber(
stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum, stats.currencyReductionSum.toFixed?.(0) ?? stats.currencyReductionSum
), ),
description: "دلار کاهش یافته", description: "دلار کاهش یافته",
icon: <DollarSign />, icon: <DollarSign />,
@ -197,7 +197,7 @@ export function ProcessInnovationPage() {
title: "کاهش خرابی های پرتکرار", title: "کاهش خرابی های پرتکرار",
value: formatNumber( value: formatNumber(
stats.frequentFailuresReductionSum.toFixed?.(1) ?? stats.frequentFailuresReductionSum.toFixed?.(1) ??
stats.frequentFailuresReductionSum, stats.frequentFailuresReductionSum
), ),
description: "مجموع درصد کاهش خرابی", description: "مجموع درصد کاهش خرابی",
icon: <Wrench />, icon: <Wrench />,
@ -418,22 +418,22 @@ export function ProcessInnovationPage() {
totalProjects: parseNum(payload?.count_innovation_process_projects), totalProjects: parseNum(payload?.count_innovation_process_projects),
averageScore: parseNum(payload?.average_project_score), averageScore: parseNum(payload?.average_project_score),
productionStopsPreventionSum: parseNum( productionStopsPreventionSum: parseNum(
payload?.sum_stopping_production, payload?.sum_stopping_production
), ),
bottleneckRemovalCount: parseNum(payload?.count_throat_removal), bottleneckRemovalCount: parseNum(payload?.count_throat_removal),
currencyReductionSum: parseNum(payload?.sum_reduction_value_currency), currencyReductionSum: parseNum(payload?.sum_reduction_value_currency),
frequentFailuresReductionSum: parseNum( frequentFailuresReductionSum: parseNum(
payload?.sum_reducing_breakdowns, payload?.sum_reducing_breakdowns
), ),
percentProductionStops: parseNum( percentProductionStops: parseNum(
payload?.percent_sum_stopping_production, payload?.percent_sum_stopping_production
), ),
percentBottleneckRemoval: parseNum(payload?.percent_throat_removal), percentBottleneckRemoval: parseNum(payload?.percent_throat_removal),
percentCurrencyReduction: parseNum( percentCurrencyReduction: parseNum(
payload?.percent_reduction_value_currency, payload?.percent_reduction_value_currency
), ),
percentFailuresReduction: parseNum( percentFailuresReduction: parseNum(
payload?.percent_reducing_breakdowns, payload?.percent_reducing_breakdowns
), ),
}; };
@ -841,7 +841,7 @@ export function ProcessInnovationPage() {
{formatNumber( {formatNumber(
((stats.averageScore ?? 0) as number).toFixed?.(1) ?? ((stats.averageScore ?? 0) as number).toFixed?.(1) ??
stats.averageScore ?? stats.averageScore ??
0, 0
)} )}
</div> </div>
</div> </div>
@ -882,7 +882,7 @@ export function ProcessInnovationPage() {
{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>
@ -897,7 +897,7 @@ export function ProcessInnovationPage() {
{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>
@ -913,9 +913,9 @@ export function ProcessInnovationPage() {
Number( Number(
selectedProjectDetails?.approved_budget.replaceAll( selectedProjectDetails?.approved_budget.replaceAll(
",", ",",
"", ""
), )
), )
) || "-"} ) || "-"}
</span> </span>
</div> </div>