first commit
This commit is contained in:
commit
367533c02b
171 files changed
+101175
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Line, LineChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { formatNumber, formatRatio } from '../utils/format.ts';
|
||||
import type { View } from '../types/common.ts';
|
||||
|
||||
export type KpiStatus = 'ok' | 'warning' | 'critical' | 'na';
|
||||
|
||||
export type KpiCardModel = {
|
||||
key: string;
|
||||
title: string;
|
||||
value: number | null;
|
||||
unit: string;
|
||||
icon: string;
|
||||
trend?: number[];
|
||||
status: KpiStatus;
|
||||
targetView?: View;
|
||||
};
|
||||
|
||||
const statusStyles: Record<KpiStatus, { bg: string; text: string; icon: string; line: string }> = {
|
||||
ok: { bg: 'bg-green-100', text: 'text-green-800', icon: 'text-green-600', line: '#16a34a' },
|
||||
warning: {
|
||||
bg: 'bg-yellow-100',
|
||||
text: 'text-yellow-800',
|
||||
icon: 'text-yellow-600',
|
||||
line: '#f59e0b',
|
||||
},
|
||||
critical: { bg: 'bg-red-100', text: 'text-red-800', icon: 'text-red-600', line: '#dc2626' },
|
||||
na: { bg: 'bg-blue-50', text: 'text-blue-900', icon: 'text-blue-600', line: '#3b82f6' },
|
||||
};
|
||||
|
||||
type Props = Omit<KpiCardModel, 'key' | 'targetView'> & {
|
||||
onClick?: () => void;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const KpiCard: React.FC<Props> = ({
|
||||
title,
|
||||
value,
|
||||
unit,
|
||||
icon,
|
||||
trend,
|
||||
status,
|
||||
onClick,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const isNA = value === null;
|
||||
const styles = isNA ? statusStyles.na : statusStyles[status];
|
||||
const trendData = useMemo(
|
||||
() => (trend ?? []).map((point, index) => ({ name: index, value: point })),
|
||||
[trend]
|
||||
);
|
||||
|
||||
const displayValue = () => {
|
||||
if (value === null) return 'N/A';
|
||||
if (title.toUpperCase().includes('FCR') || title.toUpperCase().includes('EEF')) {
|
||||
return formatRatio(value, 3);
|
||||
}
|
||||
if (unit === 'gram' && value > 999) return formatNumber(value / 1000, 2);
|
||||
return formatNumber(value, 2);
|
||||
};
|
||||
|
||||
const displayUnit = () => {
|
||||
if (value !== null && unit === 'gram' && value > 999) return 'kg';
|
||||
return unit;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-4 sm:p-5 rounded-xl shadow-sm transition-all duration-300 hover:shadow-lg ${styles.bg} ${
|
||||
onClick ? 'cursor-pointer hover:scale-105' : ''
|
||||
} flex flex-col h-full`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<div className={`text-2xl sm:text-3xl font-bold ${styles.text} truncate`}>
|
||||
{isLoading ? (
|
||||
<span className="text-base sm:text-xl">
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2" />
|
||||
Memuat...
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{displayValue()}{' '}
|
||||
{!isNA && <span className="text-lg sm:text-xl font-medium">{displayUnit()}</span>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-xs sm:text-sm font-semibold mt-1 ${styles.text}`}>{title}</p>
|
||||
</div>
|
||||
<div className={`text-2xl sm:text-3xl ${styles.icon} flex-shrink-0 ml-2`}>{icon}</div>
|
||||
</div>
|
||||
<div className="h-12 sm:h-14 lg:h-16 mt-auto">
|
||||
{!isNA && trendData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendData} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={styles.line}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(val) => [formatNumber(Number(val ?? 0), 2), title]}
|
||||
contentStyle={{ fontSize: 12, borderRadius: 8 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-xs text-gray-400">
|
||||
{isNA ? 'Data tidak tersedia' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KpiCard;
|
||||
Reference in new issue
Block a user