106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import React from 'react';
|
|
|
|
export interface ConditionBar {
|
|
label: string;
|
|
actual: number | null;
|
|
standard: number | null;
|
|
unit: string;
|
|
/** Optional note shown under the bar, e.g. why a standard is absent. */
|
|
note?: string;
|
|
}
|
|
|
|
export interface InsightConditionChartProps {
|
|
bars: ConditionBar[];
|
|
title?: string;
|
|
emptyLabel?: string;
|
|
}
|
|
|
|
const AKTUAL = '#dc2626';
|
|
const STANDAR = '#3b82f6';
|
|
|
|
const fmt = (value: number, unit: string) =>
|
|
`${Number(value).toLocaleString('id-ID', { maximumFractionDigits: 1 })}${unit ? ` ${unit}` : ''}`;
|
|
|
|
/**
|
|
* Current readings against their CP 707 standard, one row per metric.
|
|
* Used when there is no per-day history (e.g. live IoT snapshot).
|
|
*/
|
|
const InsightConditionChart: React.FC<InsightConditionChartProps> = ({
|
|
bars,
|
|
title,
|
|
emptyLabel = 'Belum ada pembacaan sensor untuk digambarkan.',
|
|
}) => {
|
|
const usable = bars.filter((bar) => bar.actual !== null || bar.standard !== null);
|
|
|
|
if (usable.length === 0) {
|
|
return (
|
|
<div className="rounded-xl border border-gray-100 bg-gray-50/60 p-4 text-center text-sm text-gray-500">
|
|
{emptyLabel}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-xl border border-gray-100 bg-white p-4 shadow-sm">
|
|
{title && <h4 className="mb-1 text-sm font-bold text-gray-800">{title}</h4>}
|
|
<div className="mb-3 flex items-center gap-4 text-xs text-gray-500">
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ background: AKTUAL }} />
|
|
Aktual
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="inline-block h-2.5 w-2.5 rounded-sm" style={{ background: STANDAR }} />
|
|
Standar CP 707
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{usable.map((bar) => {
|
|
const max = Math.max(bar.actual ?? 0, bar.standard ?? 0, 1);
|
|
const pct = (value: number | null) =>
|
|
value === null ? 0 : Math.max(2, Math.round((value / max) * 100));
|
|
|
|
return (
|
|
<div key={bar.label}>
|
|
<div className="mb-1 flex items-baseline justify-between">
|
|
<span className="text-xs font-medium text-gray-600">{bar.label}</span>
|
|
<span className="text-xs text-gray-500">
|
|
{bar.actual === null ? 'data tidak tersedia' : fmt(bar.actual, bar.unit)}
|
|
{bar.standard !== null && (
|
|
<span className="text-gray-400"> · standar {fmt(bar.standard, bar.unit)}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<div className="h-2.5 w-full rounded-sm bg-gray-100">
|
|
{bar.actual !== null && (
|
|
<div
|
|
className="h-full rounded-sm"
|
|
style={{ width: `${pct(bar.actual)}%`, background: AKTUAL }}
|
|
title={`Aktual ${fmt(bar.actual, bar.unit)}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="h-2.5 w-full rounded-sm bg-gray-100">
|
|
{bar.standard !== null && (
|
|
<div
|
|
className="h-full rounded-sm"
|
|
style={{ width: `${pct(bar.standard)}%`, background: STANDAR }}
|
|
title={`Standar ${fmt(bar.standard, bar.unit)}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{bar.note && <p className="mt-1 text-[11px] text-gray-400">{bar.note}</p>}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default InsightConditionChart;
|