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 { Badge } from "~/components/ui/badge";
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
|
||||
import type { BarChartData } from "~/components/ui/custom-bar-chart";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -583,108 +585,38 @@ 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 className="p-4">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-bold text-white font-persian text-right mb-2">
|
||||
تاثیرات فرآیندی به صورت درصد مقایسه ای
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Chart Container */}
|
||||
<div className="space-y-6 flex flex-col">
|
||||
{/* Chart Item 1 */}
|
||||
<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.percentProductionStops || 0), 100)}%`
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-emerald-400 font-bold text-sm min-w-[40px] text-left">
|
||||
{formatNumber(((stats.percentProductionStops ?? 0) as number).toFixed?.(1) ?? (stats.percentProductionStops ?? 0))}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Chart Item 2 */}
|
||||
<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.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>
|
||||
<CustomBarChart
|
||||
title="تاثیرات فرآیندی به صورت درصد مقایسه ای"
|
||||
loading={statsLoading}
|
||||
data={[
|
||||
{
|
||||
(() => {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
})()
|
||||
label: "کاهش توقفات تولید",
|
||||
value: stats.percentProductionStops || 0,
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white"
|
||||
},
|
||||
{
|
||||
label: "رفع گلوگاه تولید",
|
||||
value: stats.percentBottleneckRemoval || 0,
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white"
|
||||
},
|
||||
{
|
||||
label: "کاهش ارز بری",
|
||||
value: stats.percentCurrencyReduction || 0,
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white"
|
||||
},
|
||||
{
|
||||
label: "کاهش خرابی پر تکرار",
|
||||
value: stats.percentFailuresReduction || 0,
|
||||
color: "bg-emerald-400",
|
||||
labelColor: "text-white"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Percentage Scale */}
|
||||
]}
|
||||
barHeight="h-5"
|
||||
showAxisLabels={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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