diff --git a/package-lock.json b/package-lock.json index 38844543..61da9724 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", "formik": "^2.4.6", + "html-to-image": "^1.11.13", "input-otp": "^1.4.2", "jspdf": "^3.0.4", "jspdf-autotable": "^5.0.2", @@ -7380,6 +7381,12 @@ "integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==", "license": "ISC" }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", diff --git a/package.json b/package.json index 3a775db2..981413b3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", "formik": "^2.4.6", + "html-to-image": "^1.11.13", "input-otp": "^1.4.2", "jspdf": "^3.0.4", "jspdf-autotable": "^5.0.2", diff --git a/src/app/finance/detail/page.tsx b/src/app/finance/detail/page.tsx index 1d20e9f5..b80e8acb 100644 --- a/src/app/finance/detail/page.tsx +++ b/src/app/finance/detail/page.tsx @@ -24,8 +24,6 @@ const FinanceDetailPage = () => { ); } - console.log(finance); - // if (!finance || isResponseError(finance)) { // router.replace('/404'); // return; 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 - - + + + + {/* Dashboard Stats */} - +
+ +
{/* Use DashboardLineChart component or skeleton */} - {isLoadingDashboardProductionData ? ( - - ) : dashboardProductionData && - dashboardProductionData.charts && - Object.keys(dashboardProductionData.charts).length > 0 ? ( - + {isLoadingDashboardProductionData ? ( + + ) : dashboardProductionData && + dashboardProductionData.charts && + Object.keys(dashboardProductionData.charts).length > 0 ? ( + - ) : ( - + } + data={dashboardProductionData} + selectedKandang={ + analysisMode === 'OVERVIEW' + ? (formik.values.kandang as OptionType) + : undefined + } + /> + ) : ( + + )} + + + {/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */} + {dashboardProductionData && ( +
+ +
)} @@ -287,7 +344,7 @@ const DashboardProduction = () => { {/* Rentang Waktu */}
-
+
{ Boolean(formik.touched.startDate) } /> - — +
—
{ { formik.setFieldValue('location', selected); // Update selectedLocationIds for kandang filter @@ -422,6 +481,8 @@ const DashboardProduction = () => { formik.setFieldValue('flock', selected) } errorMessage={formik.errors.flock as string} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} options={flockOptions} isLoading={isLoadingFlockOptions} isMulti={ @@ -450,6 +511,8 @@ const DashboardProduction = () => { formik.setFieldValue('kandang', selected) } errorMessage={formik.errors.kandang as string} + onInputChange={setInputValueKandang} + onMenuScrollToBottom={loadMoreKandang} options={kandangOptions} isLoading={isLoadingKandangOptions} isMulti={ @@ -465,7 +528,9 @@ const DashboardProduction = () => {
)} - +
+ +
{/* Action Buttons */}
@@ -473,7 +538,6 @@ const DashboardProduction = () => { type='reset' variant='soft' className='ms-4 min-w-36 rounded-lg' - onClick={handleResetFilter} > Reset Filter diff --git a/src/components/pages/dashboard/chart/DashboardAllCharts.tsx b/src/components/pages/dashboard/chart/DashboardAllCharts.tsx new file mode 100644 index 00000000..fe0db0a7 --- /dev/null +++ b/src/components/pages/dashboard/chart/DashboardAllCharts.tsx @@ -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 DashboardAllChartsProps = { + data: Dashboard; + analysisMode: string; +}; + +export type DashboardAllChartsRef = { + 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 = { + 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 = { + body_weight: 'Body Weight', + performance: 'Performance', + fcr: 'FCR', + quality_control: 'Quality Control', + deplesi: 'Deplesi', +}; + +const DashboardAllCharts = forwardRef< + DashboardAllChartsRef, + DashboardAllChartsProps +>(({ 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 = { + 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 ( +
+ {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 ( +
{ + chartRefs.current[key] = el; + }} + > + +
+
+ {label}{' '} + +
+
+ + {/* Legend */} +
+ {seriesData.map((series, index) => { + const isStandard = series.id + .toString() + .toLowerCase() + .includes('std'); + + return ( +
+
+ + {series.label} + + +
+ ); + })} +
+ + {/* Chart */} + + + + + { + 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 ( + + ); + })} + + +
+
+ ); + })} +
+ ); +}); + +DashboardAllCharts.displayName = 'DashboardAllCharts'; + +export default DashboardAllCharts; diff --git a/src/components/pages/dashboard/chart/DashboardLineChart.tsx b/src/components/pages/dashboard/chart/DashboardLineChart.tsx index e586b4a3..f2449795 100644 --- a/src/components/pages/dashboard/chart/DashboardLineChart.tsx +++ b/src/components/pages/dashboard/chart/DashboardLineChart.tsx @@ -1,8 +1,10 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; +import { OptionType } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; +import { formatNumber } from '@/lib/helper'; import { Dashboard, DashboardOverviewCharts, @@ -25,20 +27,29 @@ import { type DashboardLineChartProps = { analysisMode: 'OVERVIEW' | 'COMPARISON'; data: Dashboard; + selectedKandang?: OptionType; }; // Type guard to check if charts is DashboardOverviewCharts function isOverviewCharts( - charts: DashboardOverviewCharts | DashboardComparisonCharts + charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined ): charts is DashboardOverviewCharts { - return 'deplesi' in charts; + 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 + charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined ): charts is DashboardComparisonCharts { - return 'location' in charts || 'flock' in charts || 'kandang' in charts; + if (!charts) return false; + return 'farm' in charts || 'flock' in charts || 'kandang' in charts; } const lineColors: Record = { @@ -94,6 +105,7 @@ const getLineColor = ( const DashboardLineChart = ({ analysisMode, data, + selectedKandang, }: DashboardLineChartProps) => { const [chartData, setChartData] = useState('body_weight'); @@ -123,7 +135,7 @@ const DashboardLineChart = ({ isComparisonCharts(data.charts) ) { const comparisonChart = - data.charts.location || data.charts.flock || data.charts.kandang; + data.charts.farm || data.charts.flock || data.charts.kandang; seriesData = comparisonChart?.series || []; } @@ -224,7 +236,7 @@ const DashboardLineChart = ({ isComparisonCharts(data.charts) ) { const comparisonChart = - data.charts.location || data.charts.flock || data.charts.kandang; + data.charts.farm || data.charts.flock || data.charts.kandang; seriesData = comparisonChart?.series || []; } @@ -283,261 +295,382 @@ 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.farm || + 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.farm || + 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.farm || + 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}`} + content={(props) => { + return ( +
+

+ {analysisMode === 'OVERVIEW' + ? selectedKandang + ? selectedKandang.label || 'Overview Performance' + : 'Overview Performance' + : 'Comparison Performance'} +

+
    + {props.payload.map((item, index) => { + if (item.name.startsWith('STD. ')) return null; + // 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.farm || + 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 === item.name + ); + const color = series?.id + ? getLineColor(series.id, index, analysisMode) + : '#9ca3af'; + const unit = series?.unit; + + return ( +
  • + +
    +
    + {formatNumber(item.value)} + {unit} +
    +
    + {item.name} +
  • + ); + })} +
+

+ Week {props.label} +

+
); - }); - })()} -
-
+ }} + formatter={( + value: number | undefined, + name: string | undefined + ) => { + if ( + value === undefined || + name === undefined || + name.startsWith('STD. ') + ) + return [undefined, undefined]; + + // 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.farm || + 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 id = series?.id || ''; + + return [value, id]; + }} + /> + {/* 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.farm || 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.farm || 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; + })()} +
); }; diff --git a/src/components/pages/dashboard/export/DashboardPDF.ts b/src/components/pages/dashboard/export/DashboardPDF.ts new file mode 100644 index 00000000..17c5bde4 --- /dev/null +++ b/src/components/pages/dashboard/export/DashboardPDF.ts @@ -0,0 +1,262 @@ +import jsPDF from 'jspdf'; +import { toPng } from 'html-to-image'; +import toast from 'react-hot-toast'; +import { formatDate } from '@/lib/helper'; +import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema'; +import { DashboardAllChartsRef } from '@/components/pages/dashboard/chart/DashboardAllCharts'; + +interface DashboardPDFExportParams { + filterValues: DashboardFilterType; + statsRef: React.RefObject; + allChartsRef: React.RefObject; + setExporting: (value: boolean) => void; +} + +export const generateDashboardPDF = async ({ + filterValues, + statsRef, + allChartsRef, + setExporting, +}: DashboardPDFExportParams): Promise => { + try { + setExporting(true); + toast.loading('Generating PDF...', { id: 'export-pdf' }); + + // Wait for DOM to update + await new Promise((resolve) => setTimeout(resolve, 200)); + + const pdf = new jsPDF('p', 'mm', 'a4'); + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + const margin = 10; + let yPosition = margin; + + // Add title + pdf.setFontSize(16); + pdf.setFont('helvetica', 'bold'); + pdf.text('Dashboard Produksi', margin, yPosition); + yPosition += 10; + + // Add filter information (horizontal layout) + pdf.setFontSize(6); + pdf.setFont('helvetica', 'normal'); + + const filterItems: string[] = []; + + // Period + if (filterValues.startDate || filterValues.endDate) { + const periodText = `Periode: ${ + filterValues.startDate + ? formatDate(filterValues.startDate, 'DD MMM YYYY') + : '-' + } s.d ${ + filterValues.endDate + ? formatDate(filterValues.endDate, 'DD MMM YYYY') + : '-' + }`; + filterItems.push(periodText); + } + + // Analysis Mode + const analysisModeText = `Analysis Mode: ${ + filterValues.analysisMode === 'OVERVIEW' + ? 'Performance Overview' + : 'Performance Comparison' + }`; + filterItems.push(analysisModeText); + + // Comparison Type (only for COMPARISON mode) + if ( + filterValues.analysisMode === 'COMPARISON' && + filterValues.comparisonType + ) { + const comparisonTypeLabel = + filterValues.comparisonType === 'FARM' + ? 'Farm' + : filterValues.comparisonType === 'FLOCK' + ? 'Flock' + : filterValues.comparisonType === 'KANDANG' + ? 'Kandang' + : filterValues.comparisonType; + filterItems.push(`Compared By: ${comparisonTypeLabel}`); + } + + // Farm + if (filterValues.location) { + const locationText = Array.isArray(filterValues.location) + ? filterValues.location.map((loc) => loc.label).join(', ') + : filterValues.location.label; + filterItems.push(`Farm: ${locationText || '-'}`); + } + + // Flock + if ( + filterValues.flock && + (Array.isArray(filterValues.flock) + ? filterValues.flock.length > 0 + : filterValues.flock) + ) { + const flockText = Array.isArray(filterValues.flock) + ? filterValues.flock.map((f) => f.label).join(', ') + : filterValues.flock.label; + filterItems.push(`Flock: ${flockText || '-'}`); + } + + // Kandang + if ( + filterValues.kandang && + (Array.isArray(filterValues.kandang) + ? filterValues.kandang.length > 0 + : filterValues.kandang) + ) { + const kandangText = Array.isArray(filterValues.kandang) + ? filterValues.kandang.map((k) => k.label).join(', ') + : filterValues.kandang.label; + filterItems.push(`Kandang: ${kandangText || '-'}`); + } + + // Generated timestamp + filterItems.push(`Dicetak: ${formatDate(new Date(), 'DD MMM YYYY HH:mm')}`); + + // Render filter items horizontally with word wrap and gray background + const maxWidth = pageWidth - 2 * margin; + let currentLine = ''; + const lines: string[] = []; + + // First pass: calculate all lines + filterItems.forEach((item, index) => { + const separator = index > 0 ? ' | ' : ''; + const testLine = currentLine + separator + item; + const testWidth = pdf.getTextWidth(testLine); + + if (testWidth > maxWidth && currentLine !== '') { + lines.push(currentLine); + currentLine = item; + } else { + currentLine = testLine; + } + }); + + // Add last line + if (currentLine) { + lines.push(currentLine); + } + + // Calculate background dimensions + const lineHeight = 3; + const padding = 1; + const backgroundHeight = lines.length * lineHeight + padding * 2; + + // Draw gray background + pdf.setFillColor(240, 240, 240); // Light gray (RGB: 240, 240, 240) + pdf.rect( + margin - padding, + yPosition - padding - 2, + pageWidth - 2 * margin + padding * 2, + backgroundHeight, + 'F' + ); + + // Render text on top of background + lines.forEach((line, index) => { + pdf.text(line, margin, yPosition); + if (index < lines.length - 1) { + yPosition += lineHeight; + } + }); + + yPosition += 10; + + // Capture and add stats if available + if (statsRef.current) { + const statsImage = await toPng(statsRef.current, { + quality: 1, + pixelRatio: 2, + }); + const statsImgProps = pdf.getImageProperties(statsImage); + const statsWidth = pageWidth - 2 * margin; + const statsHeight = + (statsImgProps.height * statsWidth) / statsImgProps.width; + + // Check if we need a new page + if (yPosition + statsHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + } + + pdf.addImage( + statsImage, + 'PNG', + margin, + yPosition, + statsWidth, + statsHeight + ); + yPosition += statsHeight + 10; + } + + if (allChartsRef.current) { + // Get all individual chart refs + const chartRefs = allChartsRef.current.getChartRefs(); + + // Capture each chart separately and add to PDF + for (let i = 0; i < chartRefs.length; i++) { + const { ref: chartElement, label } = chartRefs[i]; + + if (chartElement) { + // Add chart title + pdf.setFontSize(12); + pdf.setFont('helvetica', 'bold'); + + const chartImage = await toPng(chartElement, { + quality: 1, + pixelRatio: 2, + }); + const chartImgProps = pdf.getImageProperties(chartImage); + const chartWidth = pageWidth - 2 * margin; + const chartHeight = + (chartImgProps.height * chartWidth) / chartImgProps.width; + + // Calculate total height needed (title + spacing + chart) + const titleHeight = 10; + const totalHeight = titleHeight + chartHeight; + + // Check if chart fits on current page + if (yPosition + totalHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + } + + // Add title + pdf.text(label, margin, yPosition); + yPosition += titleHeight; + + // Add chart image + pdf.addImage( + chartImage, + 'PNG', + margin, + yPosition, + chartWidth, + chartHeight + ); + + // Update yPosition for next chart (add spacing between charts) + yPosition += chartHeight + 10; + } + } + } + + // Save the PDF + const fileName = `dashboard-production-${new Date().toISOString().split('T')[0]}.pdf`; + pdf.save(fileName); + + toast.success('PDF exported successfully!', { id: 'export-pdf' }); + } catch (error) { + toast.error('Failed to export PDF. Please try again.', { + id: 'export-pdf', + }); + } finally { + setExporting(false); + } +}; diff --git a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts index d62157a8..1e03252b 100644 --- a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts +++ b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts @@ -7,7 +7,7 @@ export type DashboardFilterType = { analysisMode: string; comparisonType: string | undefined; location: OptionType | OptionType[]; - lokasiIds: number[] | undefined; + locationIds: number[] | undefined; flock: OptionType | OptionType[] | undefined; flockIds: number[] | undefined; kandang: OptionType | OptionType[] | undefined; @@ -25,7 +25,7 @@ export const DashboardFilterOverviewSchema: yup.ObjectSchema schema.required('Compared by is required'), otherwise: (schema) => schema.optional(), }), - lokasiIds: yup.array().optional(), + locationIds: yup.array().optional(), flockIds: yup.array().optional(), kandangIds: yup.array().optional(), location: yup @@ -68,7 +68,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema schema.required('Compared by is required'), otherwise: (schema) => schema.optional(), }), - lokasiIds: yup.array().optional(), + locationIds: yup.array().optional(), flockIds: yup.array().optional(), kandangIds: yup.array().optional(), location: yup diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index 9c84ed4d..1f43eae1 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -43,7 +43,7 @@ const ExpenseDetail: React.FC = ({ initialValues }) => { return ( <> -
+
diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index ea4a0e8d..7057ba19 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({ return (
-
+
-
+
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
-
+
@@ -216,127 +216,141 @@ const ExpenseRealizationContent = ({
-
-

- Rincian Pengajuan Biaya Operasional -

+
+
+

+ Rincian Pengajuan Biaya Operasional +

-
- {initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => { - let expenseGrandTotal = 0; +
+ {initialValues?.kandangs.map( + (kandangExpense, kandangExpenseIdx) => { + let expenseGrandTotal = 0; - kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.qty * item.price) - ); + kandangExpense.pengajuans?.forEach( + (item) => (expenseGrandTotal += item.qty * item.price) + ); - return ( -
- - - - - - - - - - - - - - {kandangExpense.pengajuans?.map( - (pengajuanItem, pengajuanIdx) => ( - - - - - + return ( +
+
- Biaya {kandangExpense.name} -
NonstockTotal KuantitasTotal BiayaCatatan
{pengajuanItem.nonstock.name}{pengajuanItem.qty}{formatCurrency(pengajuanItem.price)}{pengajuanItem.note ?? '-'}
+ + + - ) - )} - - - - - - - -
+ Biaya {kandangExpense.name} +
- Total Biaya Keseluruhan: - {formatCurrency(expenseGrandTotal)}
-
- ); - })} + + Nonstock + Total Kuantitas + Total Biaya + Catatan + + + + {kandangExpense.pengajuans?.map( + (pengajuanItem, pengajuanIdx) => ( + + {pengajuanItem.nonstock.name} + {pengajuanItem.qty} + {formatCurrency(pengajuanItem.price)} + + {pengajuanItem.notes ?? '-'} + + + ) + )} + + + + + Total Biaya Keseluruhan: + + + {formatCurrency(expenseGrandTotal)} + + + + +
+ ); + } + )} +
-
-
-

- Rincian Realisasi Biaya Operasional -

+
+

+ Rincian Realisasi Biaya Operasional +

-
- {initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => { - let expenseGrandTotal = 0; +
+ {initialValues?.kandangs.map( + (kandangExpense, kandangExpenseIdx) => { + let expenseGrandTotal = 0; - kandangExpense.realisasi?.forEach( - (item) => (expenseGrandTotal += item.qty * item.price) - ); + kandangExpense.realisasi?.forEach( + (item) => (expenseGrandTotal += item.qty * item.price) + ); - return ( -
- - - - - - - - - - - - - - {kandangExpense.realisasi?.map( - (realisasiItem, realisasiIdx) => ( - - - - - + return ( +
+
- Biaya {kandangExpense.name} -
NonstockTotal KuantitasTotal BiayaCatatan
{realisasiItem.nonstock.name}{realisasiItem.qty}{formatCurrency(realisasiItem.price)}{realisasiItem.note ?? '-'}
+ + + - ) - )} - - - - - - - -
+ Biaya {kandangExpense.name} +
- Total Biaya Keseluruhan: - {formatCurrency(expenseGrandTotal)}
-
- ); - })} + + Nonstock + Total Kuantitas + Total Biaya + Catatan + + + + {kandangExpense.realisasi?.map( + (realisasiItem, realisasiIdx) => ( + + {realisasiItem.nonstock.name} + {realisasiItem.qty} + {formatCurrency(realisasiItem.price)} + + {realisasiItem.notes ?? '-'} + + + ) + )} + + + + + Total Biaya Keseluruhan: + + + {formatCurrency(expenseGrandTotal)} + + + + +
+ ); + } + )} +
diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index a1ad4643..ac814bcf 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -273,7 +273,7 @@ const ExpenseRequestContent = ({ <>
{initialValues && !isLoadingApprovalHistory && approvalHistory && ( -
+
)} @@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
{/* TODO: apply RBAC */} -
+
{isCurrentApprovalOnHeadArea && (
-
+
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
-
+

Rincian Pengajuan Biaya Operasional

@@ -654,7 +654,7 @@ const ExpenseRequestContent = ({ {pengajuanItem.qty} {formatCurrency(pengajuanItem.price)} - {pengajuanItem.note ?? '-'} + {pengajuanItem.notes ?? '-'} ) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index fdfd9cc3..895c0997 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -54,17 +54,19 @@ const RowOptionsMenu = ({ rejectClickHandler: () => void; deleteClickHandler: () => void; }) => { - const showEditButton = - props.row.original.latest_approval.step_number !== 6 && - (props.row.original.latest_approval.step_number === 1 || - props.row.original.latest_approval.step_number === 2 || - props.row.original.latest_approval.step_number === 3 || - props.row.original.latest_approval.step_number === 4); + const showEditButton = props.row.original.latest_approval + ? props.row.original.latest_approval.step_number !== 6 && + (props.row.original.latest_approval.step_number === 1 || + props.row.original.latest_approval.step_number === 2 || + props.row.original.latest_approval.step_number === 3 || + props.row.original.latest_approval.step_number === 4) + : false; // TODO: apply RBAC - const showRealizationButton = - props.row.original.latest_approval.action !== 'REJECTED' && - props.row.original.latest_approval.step_number === 4; + const showRealizationButton = props.row.original.latest_approval + ? props.row.original.latest_approval.action !== 'REJECTED' && + props.row.original.latest_approval.step_number === 4 + : false; return ( @@ -278,6 +280,7 @@ const ExpensesTable = () => { cell: ({ row }) => { const isCheckboxDisabled = !row.getCanSelect() || + !row.original.latest_approval || row.original.latest_approval.action === 'REJECTED'; return ( @@ -413,6 +416,8 @@ const ExpensesTable = () => { const tableEnableRowSelectionHandler: (row: Row) => boolean = ( row ) => { + if (!row.original.latest_approval) return false; + return ( row.original.latest_approval.action !== 'REJECTED' && row.original.latest_approval.step_number !== 6 @@ -692,14 +697,6 @@ const ExpensesTable = () => { )}
- -
@@ -753,17 +750,12 @@ const ExpensesTable = () => { }} /> -
diff --git a/src/components/pages/expense/form/ExpenseKandangsTable.tsx b/src/components/pages/expense/form/ExpenseKandangsTable.tsx index 7d7f76ca..5c60ae1e 100644 --- a/src/components/pages/expense/form/ExpenseKandangsTable.tsx +++ b/src/components/pages/expense/form/ExpenseKandangsTable.tsx @@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; interface ExpenseKandangsTableProps { locationId?: number; type: 'add' | 'edit' | 'detail'; + formType?: 'request' | 'realization'; selectedKandangs: { id?: number; name?: string; @@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps { const ExpenseKandangsTable = ({ type, + formType = 'request', locationId, selectedKandangs, onChange, @@ -172,69 +174,84 @@ const ExpenseKandangsTable = ({ updateSortingFilter('picSort', picSortFilter); }, [sorting, updateSortingFilter]); - return ( - - -
Pilih Kandang
+ // Tampilkan tabel jika: + // 1. Mode request pertama kali (type='add' dan formType='request') + // 2. Atau sudah ada kandang yang dipilih + const shouldShowTable = + (type === 'add' && formType === 'request') || + (selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id)); - -
- } - className='w-full!' - titleClassName='w-full p-0!' - > - - data={isResponseSuccess(kandangs) ? kandangs?.data : []} - columns={kandangsColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0} - totalItems={ - isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} + return ( + <> + {shouldShowTable && ( + - - + > + +
+ {formType === 'realization' + ? 'Kandang yang Direalisasikan' + : 'Pilih Kandang'} +
+ + +
+ } + className='w-full!' + titleClassName='w-full p-0!' + > + + data={isResponseSuccess(kandangs) ? kandangs?.data : []} + columns={kandangsColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0} + totalItems={ + isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(kandangs) && kandangs?.data?.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 first:flex first:flex-row first:justify-start', + paginationClassName: cn({ + hidden: + isResponseSuccess(kandangs) && + kandangs?.meta?.total_pages === 1, + }), + }} + /> + + + )} + ); }; diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts index 1f3682ea..94d6e38d 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts @@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = ( ? formatDate(initialValues?.realization_date, 'YYYY-MM-DD') : undefined, kandangs: initialValues?.kandangs.map((kandang) => ({ - id: kandang.kandang_id, + id: kandang.id, name: kandang.name, })), supplier: initialValues?.supplier @@ -159,7 +159,7 @@ export const getExpenseRealizationFormInitialValues = ( }, quantity: realisasiItem.qty, price: realisasiItem.price, - notes: realisasiItem.note, + notes: realisasiItem.notes, }; }) : kandangExpense.pengajuans @@ -170,7 +170,7 @@ export const getExpenseRealizationFormInitialValues = ( }, quantity: expenseItem.qty, price: expenseItem.price, - notes: expenseItem.note, + notes: expenseItem.notes, })) : []; diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index ed5aea3e..6117c920 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({ }, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]); return ( -
+
- - )} + {FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && + finance.party?.type !== 'SUPPLIER' && ( + + + + )} {FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && ( - - )} + ) && + props.row.original.party?.type !== 'SUPPLIER' && ( + + + + )} {FINANCE_INITIAL_BALANCE_STATUS.includes( props.row.original.transaction_type @@ -199,35 +199,37 @@ const FinanceTable = () => { // ===== Options ===== const transactionTypeOptions = useMemo(() => { - return [ - { label: 'Transfer', value: 'TRANSFER' }, - { label: 'Cash', value: 'CASH' }, - { label: 'Card', value: 'CARD' }, - { label: 'Cheque', value: 'CHEQUE' }, - { label: 'Saldo', value: 'SALDO' }, - ]; - }, []); - const partyTypeOptions = useMemo(() => { return [ { label: 'Customer', value: 'CUSTOMER' }, { label: 'Supplier', value: 'SUPPLIER' }, ]; }, []); + const { + options: partyTypeOptions, + isLoadingOptions: partyTypeIsLoadingOptions, + setInputValue: partyTypeInputValue, + loadMore: partyTypeLoadMore, + } = useSelect( + selectedTransactionType + ? selectedTransactionType.value === 'CUSTOMER' + ? CustomerApi.basePath + : SupplierApi.basePath + : '', + 'id', + 'name' + ); const sortByOptions = useMemo(() => { return [ { label: 'Tanggal Pembayaran', value: 'payment_date' }, { label: 'Tanggal Dibuat', value: 'created_at' }, ]; }, []); - const { options: bankOptions, rawData: bankRawData } = useSelect( - BankApi.basePath, - 'id', - 'alias', - '', - { - limit: 'limit', - } - ); + const { + options: bankOptions, + rawData: bankRawData, + setInputValue: bankInputValue, + loadMore: bankLoadMore, + } = useSelect(BankApi.basePath, 'id', 'alias'); // ===== Handler ===== const searchChangeHandler: ChangeEventHandler = (e) => { @@ -344,10 +346,10 @@ const FinanceTable = () => { }, { header: 'Pihak', - accessorFn: (finance: Finance) => finance.party.name, + accessorFn: (finance: Finance) => finance.party?.name, cell: (props: CellContext) => { - if (props.row.original.party.id) { - return {props.row.original.party.name}; + if (props.row.original.party?.id) { + return {props.row.original.party?.name}; } return {'-'}; }, @@ -368,12 +370,12 @@ const FinanceTable = () => { { header: 'Bank', accessorFn: (finance: Finance) => - `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`, + `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`, }, { header: 'Pengeluaran (Rp)', accessorFn: (finance: Finance) => - formatCurrency(finance.expense_amount), + formatCurrency(Math.abs(finance.expense_amount)), }, { header: 'Pemasukan (Rp)', @@ -476,38 +478,49 @@ const FinanceTable = () => {
+ ({ label: - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.alias + ' - ' + - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.account_number + ' - ' + - bankRawData.data.find((data) => data.id === bank.value) + bankRawData.data.find((data) => data.id === bank?.value) ?.owner, - value: bank.value, + value: bank?.value, })) : [] } label='Bank' value={selectedBank} onChange={bankChangeHandler} - isClearable - /> - { const router = useRouter(); + const [serverErrorMessage, setServerErrorMessage] = useState(''); + const [isSupplier, setIsSupplier] = useState( + initialValues?.party?.type === 'SUPPLIER' + ); // ===== Formik ===== const formikInitialValues = useMemo((): FinanceFormValues => { return { party_type_option: FINANCE_PARTY_TYPE_OPTIONS.find( - (option) => option.value === initialValues?.party.type + (option) => option.value === initialValues?.party?.type ) || null, party_id_option: initialValues?.party ? { - label: initialValues?.party.name || '', - value: initialValues?.party.id || 0, + label: initialValues?.party?.name || '', + value: initialValues?.party?.id || 0, } : null, payment_date: initialValues?.payment_date || '', @@ -72,11 +78,11 @@ const FormFinanceAdd = ({ ) || null, bank_id_option: initialValues?.bank ? { - label: initialValues.bank.name, - value: initialValues.bank.id, + label: initialValues?.bank?.name, + value: initialValues?.bank?.id, } : null, - party_account_number: initialValues?.party.account_number || '', + party_account_number: initialValues?.party?.account_number || '', reference_number: initialValues?.reference_number || '', nominal: initialValues?.nominal.toString() || '', notes: initialValues?.notes || '', @@ -113,20 +119,22 @@ const FormFinanceAdd = ({ options: partyOptions, isLoadingOptions: isLoadingPartyOptions, rawData: partyRawData, + setInputValue: setPartyInputValue, + loadMore: loadMorePartyOptions, } = useSelect( formik.values.party_type_option?.value === 'CUSTOMER' ? CustomerApi.basePath : SupplierApi.basePath, 'id', - 'name', - '', - { limit: 'limit' } + 'name' ); const { options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - } = useSelect(BankApi.basePath, 'id', 'name', '', { limit: 'limit' }); + setInputValue: setBankInputValue, + loadMore: loadMoreBankOptions, + } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== const transformFormValuesToPayload = ( @@ -151,6 +159,7 @@ const FormFinanceAdd = ({ if (isResponseError(response)) { toast.error(response.message); + setServerErrorMessage(response.message); return; } @@ -166,6 +175,7 @@ const FormFinanceAdd = ({ if (isResponseError(response)) { toast.error(response.message); + setServerErrorMessage(response.message); return; } @@ -205,6 +215,7 @@ const FormFinanceAdd = ({ ? formik.errors.party_type_option : '' } + isDisabled={type === 'edit' || isSupplier} required isClearable /> @@ -219,6 +230,8 @@ const FormFinanceAdd = ({ placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`} options={partyOptions} value={formik.values.party_id_option} + onInputChange={setPartyInputValue} + onMenuScrollToBottom={loadMorePartyOptions} onChange={(value) => { formik.setFieldValue('party_id_option', value); if (isResponseSuccess(partyRawData) && value) { @@ -241,7 +254,7 @@ const FormFinanceAdd = ({ } required isClearable - isDisabled={!formik.values.party_type_option?.value} + isDisabled={!formik.values.party_type_option?.value || isSupplier} /> { formik.setFieldValue('bank_id_option', value); }} @@ -318,6 +335,7 @@ const FormFinanceAdd = ({ } required isClearable + isDisabled={isSupplier} />