mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
fix(FE): refactor UI Dashboard pixel perfect figma
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import Card from '@/components/Card';
|
||||
import {
|
||||
Dashboard,
|
||||
DashboardOverviewCharts,
|
||||
DashboardComparisonCharts,
|
||||
DashboardChartsSeries,
|
||||
DashboardChartsDataset,
|
||||
} from '@/types/api/dashboard/dashboard';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
|
||||
type DashboardExportChartsProps = {
|
||||
data: Dashboard;
|
||||
analysisMode: string;
|
||||
};
|
||||
|
||||
export type DashboardExportChartsRef = {
|
||||
getChartRefs: () => {
|
||||
key: string;
|
||||
ref: HTMLDivElement | null;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
// Type guard to check if charts is DashboardOverviewCharts
|
||||
function isOverviewCharts(
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||
): charts is DashboardOverviewCharts {
|
||||
if (!charts) return false;
|
||||
return (
|
||||
'deplesi' in charts ||
|
||||
'body_weight' in charts ||
|
||||
'fcr' in charts ||
|
||||
'performance' in charts ||
|
||||
'quality_control' in charts
|
||||
);
|
||||
}
|
||||
|
||||
// Type guard to check if charts is DashboardComparisonCharts
|
||||
function isComparisonCharts(
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||
): charts is DashboardComparisonCharts {
|
||||
if (!charts) return false;
|
||||
return 'farm' in charts || 'flock' in charts || 'kandang' in charts;
|
||||
}
|
||||
|
||||
const lineColors: Record<string, string> = {
|
||||
body_weight: '#10B981',
|
||||
std_body_weight: '#10B981',
|
||||
act_laying: '#1062B9',
|
||||
std_laying: '#1062B9',
|
||||
act_egg_weight: '#10B981',
|
||||
std_egg_weight: '#10B981',
|
||||
act_feed_intake: '#F52419',
|
||||
std_feed_intake: '#F52419',
|
||||
act_uniformity: '#F59E0B',
|
||||
std_uniformity: '#F59E0B',
|
||||
act_fcr: '#10B981',
|
||||
std_fcr: '#10B981',
|
||||
act_fcr_cum: '#F52419',
|
||||
std_fcr_cum: '#10B981',
|
||||
normal: '#10B981',
|
||||
abnormal: '#F52419',
|
||||
act_deplesi: '#10B981',
|
||||
std_deplesi: '#10B981',
|
||||
};
|
||||
|
||||
const defaultLineColors: string[] = [
|
||||
'#10B981',
|
||||
'#1062B9',
|
||||
'#F52419',
|
||||
'#F59E0B',
|
||||
'#7F56D9',
|
||||
];
|
||||
|
||||
// Helper function to get line color
|
||||
const getLineColor = (seriesId: string | number, index: number): string => {
|
||||
const predefinedColor = lineColors[seriesId];
|
||||
if (predefinedColor) {
|
||||
return predefinedColor;
|
||||
}
|
||||
return defaultLineColors[index % defaultLineColors.length];
|
||||
};
|
||||
|
||||
// Mapping for chart type labels
|
||||
const chartTypeLabels: Record<keyof DashboardOverviewCharts, string> = {
|
||||
body_weight: 'Body Weight',
|
||||
performance: 'Performance',
|
||||
fcr: 'FCR',
|
||||
quality_control: 'Quality Control',
|
||||
deplesi: 'Deplesi',
|
||||
};
|
||||
|
||||
const DashboardExportCharts = forwardRef<
|
||||
DashboardExportChartsRef,
|
||||
DashboardExportChartsProps
|
||||
>(({ data, analysisMode }, ref) => {
|
||||
// Create refs for charts - use string keys for flexibility
|
||||
const chartRefs = useRef<{
|
||||
[key: string]: HTMLDivElement | null;
|
||||
}>({});
|
||||
|
||||
// Determine chart keys and labels based on analysis mode
|
||||
const getChartConfig = () => {
|
||||
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||
const overviewKeys: (keyof DashboardOverviewCharts)[] = [
|
||||
'body_weight',
|
||||
'performance',
|
||||
'fcr',
|
||||
'quality_control',
|
||||
'deplesi',
|
||||
];
|
||||
return overviewKeys.map((key) => ({
|
||||
key,
|
||||
label: chartTypeLabels[key],
|
||||
chartData: (data.charts as DashboardOverviewCharts)[key],
|
||||
}));
|
||||
} else if (
|
||||
analysisMode === 'COMPARISON' &&
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
// For comparison mode, find which comparison type has data
|
||||
const comparisonKey = data.charts.farm
|
||||
? 'farm'
|
||||
: data.charts.flock
|
||||
? 'flock'
|
||||
: 'kandang';
|
||||
|
||||
const comparisonLabels: Record<string, string> = {
|
||||
farm: 'Farm Comparison',
|
||||
flock: 'Flock Comparison',
|
||||
kandang: 'Kandang Comparison',
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
key: comparisonKey,
|
||||
label: comparisonLabels[comparisonKey],
|
||||
chartData: data.charts[comparisonKey],
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const chartConfig = getChartConfig();
|
||||
|
||||
// Expose method to get all chart refs
|
||||
useImperativeHandle(ref, () => ({
|
||||
getChartRefs: () => {
|
||||
return chartConfig
|
||||
.map(({ key, label }) => ({
|
||||
key,
|
||||
ref: chartRefs.current[key] || null,
|
||||
label,
|
||||
}))
|
||||
.filter((item) => item.ref !== null);
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
{chartConfig.map(({ key, label, chartData }) => {
|
||||
if (
|
||||
!chartData ||
|
||||
!chartData.dataset ||
|
||||
chartData.dataset.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const seriesData: DashboardChartsSeries[] = chartData.series || [];
|
||||
const dataset: DashboardChartsDataset[] = chartData.dataset || [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
ref={(el: HTMLDivElement | null) => {
|
||||
chartRefs.current[key] = el;
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
}}
|
||||
variant='bordered'
|
||||
>
|
||||
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6'>
|
||||
<div className='text-lg font-semibold'>
|
||||
{label}{' '}
|
||||
<Icon
|
||||
icon='heroicons:information-circle'
|
||||
width={20}
|
||||
height={20}
|
||||
className='inline text-neutral-500'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className='flex flex-wrap gap-3 mb-6'>
|
||||
{seriesData.map((series, index) => {
|
||||
const isStandard = series.id
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes('std');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={series.id}
|
||||
className='flex items-center gap-2 px-3 py-2 rounded-lg border border-neutral-400 bg-neutral-50'
|
||||
>
|
||||
<div
|
||||
className={`w-6 h-0.5 ${
|
||||
isStandard ? 'border-t-2 border-dashed' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isStandard
|
||||
? 'transparent'
|
||||
: getLineColor(series.id, index),
|
||||
borderColor: isStandard
|
||||
? getLineColor(series.id, index)
|
||||
: 'transparent',
|
||||
}}
|
||||
></div>
|
||||
<span className='text-sm text-neutral-900 font-medium'>
|
||||
{series.label}
|
||||
</span>
|
||||
<Icon
|
||||
icon='heroicons:information-circle'
|
||||
width={16}
|
||||
height={16}
|
||||
className='text-neutral-400'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ResponsiveContainer width='100%' height={350}>
|
||||
<LineChart
|
||||
data={dataset}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 10,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||
<XAxis
|
||||
dataKey='week'
|
||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
label={{
|
||||
value: 'Weeks',
|
||||
position: 'insideBottom',
|
||||
offset: -5,
|
||||
style: { fontSize: 12, fill: '#9ca3af' },
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
domain={(() => {
|
||||
const allValues: number[] = [];
|
||||
dataset.forEach((item: DashboardChartsDataset) => {
|
||||
seriesData.forEach((series) => {
|
||||
const value = item[series.id];
|
||||
if (typeof value === 'number') {
|
||||
allValues.push(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (allValues.length === 0) return [0, 100];
|
||||
|
||||
const minValue = Math.min(...allValues);
|
||||
const maxValue = Math.max(...allValues);
|
||||
const padding = (maxValue - minValue) * 0.1;
|
||||
const domainMin = Math.floor(
|
||||
Math.max(0, minValue - padding)
|
||||
);
|
||||
const domainMax = Math.ceil(maxValue + padding);
|
||||
|
||||
return [domainMin, domainMax];
|
||||
})()}
|
||||
/>
|
||||
{seriesData.map((series, index) => {
|
||||
const isStandard = series.id
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes('std');
|
||||
const dataKey = series.id.toString();
|
||||
|
||||
return (
|
||||
<Line
|
||||
key={series.id}
|
||||
type='monotone'
|
||||
dataKey={dataKey}
|
||||
name={series.label}
|
||||
stroke={getLineColor(series.id, index)}
|
||||
opacity={isStandard ? 0.5 : 1}
|
||||
strokeWidth={2}
|
||||
strokeDasharray={isStandard ? '5 5' : undefined}
|
||||
dot={
|
||||
isStandard
|
||||
? false
|
||||
: {
|
||||
r: 3,
|
||||
fill: '#fff',
|
||||
stroke: getLineColor(series.id, index),
|
||||
strokeWidth: 2,
|
||||
}
|
||||
}
|
||||
activeDot={isStandard ? undefined : { r: 5 }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
DashboardExportCharts.displayName = 'DashboardExportCharts';
|
||||
|
||||
export default DashboardExportCharts;
|
||||
Reference in New Issue
Block a user