Refactor process impacts chart to use new CustomBarChart component (#3)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
e27774c45a
commit
699548c674
|
|
@ -4,6 +4,8 @@ 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 type { BarChartData } from "~/components/ui/custom-bar-chart";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -583,108 +585,38 @@ export function ProcessInnovationPage() {
|
||||||
{/* Process Impacts Chart */}
|
{/* Process Impacts Chart */}
|
||||||
<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-4">
|
<CardContent className="p-4">
|
||||||
<div className="mb-4">
|
<CustomBarChart
|
||||||
<h3 className="text-xl font-bold text-white font-persian text-right mb-2">
|
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
|
||||||
تاثیرات فرآیندی به صورت درصد مقایسه ای
|
loading={statsLoading}
|
||||||
</h3>
|
data={[
|
||||||
</div>
|
{
|
||||||
|
label: "کاهش توقفات تولید",
|
||||||
{/* Chart Container */}
|
value: stats.percentProductionStops || 0,
|
||||||
<div className="space-y-6 flex flex-col">
|
color: "bg-emerald-400",
|
||||||
{/* Chart Item 1 */}
|
labelColor: "text-white"
|
||||||
<div className="flex items-center gap-3">
|
},
|
||||||
<span className="text-white font-persian text-sm min-w-[140px] text-right">کاهش توقفات تولید</span>
|
{
|
||||||
<div className="flex-1 flex items-center bg-gray-700 rounded-full h-5 relative">
|
label: "رفع گلوگاه تولید",
|
||||||
<div
|
value: stats.percentBottleneckRemoval || 0,
|
||||||
className="bg-emerald-400 h-5 rounded-full transition-all duration-500 ease-out"
|
color: "bg-emerald-400",
|
||||||
style={{
|
labelColor: "text-white"
|
||||||
width: `${Math.min((stats.percentProductionStops || 0), 100)}%`
|
},
|
||||||
}}
|
{
|
||||||
>
|
label: "کاهش ارز بری",
|
||||||
</div>
|
value: stats.percentCurrencyReduction || 0,
|
||||||
</div>
|
color: "bg-emerald-400",
|
||||||
<span className="text-emerald-400 font-bold text-sm min-w-[40px] text-left">
|
labelColor: "text-white"
|
||||||
{formatNumber(((stats.percentProductionStops ?? 0) as number).toFixed?.(1) ?? (stats.percentProductionStops ?? 0))}%
|
},
|
||||||
</span>
|
{
|
||||||
</div>
|
label: "کاهش خرابی پر تکرار",
|
||||||
|
value: stats.percentFailuresReduction || 0,
|
||||||
{/* Chart Item 2 */}
|
color: "bg-emerald-400",
|
||||||
<div className="flex items-center gap-3">
|
labelColor: "text-white"
|
||||||
<span className="text-white font-persian text-sm min-w-[140px] text-right">رفع گلوگاه تولید</span>
|
}
|
||||||
<div className="flex-1 flex items-center bg-gray-700 rounded-full h-5 relative">
|
]}
|
||||||
<div
|
barHeight="h-5"
|
||||||
className="bg-emerald-400 h-5 rounded-full transition-all duration-500 ease-out"
|
showAxisLabels={true}
|
||||||
style={{
|
/>
|
||||||
width: `${Math.min((stats.percentBottleneckRemoval || 0), 100)}%`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-emerald-400 font-bold text-sm min-w-[40px] text-left">
|
|
||||||
{formatNumber(((stats.percentBottleneckRemoval ?? 0) as number).toFixed?.(1) ?? (stats.percentBottleneckRemoval ?? 0))}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart Item 3 */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-white font-persian text-sm min-w-[140px] text-right">کاهش ارز بری</span>
|
|
||||||
<div className="flex-1 flex items-center bg-gray-700 rounded-full h-5 relative">
|
|
||||||
<div
|
|
||||||
className="bg-emerald-400 h-5 rounded-full transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min((stats.percentCurrencyReduction || 0), 100)}%`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-emerald-400 font-bold text-sm min-w-[40px] text-left">
|
|
||||||
{formatNumber(((stats.percentCurrencyReduction ?? 0) as number).toFixed?.(0) ?? (stats.percentCurrencyReduction ?? 0))}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chart Item 4 */}
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-white font-persian text-sm min-w-[140px] text-right">کاهش خرابی پر تکرار</span>
|
|
||||||
<div className="flex-1 flex items-center bg-gray-700 rounded-full h-5 relative">
|
|
||||||
<div
|
|
||||||
className="bg-emerald-400 h-5 rounded-full transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min((stats.percentFailuresReduction || 0), 100)}%`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-emerald-400 font-bold text-sm min-w-[40px] text-left">
|
|
||||||
{formatNumber(((stats.percentFailuresReduction ?? 0) as number).toFixed?.(1) ?? (stats.percentFailuresReduction ?? 0))}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center ml-[50px] gap-3">
|
|
||||||
<span className="min-w-[140px]"></span>
|
|
||||||
|
|
||||||
<div className="flex w-full justify-between pt-4 border-t border-gray-700">
|
|
||||||
<span className="text-gray-400 text-xs">۰٪</span>
|
|
||||||
{
|
|
||||||
(() => {
|
|
||||||
const p1 = (stats.percentProductionStops || 0);
|
|
||||||
const p2 = (stats.percentBottleneckRemoval || 0);
|
|
||||||
const p3 = (stats.percentCurrencyReduction || 0);
|
|
||||||
const p4 = (stats.percentFailuresReduction || 0);
|
|
||||||
const maxVal = Math.max(p1, p2, p3, p4);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(maxVal / 4))}٪</span>
|
|
||||||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(maxVal / 2))}٪</span>
|
|
||||||
<span className="text-gray-400 text-xs">{formatNumber(Math.round((maxVal * 3) / 4))}٪</span>
|
|
||||||
<span className="text-gray-400 text-xs">{formatNumber(Math.round(maxVal))}٪</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Percentage Scale */}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
144
app/components/ui/custom-bar-chart.tsx
Normal file
144
app/components/ui/custom-bar-chart.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface BarChartData {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
maxValue?: number;
|
||||||
|
color?: string;
|
||||||
|
labelColor?: string;
|
||||||
|
valuePrefix?: string;
|
||||||
|
valueSuffix?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomBarChartProps {
|
||||||
|
data: BarChartData[];
|
||||||
|
title?: string;
|
||||||
|
height?: string;
|
||||||
|
barHeight?: string;
|
||||||
|
showAxisLabels?: boolean;
|
||||||
|
className?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomBarChart({
|
||||||
|
data,
|
||||||
|
title,
|
||||||
|
height = "auto",
|
||||||
|
barHeight = "h-6",
|
||||||
|
showAxisLabels = true,
|
||||||
|
className = "",
|
||||||
|
loading = false
|
||||||
|
}: CustomBarChartProps) {
|
||||||
|
// Calculate the maximum value across all data points for consistent scaling
|
||||||
|
const globalMaxValue = Math.max(...data.map(item => item.maxValue || item.value));
|
||||||
|
|
||||||
|
// Function to format numbers with Persian digits
|
||||||
|
const formatNumber = (value: number): string => {
|
||||||
|
const str = String(value);
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading skeleton
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||||||
|
{title && (
|
||||||
|
<div className="h-7 bg-gray-600 rounded animate-pulse mb-4 w-1/2"></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-3">
|
||||||
|
{/* Label skeleton */}
|
||||||
|
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[160px]"></div>
|
||||||
|
|
||||||
|
{/* Bar skeleton */}
|
||||||
|
<div className="flex-1 bg-gray-700 rounded-full h-6">
|
||||||
|
<div className="h-6 bg-gray-600 rounded-full animate-pulse" style={{ width: `${Math.random() * 60 + 20}%` }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Value skeleton */}
|
||||||
|
<div className="h-4 bg-gray-600 rounded animate-pulse min-w-[60px]"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-6 ${className}`} style={{ height }}>
|
||||||
|
{title && (
|
||||||
|
<h3 className="text-xl font-bold text-white font-persian text-right mb-4">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{data.map((item, index) => {
|
||||||
|
const percentage = globalMaxValue > 0 ? (item.value / globalMaxValue) * 100 : 0;
|
||||||
|
const displayValue = item.value.toFixed(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center gap-3">
|
||||||
|
{/* Label */}
|
||||||
|
<span
|
||||||
|
className={`font-persian text-sm min-w-[160px] text-right ${
|
||||||
|
item.labelColor || 'text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Bar Container */}
|
||||||
|
<div className={`flex-1 flex items-center bg-gray-700 rounded-full relative overflow-hidden ${barHeight}`}>
|
||||||
|
<div
|
||||||
|
className={`${barHeight} rounded-full transition-all duration-700 ease-out relative ${
|
||||||
|
item.color || 'bg-emerald-400'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(percentage, 100)}%`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Add a subtle gradient effect for better visual appeal */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-white/10 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Value Label */}
|
||||||
|
<span className={`font-bold text-sm min-w-[60px] text-left ${
|
||||||
|
item.color?.includes('emerald') ? 'text-emerald-400' :
|
||||||
|
item.color?.includes('blue') ? 'text-blue-400' :
|
||||||
|
item.color?.includes('purple') ? 'text-purple-400' :
|
||||||
|
item.color?.includes('red') ? 'text-red-400' :
|
||||||
|
item.color?.includes('yellow') ? 'text-yellow-400' :
|
||||||
|
'text-emerald-400'
|
||||||
|
}`}>
|
||||||
|
{item.valuePrefix || ''}{formatNumber(parseFloat(displayValue))}{item.valueSuffix || '%'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Axis Labels */}
|
||||||
|
{showAxisLabels && globalMaxValue > 0 && (
|
||||||
|
<div className="flex items-center gap-3 mt-6">
|
||||||
|
<span className="min-w-[160px]"></span>
|
||||||
|
<div className="flex-1 flex justify-between pt-2 border-t border-gray-700">
|
||||||
|
<span className="text-gray-400 text-xs">{formatNumber(0)}%</span>
|
||||||
|
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 4))}%</span>
|
||||||
|
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue / 2))}%</span>
|
||||||
|
<span className="text-gray-400 text-xs">{formatNumber(Math.round((globalMaxValue * 3) / 4))}%</span>
|
||||||
|
<span className="text-gray-400 text-xs">{formatNumber(Math.round(globalMaxValue))}%</span>
|
||||||
|
</div>
|
||||||
|
<span className="min-w-[60px]"></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user