Merge branch 'dev/restu' into 'development'

[FEAT/FE] Add Uniformity Chart Data (Ideal and Outside Range)

See merge request mbugroup/lti-web-client!147
This commit is contained in:
Rivaldi A N S
2026-01-09 03:51:22 +00:00
6 changed files with 316 additions and 83 deletions
+20 -4
View File
@@ -4506,6 +4506,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -4516,6 +4517,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -4597,6 +4599,7 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2", "@typescript-eslint/types": "8.46.2",
@@ -5120,6 +5123,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5825,7 +5829,8 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
"version": "3.2.4", "version": "3.2.4",
@@ -6201,7 +6206,8 @@
"version": "8.6.0", "version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/embla-carousel-react": { "node_modules/embla-carousel-react": {
"version": "8.6.0", "version": "8.6.0",
@@ -6462,6 +6468,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -6635,6 +6642,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -8152,6 +8160,7 @@
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0", "fast-png": "^6.2.0",
@@ -9371,6 +9380,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -9401,6 +9411,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -9468,7 +9479,8 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/react-number-format": { "node_modules/react-number-format": {
"version": "5.4.4", "version": "5.4.4",
@@ -9485,6 +9497,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -9653,7 +9666,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -10519,6 +10533,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -10686,6 +10701,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -189,12 +189,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return; return;
} }
const documents: File[] = []; const documents: File[] = [];
const documentNameToIndex = new Map<string, number>();
let sequentialDocumentIndex = 0;
const deliveriesPayload = values.deliveries.map((d) => { const deliveriesPayload = values.deliveries.map((d) => {
let documentIndex = 0; let documentIndex = -1;
if (d.document && d.document instanceof File) { if (d.document && d.document instanceof File) {
const fileName = d.document.name;
if (documentNameToIndex.has(fileName)) {
documentIndex = documentNameToIndex.get(fileName)!;
} else {
documents.push(d.document); documents.push(d.document);
documentIndex = documents.length - 1; documentIndex = sequentialDocumentIndex;
documentNameToIndex.set(fileName, documentIndex);
sequentialDocumentIndex++;
}
} else if (d.document_path) {
const pathFileName =
d.document_path.split('/').pop() || d.document_path;
if (documentNameToIndex.has(pathFileName)) {
documentIndex = documentNameToIndex.get(pathFileName)!;
} else {
documentIndex = sequentialDocumentIndex;
documentNameToIndex.set(pathFileName, documentIndex);
sequentialDocumentIndex++;
}
} else if (d.document && !(d.document instanceof File)) {
const existingDocFileName =
d.document.path.split('/').pop() || d.document.path;
if (documentNameToIndex.has(existingDocFileName)) {
documentIndex = documentNameToIndex.get(existingDocFileName)!;
} else {
documentIndex = sequentialDocumentIndex;
documentNameToIndex.set(existingDocFileName, documentIndex);
sequentialDocumentIndex++;
}
} }
return { return {
@@ -202,7 +235,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
delivery_cost_per_item: delivery_cost_per_item:
parseInt((d.delivery_cost_per_item || '').toString()) || 0, parseInt((d.delivery_cost_per_item || '').toString()) || 0,
document_index: documentIndex, document_index: documentIndex,
document_path: d.document_path,
driver_name: d.driver_name, driver_name: d.driver_name,
vehicle_plate: d.vehicle_plate, vehicle_plate: d.vehicle_plate,
supplier_id: d.supplier_id, supplier_id: d.supplier_id,
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState, useEffect } from 'react';
import Card from '@/components/Card'; import Card from '@/components/Card';
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
@@ -22,13 +22,27 @@ const UniformityChart = ({
return uniformityData.chart_data; return uniformityData.chart_data;
}, [uniformityData]); }, [uniformityData]);
useEffect(() => {
if (uniformityData?.chart_data?.gauge_chart?.week_info) {
const { current_week_index } =
uniformityData.chart_data.gauge_chart.week_info;
setCurrentWeekIndex(current_week_index);
}
}, [uniformityData]);
const barChartData = useMemo(() => { const barChartData = useMemo(() => {
if (!chartData?.bar_chart) { if (!chartData?.bar_chart || !chartData?.gauge_chart) {
return []; return [];
} }
const { bar_chart } = chartData; const { bar_chart, gauge_chart } = chartData;
const currentWeekStr = String(bar_chart.current_week); const currentWeekData = gauge_chart.available_weeks[currentWeekIndex];
if (!currentWeekData || !currentWeekData.has_data) {
return [];
}
const currentWeekStr = String(currentWeekData.week);
const weekData = bar_chart.all_weeks[currentWeekStr]; const weekData = bar_chart.all_weeks[currentWeekStr];
if (!weekData || !weekData.has_data) { if (!weekData || !weekData.has_data) {
@@ -39,11 +53,10 @@ const UniformityChart = ({
name: range.range, name: range.range,
uv: range.bird_count, uv: range.bird_count,
isIdeal: range.is_ideal_range, isIdeal: range.is_ideal_range,
idealCount: range.is_ideal_range idealRange: range.ideal_range,
? weekData.ideal_range.total_ideal_birds outsideRange: range.outside_range,
: undefined,
})); }));
}, [chartData]); }, [chartData, currentWeekIndex]);
const gaugeChartData = useMemo(() => { const gaugeChartData = useMemo(() => {
if (!chartData?.gauge_chart || !uniformityData) return undefined; if (!chartData?.gauge_chart || !uniformityData) return undefined;
@@ -55,28 +68,33 @@ const UniformityChart = ({
return undefined; return undefined;
} }
const hasPrevWeek = currentWeekIndex > 0;
const hasNextWeek =
currentWeekIndex < gauge_chart.available_weeks.length - 1;
return { return {
value: currentWeekData.uniformity_percentage, value: currentWeekData.uniformity_percentage,
label: 'Uniformity', label: 'Uniformity',
week: `Week ${currentWeekData.week}`, week: `Week ${currentWeekData.week}`,
currentValue: currentWeekData.ideal_count, currentValue: currentWeekData.ideal_count,
totalValue: currentWeekData.total_count, totalValue: currentWeekData.total_count,
hasPrevWeek: gauge_chart.week_info.has_prev_week, hasPrevWeek,
hasNextWeek: gauge_chart.week_info.has_next_week, hasNextWeek,
}; };
}, [chartData, currentWeekIndex, uniformityData]); }, [chartData, currentWeekIndex, uniformityData]);
const handleWeekChange = (direction: 'prev' | 'next') => { const handleWeekChange = (direction: 'prev' | 'next') => {
if (!chartData?.gauge_chart) return; if (!chartData?.gauge_chart) return;
const { available_weeks, week_info } = chartData.gauge_chart; const { available_weeks } = chartData.gauge_chart;
if (direction === 'prev' && week_info.has_prev_week) { if (direction === 'prev' && currentWeekIndex > 0) {
setCurrentWeekIndex((prev) => Math.max(0, prev - 1)); setCurrentWeekIndex((prev) => prev - 1);
} else if (direction === 'next' && week_info.has_next_week) { } else if (
setCurrentWeekIndex((prev) => direction === 'next' &&
Math.min(available_weeks.length - 1, prev + 1) currentWeekIndex < available_weeks.length - 1
); ) {
setCurrentWeekIndex((prev) => prev + 1);
} }
}; };
@@ -230,6 +230,7 @@ const UniformityTable = () => {
const [filterStartDate, setFilterStartDate] = useState(''); const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState('');
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
const { const {
setInputValue: setFilterLocationInputValue, setInputValue: setFilterLocationInputValue,
@@ -423,9 +424,38 @@ const UniformityTable = () => {
}, []); }, []);
const handleApplyFilters = useCallback(() => { const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
if (!filterLocation) {
errors.location = 'Lokasi wajib dipilih';
}
if (!filterProjectFlock) {
errors.project_flock = 'Project Flock wajib dipilih';
}
if (!filterKandang) {
errors.kandang = 'Kandang wajib dipilih';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true); setIsSubmitted(true);
filterModal.closeModal(); filterModal.closeModal();
}, [filterModal]); }
}, [
filterModal,
filterStartDate,
filterEndDate,
filterLocation,
filterProjectFlock,
filterKandang,
]);
const selectedRowIds = useMemo(() => { const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection) return Object.keys(rowSelection)
@@ -614,7 +644,7 @@ const UniformityTable = () => {
if (filterEndDate) { if (filterEndDate) {
queryParams.append('end_date', filterEndDate); queryParams.append('end_date', filterEndDate);
} }
queryParams.append('limit', '10000'); queryParams.append('limit', '100');
queryParams.append('page', '1'); queryParams.append('page', '1');
const queryString = queryParams.toString(); const queryString = queryParams.toString();
@@ -1124,58 +1154,105 @@ const UniformityTable = () => {
</div> </div>
<div className='space-y-4 px-4'> <div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'> <div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput <DateInput
label='Tanggal' label='Tanggal'
name='start_date' name='start_date'
value={filterStartDate} value={filterStartDate}
onChange={(e) => setFilterStartDate(e.target.value)} onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div>
<DateInput <DateInput
label=' ' label=' '
name='end_date' name='end_date'
value={filterEndDate} value={filterEndDate}
onChange={(e) => setFilterEndDate(e.target.value)} onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div> </div>
<div>
<SelectInput <SelectInput
label='Lokasi' label='Lokasi'
placeholder='Pilih Lokasi...' placeholder='Pilih Lokasi...'
value={filterLocation} value={filterLocation}
onChange={handleFilterLocationChange} onChange={(value) => {
handleFilterLocationChange(value);
setFilterErrors((prev) => ({ ...prev, location: '' }));
}}
options={filterLocationOptions} options={filterLocationOptions}
onInputChange={setFilterLocationInputValue} onInputChange={setFilterLocationInputValue}
isLoading={isLoadingFilterLocations} isLoading={isLoadingFilterLocations}
isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.location && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.location}
</p>
)}
</div>
<div>
<SelectInput <SelectInput
label='Project Flock' label='Project Flock'
placeholder='Pilih Project Flock...' placeholder='Pilih Project Flock...'
value={filterProjectFlock} value={filterProjectFlock}
onChange={handleFilterProjectFlockChange} onChange={(value) => {
handleFilterProjectFlockChange(value);
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
}}
options={filterProjectFlockOptions} options={filterProjectFlockOptions}
onInputChange={setProjectFlockSearchValue} onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingFilterProjectFlocks} isLoading={isLoadingFilterProjectFlocks}
isDisabled={!filterLocation} isDisabled={!filterLocation}
isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.project_flock && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.project_flock}
</p>
)}
</div>
<div>
<SelectInput <SelectInput
label='Kandang' label='Kandang'
placeholder='Pilih Kandang...' placeholder='Pilih Kandang...'
value={filterKandang} value={filterKandang}
onChange={handleFilterKandangChange} onChange={(value) => {
handleFilterKandangChange(value);
setFilterErrors((prev) => ({ ...prev, kandang: '' }));
}}
options={filterKandangOptions} options={filterKandangOptions}
isDisabled={!filterProjectFlock} isDisabled={!filterProjectFlock}
isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
{filterErrors.kandang && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.kandang}
</p>
)}
</div>
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}
@@ -27,7 +27,8 @@ interface BarChartData {
name: string; name: string;
uv: number; uv: number;
isIdeal?: boolean; isIdeal?: boolean;
idealCount?: number; idealRange?: string;
outsideRange?: string;
} }
interface UniformityBarChartProps { interface UniformityBarChartProps {
@@ -40,30 +41,117 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
const chartData = data.payload as BarChartData; const chartData = data.payload as BarChartData;
const labelStr = String(label); const labelStr = String(label);
if (chartData.isIdeal && chartData.idealCount !== undefined) { // If the range has both ideal and outside ranges (like 340-344)
if (chartData.idealRange && chartData.outsideRange) {
return ( return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'> <div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p> <p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'> <div className='flex flex-col gap-2 mt-2'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div> <div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
{chartData.idealCount} of Birds <span className='text-sm'>Ideal</span>
</div> </div>
<span>{labelStr}</span> <span className='text-sm font-medium'>
{chartData.idealRange}
</span>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#EF4444] rounded-md'></div>
<span className='text-sm'>Outside</span>
</div>
<span className='text-sm font-medium'>
{chartData.outsideRange}
</span>
</div>
<div className='border-t border-white/20 pt-2 mt-1'>
<div className='flex items-center justify-between'>
<span className='text-white/70 text-sm'>Total Birds:</span>
<span className='font-semibold'>{payload[0].value}</span>
</div>
</div>
<div className='text-center text-xs text-white/50'>{labelStr}</div>
</div> </div>
</div> </div>
); );
} }
// If the range has only ideal range
if (chartData.idealRange) {
return ( return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'> <div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p> <p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'> <div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div> <div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
{payload[0].value} of Birds <span className='text-sm'>Ideal</span>
</div>
<span className='text-sm font-medium'>{chartData.idealRange}</span>
</div>
<div className='border-t border-white/20 pt-2 mt-2'>
<div className='flex items-center justify-between'>
<span className='text-white/70 text-sm'>Birds:</span>
<span className='font-semibold'>{payload[0].value}</span>
</div>
</div>
<div className='text-center text-xs text-white/50 mt-1'>
{labelStr}
</div>
</div>
);
}
// If the range has only outside range
if (chartData.outsideRange) {
return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#EF4444] rounded-md'></div>
<span className='text-sm'>Outside</span>
</div>
<span className='text-sm font-medium'>
{chartData.outsideRange}
</span>
</div>
<div className='border-t border-white/20 pt-2 mt-2'>
<div className='flex items-center justify-between'>
<span className='text-white/70 text-sm'>Birds:</span>
<span className='font-semibold'>{payload[0].value}</span>
</div>
</div>
<div className='text-center text-xs text-white/50 mt-1'>
{labelStr}
</div>
</div>
);
}
// Fallback for backward compatibility
return (
<div className='bg-[#18181B] p-2.5 shadow-sm text-white rounded-2xl rounded-bl-none'>
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'>
<div
className='w-5 h-5 rounded-md'
style={{
backgroundColor: chartData.isIdeal ? '#0069E0' : '#EF4444',
}}
></div>
<span className='text-sm'>
{chartData.isIdeal ? 'Ideal' : 'Outside'}
</span>
</div>
<span className='text-sm font-medium'>{labelStr}</span>
</div>
<div className='border-t border-white/20 pt-2 mt-2'>
<div className='flex items-center justify-between'>
<span className='text-white/70 text-sm'>Birds:</span>
<span className='font-semibold'>{payload[0].value}</span>
</div> </div>
<span>{labelStr}</span>
</div> </div>
</div> </div>
); );
+2
View File
@@ -8,6 +8,8 @@ export type WeightDistributionRange = {
max_weight: number; max_weight: number;
bird_count: number; bird_count: number;
is_ideal_range: boolean; is_ideal_range: boolean;
ideal_range?: string;
outside_range?: string;
}; };
export type IdealRange = { export type IdealRange = {