From cb8a1a17ac914fb4b8db0f274f120a208cd29abe Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 15:36:21 +0700 Subject: [PATCH 1/3] feat(FE): adding export feature on dashboard --- src/components/helper/ButtonFilter.tsx | 8 +- src/components/helper/form/FormErrors.tsx | 2 +- .../pages/dashboard/DashboardProduction.tsx | 113 ++++++++---------- 3 files changed, 58 insertions(+), 65 deletions(-) diff --git a/src/components/helper/ButtonFilter.tsx b/src/components/helper/ButtonFilter.tsx index 81f70b92..90343fed 100644 --- a/src/components/helper/ButtonFilter.tsx +++ b/src/components/helper/ButtonFilter.tsx @@ -1,5 +1,6 @@ import Button, { ButtonProps } from '@/components/Button'; import { getFilledFormikValuesCount } from '@/lib/formik-helper'; +import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; import { FormikValues } from 'formik'; @@ -13,11 +14,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => { - + } + className={{ + content: 'w-full', + }} > - - Export - - + + + + @@ -287,7 +276,7 @@ const DashboardProduction = () => { {/* Rentang Waktu */}
-
+
{ Boolean(formik.touched.startDate) } /> - +
{
)} - +
+ +
{/* Action Buttons */}
From 524036a6bf5e8246fc0e09d6f5a85b590fb95aa9 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 15:43:53 +0700 Subject: [PATCH 2/3] fix(FE): implement lazy load options select --- .../pages/dashboard/DashboardProduction.tsx | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx index 68a4bc11..cf5eeaa2 100644 --- a/src/components/pages/dashboard/DashboardProduction.tsx +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -70,22 +70,32 @@ const DashboardProduction = () => { : undefined; // ===== SELECT ===== - const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } = - useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { - limit: 'limit', - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); const { + setInputValue: setInputValueFlock, + options: flockOptions, + isLoadingOptions: isLoadingFlockOptions, + loadMore: loadMoreFlock, + } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { + limit: 'limit', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', + }); + const { + setInputValue: setInputValueLocation, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocation, } = useSelect(LocationApi.basePath, 'id', 'name', '', { limit: 'limit', }); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } = - useSelect(KandangApi.basePath, 'id', 'name', '', { - limit: 'limit', - location_id: selectedLocationIds ? selectedLocationIds.toString() : '', - }); + const { + setInputValue: setInputValueKandang, + options: kandangOptions, + isLoadingOptions: isLoadingKandangOptions, + loadMore: loadMoreKandang, + } = useSelect(KandangApi.basePath, 'id', 'name', '', { + limit: 'limit', + location_id: selectedLocationIds ? selectedLocationIds.toString() : '', + }); const comparisonTypeOptions = [ { value: 'FARM', label: 'Farm' }, { value: 'FLOCK', label: 'Flock' }, @@ -372,6 +382,8 @@ const DashboardProduction = () => { { formik.setFieldValue('location', selected); // Update selectedLocationIds for kandang filter @@ -411,6 +423,8 @@ const DashboardProduction = () => { formik.setFieldValue('flock', selected) } errorMessage={formik.errors.flock as string} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} options={flockOptions} isLoading={isLoadingFlockOptions} isMulti={ @@ -439,6 +453,8 @@ const DashboardProduction = () => { formik.setFieldValue('kandang', selected) } errorMessage={formik.errors.kandang as string} + onInputChange={setInputValueKandang} + onMenuScrollToBottom={loadMoreKandang} options={kandangOptions} isLoading={isLoadingKandangOptions} isMulti={ From 3cb11f615892ad637e564e7994866689191813bd Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 16:27:13 +0700 Subject: [PATCH 3/3] fix(FE): add empty state overlay on chart null value --- .../dashboard/chart/DashboardLineChart.tsx | 544 ++++++++++-------- 1 file changed, 297 insertions(+), 247 deletions(-) diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index e586b4a3..348f6c43 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -283,261 +283,311 @@ const DashboardLineChart = ({ })()}
- {/* Chart */} - - { - // Transform data based on analysisMode - if (analysisMode === 'OVERVIEW') { - // For OVERVIEW mode, use the selected chart data - if (isOverviewCharts(data.charts)) { - const selectedChartData = data.charts[chartData]; - if (!selectedChartData || !selectedChartData.dataset) return []; - return selectedChartData.dataset; + {/* Chart Container with Empty State Overlay */} +
+ {/* Chart */} + + { + // Transform data based on analysisMode + if (analysisMode === 'OVERVIEW') { + // For OVERVIEW mode, use the selected chart data + if (isOverviewCharts(data.charts)) { + const selectedChartData = data.charts[chartData]; + if (!selectedChartData || !selectedChartData.dataset) + return []; + return selectedChartData.dataset; + } + return []; + } else { + // For COMPARISON mode, use the first available comparison chart + if (isComparisonCharts(data.charts)) { + const chartData = + data.charts.location || + data.charts.flock || + data.charts.kandang; + + if (!chartData || !chartData.dataset) return []; + return chartData.dataset; + } + return []; } - return []; - } else { - // For COMPARISON mode, use the first available comparison chart - if (isComparisonCharts(data.charts)) { - const chartData = - data.charts.location || - data.charts.flock || - data.charts.kandang; - - if (!chartData || !chartData.dataset) return []; - return chartData.dataset; - } - return []; - } - })()} - margin={{ - top: 5, - right: 10, - left: 0, - bottom: 5, - }} - > - - - { - // Calculate dynamic domain based on visible data - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - // Get all values from visible series - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 100]; - - const minValue = Math.min(...allValues); - const maxValue = Math.max(...allValues); - - // Add padding (10% on each side) - const padding = (maxValue - minValue) * 0.1; - const domainMin = Math.floor(Math.max(0, minValue - padding)); - const domainMax = Math.ceil(maxValue + padding); - - return [domainMin, domainMax]; })()} - ticks={(() => { - // Calculate dynamic ticks based on domain - let seriesData: DashboardChartsSeries[] = []; - let dataset: DashboardChartsDataset[] = []; - - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - dataset = data.charts[chartData]?.dataset || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - dataset = comparisonChart?.dataset || []; - } - - const visibleSeriesIds = Array.from(visibleSeries); - const allValues: number[] = []; - - dataset.forEach((item: DashboardChartsDataset) => { - visibleSeriesIds.forEach((seriesId) => { - const value = item[seriesId]; - if (typeof value === 'number') { - allValues.push(value); - } - }); - }); - - if (allValues.length === 0) return [0, 25, 50, 75, 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); - - // Generate 5 evenly spaced ticks - const range = domainMax - domainMin; - const step = range / 4; - - return [ - domainMin, - Math.round(domainMin + step), - Math.round(domainMin + step * 2), - Math.round(domainMin + step * 3), - domainMax, - ]; - })()} - /> - `Week ${value}`} - formatter={( - value: number | undefined, - name: string | undefined - ) => { - if (value === undefined || name === undefined) return ['', '']; + > + + + { + // Calculate dynamic domain based on visible data + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; - // Get series data to find the unit - let seriesData: DashboardChartsSeries[] = []; - if ( - analysisMode === 'OVERVIEW' && - isOverviewCharts(data.charts) - ) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } - // Find the series that matches this line's name - const series = seriesData.find((s) => s.label === name); - const unit = series?.unit || ''; + // Get all values from visible series + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; - return [`${value} ${unit}`, name]; - }} - /> - {/* Dynamic Line rendering based on visible series */} - {(() => { - let seriesData: DashboardChartsSeries[] = []; - - if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { - seriesData = data.charts[chartData]?.series || []; - } else if ( - analysisMode === 'COMPARISON' && - isComparisonCharts(data.charts) - ) { - const comparisonChart = - data.charts.location || - data.charts.flock || - data.charts.kandang; - seriesData = comparisonChart?.series || []; - } - - return seriesData - .filter((series) => visibleSeries.has(series.id)) - .map((series, index) => { - const isStandard = series.id - .toString() - .toLowerCase() - .includes('std'); - // Use series.id directly as dataKey to match dataset fields - const dataKey = series.id.toString(); - - return ( - { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); } - activeDot={isStandard ? undefined : { r: 5 }} + }); + }); + + if (allValues.length === 0) return [0, 100]; + + const minValue = Math.min(...allValues); + const maxValue = Math.max(...allValues); + + // Add padding (10% on each side) + const padding = (maxValue - minValue) * 0.1; + const domainMin = Math.floor(Math.max(0, minValue - padding)); + const domainMax = Math.ceil(maxValue + padding); + + return [domainMin, domainMax]; + })()} + ticks={(() => { + // Calculate dynamic ticks based on domain + let seriesData: DashboardChartsSeries[] = []; + let dataset: DashboardChartsDataset[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + dataset = comparisonChart?.dataset || []; + } + + const visibleSeriesIds = Array.from(visibleSeries); + const allValues: number[] = []; + + dataset.forEach((item: DashboardChartsDataset) => { + visibleSeriesIds.forEach((seriesId) => { + const value = item[seriesId]; + if (typeof value === 'number') { + allValues.push(value); + } + }); + }); + + if (allValues.length === 0) return [0, 25, 50, 75, 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); + + // Generate 5 evenly spaced ticks + const range = domainMax - domainMin; + const step = range / 4; + + return [ + domainMin, + Math.round(domainMin + step), + Math.round(domainMin + step * 2), + Math.round(domainMin + step * 3), + domainMax, + ]; + })()} + /> + `Week ${value}`} + formatter={( + value: number | undefined, + name: string | undefined + ) => { + if (value === undefined || name === undefined) return ['', '']; + + // Get series data to find the unit + let seriesData: DashboardChartsSeries[] = []; + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + // Find the series that matches this line's name + const series = seriesData.find((s) => s.label === name); + const unit = series?.unit || ''; + + return [`${value} ${unit}`, name]; + }} + /> + {/* Dynamic Line rendering based on visible series */} + {(() => { + let seriesData: DashboardChartsSeries[] = []; + + if ( + analysisMode === 'OVERVIEW' && + isOverviewCharts(data.charts) + ) { + seriesData = data.charts[chartData]?.series || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || + data.charts.flock || + data.charts.kandang; + seriesData = comparisonChart?.series || []; + } + + return seriesData + .filter((series) => visibleSeries.has(series.id)) + .map((series, index) => { + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + // Use series.id directly as dataKey to match dataset fields + const dataKey = series.id.toString(); + + return ( + + ); + }); + })()} + + + + {/* Empty State Overlay */} + {(() => { + // Get current dataset + let dataset: DashboardChartsDataset[] = []; + + if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) { + dataset = data.charts[chartData]?.dataset || []; + } else if ( + analysisMode === 'COMPARISON' && + isComparisonCharts(data.charts) + ) { + const comparisonChart = + data.charts.location || data.charts.flock || data.charts.kandang; + dataset = comparisonChart?.dataset || []; + } + + // Show empty state if dataset is empty + if (dataset.length === 0) { + return ( +
+ {/* Chart icon */} +
+ - ); - }); - })()} - - +
+ + {/* Empty state text */} +

+ Data Not Yet Available +

+

+ Please change your filters to get the data. +

+
+ ); + } + return null; + })()} +
); };