diff --git a/package-lock.json b/package-lock.json index 43e4a964..d7ffd3eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1478,9 +1478,9 @@ } }, "node_modules/@reduxjs/toolkit/node_modules/immer": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz", - "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==", + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -1983,7 +1983,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2066,7 +2065,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2590,7 +2588,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3256,8 +3253,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -3860,7 +3856,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4034,7 +4029,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5524,7 +5518,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -6625,7 +6618,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6656,7 +6648,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6708,8 +6699,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-number-format": { "version": "5.4.4", @@ -6726,7 +6716,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -6816,8 +6805,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -7673,7 +7661,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7841,7 +7828,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 4f2c344e..426cf6b9 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,9 +1,7 @@ +import DashboardProduction from '@/components/pages/dashboard/DashboardProduction'; + const Dashboard = () => { - return ( -
-

Dashboard

-
- ); + return ; }; export default Dashboard; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index ea27fd80..b6703549 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -3,7 +3,7 @@ import Card from '@/components/Card'; import Table from '@/components/Table'; -import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatNumber } from '@/lib/helper'; import { RowSapronakCalculation, TotalSapronakCalculation, @@ -54,7 +54,7 @@ const ClosingSapronakCalculationTable = ({ footer: total ? () => (
- {formatNumber(total.qty_masuk)} + {formatNumber(total?.qty_masuk)}
) : '', @@ -66,7 +66,7 @@ const ClosingSapronakCalculationTable = ({ footer: total ? () => (
- {formatNumber(total.qty_keluar)} + {formatNumber(total?.qty_keluar)}
) : '', @@ -78,7 +78,7 @@ const ClosingSapronakCalculationTable = ({ footer: total ? () => (
- {formatNumber(total.qty_pakai)} + {formatNumber(total?.qty_pakai)}
) : '', @@ -102,7 +102,7 @@ const ClosingSapronakCalculationTable = ({ footer: total ? () => (
- {formatCurrency(total.harga_beli_per_qty)} + {formatCurrency(total?.harga_beli_per_qty)}
) : '', @@ -114,7 +114,7 @@ const ClosingSapronakCalculationTable = ({ footer: total ? () => (
- {formatCurrency(total.total_harga)} + {formatCurrency(total?.total_harga)}
) : '', @@ -131,7 +131,7 @@ const ClosingSapronakCalculationTable = ({ const docBroilerColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.doc_broiler.total) + ? createColumns(sapronakCalculation.data?.doc_broiler?.total) : createColumns(), [sapronakCalculation] ); @@ -139,7 +139,7 @@ const ClosingSapronakCalculationTable = ({ const ovkColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.ovk.total) + ? createColumns(sapronakCalculation.data?.ovk?.total) : createColumns(), [sapronakCalculation] ); @@ -147,7 +147,7 @@ const ClosingSapronakCalculationTable = ({ const pakanColumns = useMemo( () => isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.pakan.total) + ? createColumns(sapronakCalculation.data?.pakan?.total) : createColumns(), [sapronakCalculation] ); @@ -166,7 +166,7 @@ const ClosingSapronakCalculationTable = ({ data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc_broiler.rows ?? []) + ? (sapronakCalculation.data?.doc_broiler?.rows ?? []) : [] } columns={docBroilerColumns} @@ -189,7 +189,7 @@ const ClosingSapronakCalculationTable = ({ data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.ovk.rows ?? []) + ? (sapronakCalculation.data?.ovk?.rows ?? []) : [] } columns={ovkColumns} @@ -212,7 +212,7 @@ const ClosingSapronakCalculationTable = ({ data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pakan.rows ?? []) + ? (sapronakCalculation.data?.pakan?.rows ?? []) : [] } columns={pakanColumns} diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx new file mode 100644 index 00000000..fb8190aa --- /dev/null +++ b/src/components/pages/dashboard/DashboardProduction.tsx @@ -0,0 +1,399 @@ +'use client'; + +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import { Icon } from '@iconify/react'; +import ProductionLineChart from '@/components/pages/dashboard/chart/ProductionLineChart'; +import StandardLineChart from '@/components/pages/dashboard/chart/StandardLineChart'; +import EggWeightBarChart from '@/components/pages/dashboard/chart/EggWeightBarChart'; +import FCRBarChart from '@/components/pages/dashboard/chart/FCRBarChart'; +import ProductionStat from '@/components/pages/dashboard/chart/ProductionStat'; +import Modal, { useModal } from '@/components/Modal'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import { RadioGroup } from '@/components/input/RadioInput'; +import { useState } from 'react'; +import useSWR from 'swr'; +import { DashboardApi } from '@/services/api/dashboard'; +import { useFormik } from 'formik'; +import dashboardProductionFilterSchema from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema'; +import { ProjectFlockApi } from '@/services/api/production'; +import { ProductionStandardApi } from '@/services/api/master-data'; + +const DashboardProduction = () => { + const filterModal = useModal(); + const [selectedPeriod, setSelectedPeriod] = useState('daily'); + const [selectedStandards, setSelectedStandards] = useState([ + 'hen_day', + 'hen_house', + ]); + const [endpointUrl, setEndpointUrl] = useState('/dashboard'); + + // ===== FETCH DATA ===== + const { + data: dashboardProductionResponse, + isLoading: isLoadingDashboardProductionData, + error: dashboardProductionError, + } = useSWR(endpointUrl, () => + DashboardApi.getDashboardProductionFetcher(endpointUrl) + ); + + const dashboardProductionData = + dashboardProductionResponse?.status === 'success' + ? dashboardProductionResponse.data + : undefined; + + // ===== SELECT ===== + const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } = + useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', { + limit: 'limit', + category: 'LAYING', + }); + const { + options: standardProductionOptions, + isLoadingOptions: isLoadingStandardProductionOptions, + } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { + limit: 'limit', + }); + + // ===== FORMIK ===== + const formik = useFormik({ + initialValues: { + startDate: '', + endDate: '', + flock: [] as OptionType[], + standard_production_id: [] as OptionType[], + standard_productions: [] as OptionType[], + period: selectedPeriod, + }, + validationSchema: dashboardProductionFilterSchema, + onSubmit: (values) => { + console.log(values); + // Build URL with query parameters + const params = new URLSearchParams(); + + if (values.startDate) params.set('startDate', values.startDate); + if (values.endDate) params.set('endDate', values.endDate); + + if (values.flock && values.flock.length > 0) { + const flockIds = values.flock + .map((f: OptionType) => f.value || f) + .join(','); + params.set('flock', flockIds); + } + + if ( + values.standard_production_id && + values.standard_production_id.length > 0 + ) { + const standardIds = values.standard_production_id + .map((s: OptionType) => s.value || s) + .join(','); + params.set('standard_production_id', standardIds); + } + + if (selectedStandards.length > 0) { + params.set('standards', selectedStandards.join(',')); + } + + params.set('period', selectedPeriod); + + const newUrl = `/dashboard?${params.toString()}`; + setEndpointUrl(newUrl); + + // Close modal after applying filter + filterModal.closeModal(); + }, + }); + + const handleResetFilter = () => { + formik.resetForm(); + setSelectedPeriod('daily'); + setSelectedStandards(['hen_day', 'hen_house']); + setEndpointUrl('/dashboard'); + }; + + if (isLoadingDashboardProductionData) { + return ( +
+ +
+ ); + } + return ( + <> +
+
+

Dashboard

+
+ + +
+
+ + {/* Dashboard Statistics */} + + + {/* Charts Grid */} +
+ {/* Production Line Chart */} + + + + + {/* Standard Line Chart */} + + + + + {/* Bar Charts Grid - 2 columns */} +
+ {/* FCR Bar Chart */} + + + + + {/* Egg Weight Bar Chart */} + + + +
+
+
+ +
+ {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+ {/* Rentang Waktu */} +
+ +
+ + + +
+
+ + {/* Flock */} +
+ formik.setFieldValue('flock', selected)} + errorMessage={formik.errors.flock as string} + options={flockOptions} + isLoading={isLoadingFlockOptions} + isMulti + isError={ + Boolean(formik.errors.flock) && Boolean(formik.touched.flock) + } + /> +
+ + {/* Production */} +
+ + formik.setFieldValue('standard_production_id', selected) + } + errorMessage={formik.errors.standard_production_id as string} + options={standardProductionOptions} + isLoading={isLoadingStandardProductionOptions} + isMulti + isError={ + Boolean(formik.errors.standard_production_id) && + Boolean(formik.touched.standard_production_id) + } + /> +
+ + {/* Standard */} +
+ ({ + value: s, + label: + s === 'hen_day' + ? 'Hen Day' + : s === 'hen_house' + ? 'Hen House' + : s === 'uniformity' + ? 'Uniformity' + : s === 'egg_weight' + ? 'Egg Weight' + : 'Egg Mass', + }))} + options={[ + { value: 'hen_day', label: 'Hen Day' }, + { value: 'hen_house', label: 'Hen House' }, + { value: 'uniformity', label: 'Uniformity' }, + { value: 'egg_weight', label: 'Egg Weight' }, + { value: 'egg_mass', label: 'Egg Mass' }, + ]} + isMulti + onChange={(selected: OptionType | OptionType[] | null) => { + const values = Array.isArray(selected) + ? selected.map((item) => String(item.value)) + : []; + setSelectedStandards( + values.length > 0 ? values : ['hen_day'] + ); + }} + isError={ + Boolean(formik.errors.standard_productions) && + Boolean(formik.touched.standard_productions) + } + /> +
+ + {/* Periode Perbandingan */} +
+ +
+ + + + +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ + ); +}; + +export default DashboardProduction; diff --git a/src/components/pages/dashboard/chart/EggWeightBarChart.tsx b/src/components/pages/dashboard/chart/EggWeightBarChart.tsx new file mode 100644 index 00000000..7a9a02c6 --- /dev/null +++ b/src/components/pages/dashboard/chart/EggWeightBarChart.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from 'recharts'; +import { DashboardProductionEggWeights } from '@/types/api/dashboard/dashboard-production'; + +interface EggWeightBarChartProps { + data?: DashboardProductionEggWeights[]; +} + +const EggWeightBarChart = ({ data }: EggWeightBarChartProps) => { + // Show loading state if no data + if (!data || data.length === 0) { + return ( +
+

+ Rata-rata Berat Telur (EW) +

+
+

Memuat data...

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

Rata-rata Berat Telur (EW)

+ + + + + + + value !== undefined ? [`${value} gram`, ''] : ['', ''] + } + cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }} + /> + + {data.map((entry, index) => ( + + ))} + + + +
+ ); +}; + +export default EggWeightBarChart; diff --git a/src/components/pages/dashboard/chart/FCRBarChart.tsx b/src/components/pages/dashboard/chart/FCRBarChart.tsx new file mode 100644 index 00000000..2647c7f7 --- /dev/null +++ b/src/components/pages/dashboard/chart/FCRBarChart.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from 'recharts'; +import { DashboardProductionFcrData } from '@/types/api/dashboard/dashboard-production'; + +interface FCRBarChartProps { + data?: DashboardProductionFcrData[]; +} + +// Alternating colors: green and red +const colors = ['#10b981', '#ef4444']; + +const FCRBarChart = ({ data }: FCRBarChartProps) => { + // Show loading state if no data + if (!data || data.length === 0) { + return ( +
+

+ Feed Conversion Ratio (FCR) +

+
+

Memuat data...

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

+ Feed Conversion Ratio (FCR) +

+ + + + + + + value !== undefined ? [value.toFixed(2), 'FCR'] : ['', ''] + } + cursor={{ fill: 'rgba(16, 185, 129, 0.1)' }} + /> + + {data.map((entry, index) => ( + + ))} + + + +
+ ); +}; + +export default FCRBarChart; diff --git a/src/components/pages/dashboard/chart/ProductionLineChart.tsx b/src/components/pages/dashboard/chart/ProductionLineChart.tsx new file mode 100644 index 00000000..470e09c9 --- /dev/null +++ b/src/components/pages/dashboard/chart/ProductionLineChart.tsx @@ -0,0 +1,357 @@ +'use client'; + +import { useState } from 'react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; + +// Sample data in API format +const sampleApiData: ProductionChartItem[] = [ + { + date: '2025-12-01T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 88 }, + { id: 2, name: 'Flock A-001', data: 92 }, + { id: 3, name: 'Flock B-001', data: 90 }, + { id: 4, name: 'Flock B-002', data: 85 }, + ], + }, + { + date: '2025-12-03T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 85 }, + { id: 2, name: 'Flock A-001', data: 95 }, + { id: 3, name: 'Flock B-001', data: 93 }, + { id: 4, name: 'Flock B-002', data: 87 }, + ], + }, + { + date: '2025-12-05T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 82 }, + { id: 2, name: 'Flock A-001', data: 98 }, + { id: 3, name: 'Flock B-001', data: 91 }, + { id: 4, name: 'Flock B-002', data: 84 }, + ], + }, + { + date: '2025-12-07T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 80 }, + { id: 2, name: 'Flock A-001', data: 89 }, + { id: 3, name: 'Flock B-001', data: 88 }, + { id: 4, name: 'Flock B-002', data: 82 }, + ], + }, + { + date: '2025-12-08T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 83 }, + { id: 2, name: 'Flock A-001', data: 92 }, + { id: 3, name: 'Flock B-001', data: 95 }, + { id: 4, name: 'Flock B-002', data: 85 }, + ], + }, + { + date: '2025-12-11T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 81 }, + { id: 2, name: 'Flock A-001', data: 88 }, + { id: 3, name: 'Flock B-001', data: 92 }, + { id: 4, name: 'Flock B-002', data: 83 }, + ], + }, + { + date: '2025-12-13T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 84 }, + { id: 2, name: 'Flock A-001', data: 90 }, + { id: 3, name: 'Flock B-001', data: 89 }, + { id: 4, name: 'Flock B-002', data: 86 }, + ], + }, + { + date: '2025-12-15T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 82 }, + { id: 2, name: 'Flock A-001', data: 94 }, + { id: 3, name: 'Flock B-001', data: 96 }, + { id: 4, name: 'Flock B-002', data: 84 }, + ], + }, + { + date: '2025-12-17T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 80 }, + { id: 2, name: 'Flock A-001', data: 91 }, + { id: 3, name: 'Flock B-001', data: 93 }, + { id: 4, name: 'Flock B-002', data: 82 }, + ], + }, + { + date: '2025-12-19T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 79 }, + { id: 2, name: 'Flock A-001', data: 88 }, + { id: 3, name: 'Flock B-001', data: 90 }, + { id: 4, name: 'Flock B-002', data: 81 }, + ], + }, + { + date: '2025-12-21T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 81 }, + { id: 2, name: 'Flock A-001', data: 97 }, + { id: 3, name: 'Flock B-001', data: 92 }, + { id: 4, name: 'Flock B-002', data: 83 }, + ], + }, + { + date: '2025-12-23T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 83 }, + { id: 2, name: 'Flock A-001', data: 95 }, + { id: 3, name: 'Flock B-001', data: 98 }, + { id: 4, name: 'Flock B-002', data: 85 }, + ], + }, + { + date: '2025-12-25T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 80 }, + { id: 2, name: 'Flock A-001', data: 89 }, + { id: 3, name: 'Flock B-001', data: 94 }, + { id: 4, name: 'Flock B-002', data: 82 }, + ], + }, + { + date: '2025-12-27T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 82 }, + { id: 2, name: 'Flock A-001', data: 93 }, + { id: 3, name: 'Flock B-001', data: 96 }, + { id: 4, name: 'Flock B-002', data: 84 }, + ], + }, + { + date: '2025-12-28T00:00:00Z', + flocks: [ + { id: 1, name: 'Flock A-002', data: 85 }, + { id: 2, name: 'Flock A-001', data: 96 }, + { id: 3, name: 'Flock B-001', data: 95 }, + { id: 4, name: 'Flock B-002', data: 87 }, + ], + }, +]; + +// Helper function to format date based on period +const formatDateByPeriod = ( + dateString: string, + period: 'daily' | 'weekly' | 'monthly' | 'yearly' +): string => { + const date = new Date(dateString); + const monthNames = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'Mei', + 'Jun', + 'Jul', + 'Agu', + 'Sep', + 'Okt', + 'Nov', + 'Des', + ]; + + switch (period) { + case 'daily': + // Format: "1 Des" + return `${date.getDate()} ${monthNames[date.getMonth()]}`; + + case 'weekly': + // Format: "Week 1 Des" + const weekNumber = Math.ceil(date.getDate() / 7); + return `Week ${weekNumber} ${monthNames[date.getMonth()]}`; + + case 'monthly': + // Format: "Des" + return monthNames[date.getMonth()]; + + case 'yearly': + // Format: "2025" + return date.getFullYear().toString(); + + default: + return dateString; + } +}; + +// Type definitions for API data +interface FlockData { + id: number; + name: string; + data: number; +} + +interface ProductionChartItem { + date: string; + flocks: FlockData[]; +} + +interface ProductionChartsData { + production_charts: ProductionChartItem[]; +} + +// Transform API data to Recharts format +const transformProductionData = (apiData: ProductionChartItem[]) => { + return apiData.map((item) => { + const transformed: Record = { + date: item.date.split('T')[0], // Extract YYYY-MM-DD from ISO string + }; + + // Add each flock's data as a property + item.flocks.forEach((flock) => { + transformed[flock.name] = flock.data; + }); + + return transformed; + }); +}; + +interface ProductionLineChartProps { + period?: 'daily' | 'weekly' | 'monthly' | 'yearly'; + data?: ProductionChartItem[]; // Optional API data +} + +const ProductionLineChart = ({ + period = 'daily', + data: apiData, +}: ProductionLineChartProps) => { + // State to track which lines are hidden + const [hiddenLines, setHiddenLines] = useState([]); + + // Use API data if provided, otherwise use sample data + const chartData = apiData + ? transformProductionData(apiData) + : transformProductionData(sampleApiData); + + // Handle legend click to show/hide lines + const handleLegendClick = (dataKey: string) => { + setHiddenLines((prev) => + prev.includes(dataKey) + ? prev.filter((key) => key !== dataKey) + : [...prev, dataKey] + ); + }; + + return ( +
+

+ Performa Produksi per Flock +

+ + + + formatDateByPeriod(value, period)} + /> + + + formatDateByPeriod(value as string, period) + } + /> + { + if (e.dataKey) handleLegendClick(e.dataKey as string); + }} + style={{ cursor: 'pointer' }} + /> + + + + + + +
+ ); +}; + +export default ProductionLineChart; + +// Export types for external use +export type { FlockData, ProductionChartItem, ProductionChartsData }; diff --git a/src/components/pages/dashboard/chart/ProductionStat.tsx b/src/components/pages/dashboard/chart/ProductionStat.tsx new file mode 100644 index 00000000..7e299223 --- /dev/null +++ b/src/components/pages/dashboard/chart/ProductionStat.tsx @@ -0,0 +1,107 @@ +import Card from '@/components/Card'; +import { Icon } from '@iconify/react'; +import { DashboardProductionStatisticsData } from '@/types/api/dashboard/dashboard-production'; +import { formatCurrency } from '@/lib/helper'; + +interface ProductionStatProps { + data?: DashboardProductionStatisticsData[]; +} + +const ProductionStat = ({ data }: ProductionStatProps) => { + // Helper function to get icon based on title + const getIcon = (title: string) => { + if (title.toLowerCase().includes('keuangan')) + return 'heroicons:currency-dollar'; + if (title.toLowerCase().includes('penjualan')) + return 'heroicons:arrow-trending-up'; + if (title.toLowerCase().includes('pembelian')) + return 'heroicons:shopping-cart'; + if (title.toLowerCase().includes('overhead')) return 'heroicons:calculator'; + return 'heroicons:chart-bar'; + }; + + // Helper function to get icon background color + const getIconBgColor = (title: string) => { + if (title.toLowerCase().includes('keuangan')) return 'bg-blue-500'; + if (title.toLowerCase().includes('penjualan')) return 'bg-green-500'; + if (title.toLowerCase().includes('pembelian')) return 'bg-orange-500'; + if (title.toLowerCase().includes('overhead')) return 'bg-purple-500'; + return 'bg-gray-500'; + }; + + // Show loading state if no data + if (!data || data.length === 0) { + return ( +
+ {[1, 2, 3, 4].map((i) => ( + +
+
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+ {data.map((stat, index) => ( + +
+
+

{stat.title}

+

+ {formatCurrency(stat.value)} +

+

+ + {stat.change > 0 ? '+' : ''} + {stat.change}% vs{' '} + {stat.period === 'monthly' ? 'bulan lalu' : 'periode lalu'} +

+
+
+
+ +
+
+
+
+ ))} +
+ ); +}; + +export default ProductionStat; diff --git a/src/components/pages/dashboard/chart/StandardLineChart.tsx b/src/components/pages/dashboard/chart/StandardLineChart.tsx new file mode 100644 index 00000000..18bcabf6 --- /dev/null +++ b/src/components/pages/dashboard/chart/StandardLineChart.tsx @@ -0,0 +1,691 @@ +'use client'; + +import { useState } from 'react'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; + +// Type definitions for API data +interface FlockData { + id: number; + name: string; + data: number; +} + +interface StandardData { + name: string; + value: number; +} + +interface StandardChartItem { + week: number; + standards: StandardData[]; + flocks: FlockData[]; +} + +// Sample data in API format +const sampleApiData: StandardChartItem[] = [ + { + week: 18, + standards: [ + { name: 'hen_day', value: 40 }, + { name: 'hen_house', value: 38 }, + { name: 'uniformity', value: 85 }, + { name: 'egg_weight', value: 52 }, + { name: 'egg_mass', value: 20 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 38 }, + { id: 2, name: 'Flock A-002', data: 37 }, + { id: 3, name: 'Flock B-001', data: 39 }, + { id: 4, name: 'Flock B-002', data: 36 }, + ], + }, + { + week: 20, + standards: [ + { name: 'hen_day', value: 45 }, + { name: 'hen_house', value: 43 }, + { name: 'uniformity', value: 86 }, + { name: 'egg_weight', value: 54 }, + { name: 'egg_mass', value: 24 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 43 }, + { id: 2, name: 'Flock A-002', data: 42 }, + { id: 3, name: 'Flock B-001', data: 44 }, + { id: 4, name: 'Flock B-002', data: 41 }, + ], + }, + { + week: 22, + standards: [ + { name: 'hen_day', value: 48 }, + { name: 'hen_house', value: 46 }, + { name: 'uniformity', value: 87 }, + { name: 'egg_weight', value: 55 }, + { name: 'egg_mass', value: 26 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 47 }, + { id: 2, name: 'Flock A-002', data: 46 }, + { id: 3, name: 'Flock B-001', data: 48 }, + { id: 4, name: 'Flock B-002', data: 45 }, + ], + }, + { + week: 24, + standards: [ + { name: 'hen_day', value: 50 }, + { name: 'hen_house', value: 48 }, + { name: 'uniformity', value: 88 }, + { name: 'egg_weight', value: 56 }, + { name: 'egg_mass', value: 28 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 49 }, + { id: 2, name: 'Flock A-002', data: 48 }, + { id: 3, name: 'Flock B-001', data: 50 }, + { id: 4, name: 'Flock B-002', data: 47 }, + ], + }, + { + week: 26, + standards: [ + { name: 'hen_day', value: 52 }, + { name: 'hen_house', value: 50 }, + { name: 'uniformity', value: 89 }, + { name: 'egg_weight', value: 57 }, + { name: 'egg_mass', value: 30 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 50 }, + { id: 2, name: 'Flock A-002', data: 49 }, + { id: 3, name: 'Flock B-001', data: 51 }, + { id: 4, name: 'Flock B-002', data: 48 }, + ], + }, + { + week: 28, + standards: [ + { name: 'hen_day', value: 55 }, + { name: 'hen_house', value: 53 }, + { name: 'uniformity', value: 90 }, + { name: 'egg_weight', value: 58 }, + { name: 'egg_mass', value: 32 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 53 }, + { id: 2, name: 'Flock A-002', data: 52 }, + { id: 3, name: 'Flock B-001', data: 54 }, + { id: 4, name: 'Flock B-002', data: 51 }, + ], + }, + { + week: 30, + standards: [ + { name: 'hen_day', value: 58 }, + { name: 'hen_house', value: 56 }, + { name: 'uniformity', value: 91 }, + { name: 'egg_weight', value: 59 }, + { name: 'egg_mass', value: 34 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 55 }, + { id: 2, name: 'Flock A-002', data: 54 }, + { id: 3, name: 'Flock B-001', data: 56 }, + { id: 4, name: 'Flock B-002', data: 53 }, + ], + }, + { + week: 32, + standards: [ + { name: 'hen_day', value: 60 }, + { name: 'hen_house', value: 58 }, + { name: 'uniformity', value: 92 }, + { name: 'egg_weight', value: 60 }, + { name: 'egg_mass', value: 36 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 58 }, + { id: 2, name: 'Flock A-002', data: 57 }, + { id: 3, name: 'Flock B-001', data: 59 }, + { id: 4, name: 'Flock B-002', data: 56 }, + ], + }, + { + week: 34, + standards: [ + { name: 'hen_day', value: 62 }, + { name: 'hen_house', value: 60 }, + { name: 'uniformity', value: 92 }, + { name: 'egg_weight', value: 61 }, + { name: 'egg_mass', value: 38 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 60 }, + { id: 2, name: 'Flock A-002', data: 59 }, + { id: 3, name: 'Flock B-001', data: 61 }, + { id: 4, name: 'Flock B-002', data: 58 }, + ], + }, + { + week: 36, + standards: [ + { name: 'hen_day', value: 64 }, + { name: 'hen_house', value: 62 }, + { name: 'uniformity', value: 93 }, + { name: 'egg_weight', value: 62 }, + { name: 'egg_mass', value: 40 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 62 }, + { id: 2, name: 'Flock A-002', data: 61 }, + { id: 3, name: 'Flock B-001', data: 63 }, + { id: 4, name: 'Flock B-002', data: 60 }, + ], + }, + { + week: 38, + standards: [ + { name: 'hen_day', value: 66 }, + { name: 'hen_house', value: 64 }, + { name: 'uniformity', value: 93 }, + { name: 'egg_weight', value: 63 }, + { name: 'egg_mass', value: 42 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 64 }, + { id: 2, name: 'Flock A-002', data: 63 }, + { id: 3, name: 'Flock B-001', data: 65 }, + { id: 4, name: 'Flock B-002', data: 62 }, + ], + }, + { + week: 40, + standards: [ + { name: 'hen_day', value: 68 }, + { name: 'hen_house', value: 66 }, + { name: 'uniformity', value: 94 }, + { name: 'egg_weight', value: 64 }, + { name: 'egg_mass', value: 44 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 66 }, + { id: 2, name: 'Flock A-002', data: 65 }, + { id: 3, name: 'Flock B-001', data: 67 }, + { id: 4, name: 'Flock B-002', data: 64 }, + ], + }, + { + week: 42, + standards: [ + { name: 'hen_day', value: 70 }, + { name: 'hen_house', value: 68 }, + { name: 'uniformity', value: 94 }, + { name: 'egg_weight', value: 65 }, + { name: 'egg_mass', value: 46 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 68 }, + { id: 2, name: 'Flock A-002', data: 67 }, + { id: 3, name: 'Flock B-001', data: 69 }, + { id: 4, name: 'Flock B-002', data: 66 }, + ], + }, + { + week: 44, + standards: [ + { name: 'hen_day', value: 72 }, + { name: 'hen_house', value: 70 }, + { name: 'uniformity', value: 95 }, + { name: 'egg_weight', value: 66 }, + { name: 'egg_mass', value: 48 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 70 }, + { id: 2, name: 'Flock A-002', data: 69 }, + { id: 3, name: 'Flock B-001', data: 71 }, + { id: 4, name: 'Flock B-002', data: 68 }, + ], + }, + { + week: 46, + standards: [ + { name: 'hen_day', value: 74 }, + { name: 'hen_house', value: 72 }, + { name: 'uniformity', value: 95 }, + { name: 'egg_weight', value: 67 }, + { name: 'egg_mass', value: 50 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 72 }, + { id: 2, name: 'Flock A-002', data: 71 }, + { id: 3, name: 'Flock B-001', data: 73 }, + { id: 4, name: 'Flock B-002', data: 70 }, + ], + }, + { + week: 48, + standards: [ + { name: 'hen_day', value: 76 }, + { name: 'hen_house', value: 74 }, + { name: 'uniformity', value: 95 }, + { name: 'egg_weight', value: 68 }, + { name: 'egg_mass', value: 52 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 74 }, + { id: 2, name: 'Flock A-002', data: 73 }, + { id: 3, name: 'Flock B-001', data: 75 }, + { id: 4, name: 'Flock B-002', data: 72 }, + ], + }, + { + week: 50, + standards: [ + { name: 'hen_day', value: 78 }, + { name: 'hen_house', value: 76 }, + { name: 'uniformity', value: 96 }, + { name: 'egg_weight', value: 69 }, + { name: 'egg_mass', value: 54 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 76 }, + { id: 2, name: 'Flock A-002', data: 75 }, + { id: 3, name: 'Flock B-001', data: 77 }, + { id: 4, name: 'Flock B-002', data: 74 }, + ], + }, + { + week: 52, + standards: [ + { name: 'hen_day', value: 80 }, + { name: 'hen_house', value: 78 }, + { name: 'uniformity', value: 96 }, + { name: 'egg_weight', value: 70 }, + { name: 'egg_mass', value: 56 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 78 }, + { id: 2, name: 'Flock A-002', data: 77 }, + { id: 3, name: 'Flock B-001', data: 79 }, + { id: 4, name: 'Flock B-002', data: 76 }, + ], + }, + { + week: 54, + standards: [ + { name: 'hen_day', value: 82 }, + { name: 'hen_house', value: 80 }, + { name: 'uniformity', value: 96 }, + { name: 'egg_weight', value: 71 }, + { name: 'egg_mass', value: 58 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 80 }, + { id: 2, name: 'Flock A-002', data: 79 }, + { id: 3, name: 'Flock B-001', data: 81 }, + { id: 4, name: 'Flock B-002', data: 78 }, + ], + }, + { + week: 56, + standards: [ + { name: 'hen_day', value: 84 }, + { name: 'hen_house', value: 82 }, + { name: 'uniformity', value: 97 }, + { name: 'egg_weight', value: 72 }, + { name: 'egg_mass', value: 60 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 82 }, + { id: 2, name: 'Flock A-002', data: 81 }, + { id: 3, name: 'Flock B-001', data: 83 }, + { id: 4, name: 'Flock B-002', data: 80 }, + ], + }, + { + week: 58, + standards: [ + { name: 'hen_day', value: 86 }, + { name: 'hen_house', value: 84 }, + { name: 'uniformity', value: 97 }, + { name: 'egg_weight', value: 73 }, + { name: 'egg_mass', value: 62 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 84 }, + { id: 2, name: 'Flock A-002', data: 83 }, + { id: 3, name: 'Flock B-001', data: 85 }, + { id: 4, name: 'Flock B-002', data: 82 }, + ], + }, + { + week: 60, + standards: [ + { name: 'hen_day', value: 88 }, + { name: 'hen_house', value: 86 }, + { name: 'uniformity', value: 97 }, + { name: 'egg_weight', value: 74 }, + { name: 'egg_mass', value: 64 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 86 }, + { id: 2, name: 'Flock A-002', data: 85 }, + { id: 3, name: 'Flock B-001', data: 87 }, + { id: 4, name: 'Flock B-002', data: 84 }, + ], + }, + { + week: 62, + standards: [ + { name: 'hen_day', value: 90 }, + { name: 'hen_house', value: 88 }, + { name: 'uniformity', value: 98 }, + { name: 'egg_weight', value: 75 }, + { name: 'egg_mass', value: 66 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 88 }, + { id: 2, name: 'Flock A-002', data: 87 }, + { id: 3, name: 'Flock B-001', data: 89 }, + { id: 4, name: 'Flock B-002', data: 86 }, + ], + }, + { + week: 64, + standards: [ + { name: 'hen_day', value: 92 }, + { name: 'hen_house', value: 90 }, + { name: 'uniformity', value: 98 }, + { name: 'egg_weight', value: 76 }, + { name: 'egg_mass', value: 68 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 90 }, + { id: 2, name: 'Flock A-002', data: 89 }, + { id: 3, name: 'Flock B-001', data: 91 }, + { id: 4, name: 'Flock B-002', data: 88 }, + ], + }, + { + week: 66, + standards: [ + { name: 'hen_day', value: 94 }, + { name: 'hen_house', value: 92 }, + { name: 'uniformity', value: 98 }, + { name: 'egg_weight', value: 77 }, + { name: 'egg_mass', value: 70 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 92 }, + { id: 2, name: 'Flock A-002', data: 91 }, + { id: 3, name: 'Flock B-001', data: 93 }, + { id: 4, name: 'Flock B-002', data: 90 }, + ], + }, + { + week: 68, + standards: [ + { name: 'hen_day', value: 95 }, + { name: 'hen_house', value: 93 }, + { name: 'uniformity', value: 98 }, + { name: 'egg_weight', value: 78 }, + { name: 'egg_mass', value: 72 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 93 }, + { id: 2, name: 'Flock A-002', data: 92 }, + { id: 3, name: 'Flock B-001', data: 94 }, + { id: 4, name: 'Flock B-002', data: 91 }, + ], + }, + { + week: 70, + standards: [ + { name: 'hen_day', value: 96 }, + { name: 'hen_house', value: 94 }, + { name: 'uniformity', value: 99 }, + { name: 'egg_weight', value: 79 }, + { name: 'egg_mass', value: 74 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 94 }, + { id: 2, name: 'Flock A-002', data: 93 }, + { id: 3, name: 'Flock B-001', data: 95 }, + { id: 4, name: 'Flock B-002', data: 92 }, + ], + }, + { + week: 72, + standards: [ + { name: 'hen_day', value: 97 }, + { name: 'hen_house', value: 95 }, + { name: 'uniformity', value: 99 }, + { name: 'egg_weight', value: 80 }, + { name: 'egg_mass', value: 76 }, + ], + flocks: [ + { id: 1, name: 'Flock A-001', data: 95 }, + { id: 2, name: 'Flock A-002', data: 94 }, + { id: 3, name: 'Flock B-001', data: 96 }, + { id: 4, name: 'Flock B-002', data: 93 }, + ], + }, +]; + +// Transform API data to Recharts format +const transformStandardData = ( + apiData: StandardChartItem[], + selectedStandards: string[] = [ + 'hen_day', + 'hen_house', + 'uniformity', + 'egg_weight', + 'egg_mass', + ] +) => { + return apiData.map((item) => { + const transformed: Record = { + week: item.week, + }; + + // Add selected standards as properties + selectedStandards.forEach((standardName) => { + const standardData = item.standards.find((s) => s.name === standardName); + if (standardData) { + transformed[standardName] = standardData.value; + } + }); + + // Add each flock's data as a property + item.flocks.forEach((flock) => { + transformed[flock.name] = flock.data; + }); + + return transformed; + }); +}; + +interface StandardLineChartProps { + data?: StandardChartItem[]; + selectedStandards?: string[]; +} + +const StandardLineChart = ({ + data: apiData, + selectedStandards = [ + 'hen_day', + 'hen_house', + 'uniformity', + 'egg_weight', + 'egg_mass', + ], +}: StandardLineChartProps) => { + // State to track which lines are hidden + const [hiddenLines, setHiddenLines] = useState([]); + + // Use API data if provided, otherwise use sample data + const chartData = apiData + ? transformStandardData(apiData, selectedStandards) + : transformStandardData(sampleApiData, selectedStandards); + + // Handle legend click to show/hide lines + const handleLegendClick = (dataKey: string) => { + setHiddenLines((prev) => + prev.includes(dataKey) + ? prev.filter((key) => key !== dataKey) + : [...prev, dataKey] + ); + }; + + // Standard line colors mapping + const standardColors: Record = { + hen_day: '#94a3b8', + hen_house: '#64748b', + uniformity: '#475569', + egg_weight: '#334155', + egg_mass: '#1e293b', + }; + + // Standard names mapping for display + const standardLabels: Record = { + hen_day: 'Hen Day', + hen_house: 'Hen House', + uniformity: 'Uniformity', + egg_weight: 'Egg Weight', + egg_mass: 'Egg Mass', + }; + + return ( +
+

+ Perbandingan Henday per Umur +

+ + + + + + + value !== undefined ? [`${value}%`, ''] : ['', ''] + } + labelFormatter={(label) => `Minggu ${label}`} + /> + { + if (e.dataKey) handleLegendClick(e.dataKey as string); + }} + style={{ cursor: 'pointer' }} + /> + {/* Dynamic Standard Lines */} + {selectedStandards.map((standardName) => ( + + ))} + {/* Flock Lines */} + + + + + + +
+ ); +}; + +export default StandardLineChart; + +// Export types for external use +export type { FlockData, StandardData, StandardChartItem }; diff --git a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts new file mode 100644 index 00000000..4ed86a48 --- /dev/null +++ b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts @@ -0,0 +1,16 @@ +import * as yup from 'yup'; + +const dashboardProductionFilterSchema = yup.object({ + startDate: yup.string().optional(), + endDate: yup.string().optional(), + flock: yup.array().optional(), + standard_production_id: yup.array().optional(), + standard_productions: yup.array().optional(), + period: yup.string().optional(), +}); + +export type DashboardProductionFilterValues = yup.InferType< + typeof dashboardProductionFilterSchema +>; + +export default dashboardProductionFilterSchema; diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index c69f089f..ccd57ec3 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -16,7 +16,7 @@ import { } from '@/components/pages/expense/form/ExpenseRequestForm.schema'; import { ExpenseApi } from '@/services/api/expense'; import { isResponseSuccess } from '@/lib/api-helper'; -import { ACCEPTED_FILE_TYPE } from '@/config/constant'; +import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant'; interface ExpenseRealizationContentProps { initialValues?: Expense; @@ -103,24 +103,32 @@ const ExpenseRealizationContent = ({ initialValues?.realization_docs.length > 0 && (
    {initialValues?.realization_docs.map( - (realizationDocument, realizationDocumentIdx) => ( -
  • - - {realizationDocument.path}{' '} - - -
  • - ) + (realizationDocument, realizationDocumentIdx) => { + const path = realizationDocument.path.startsWith( + '/' + ) + ? realizationDocument.path.slice(1) + : realizationDocument.path; + const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`; + return ( +
  • + + {realizationDocument.path}{' '} + + +
  • + ); + } )}
)} @@ -211,7 +219,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.price) + (item) => (expenseGrandTotal += item.qty * item.price) ); return ( @@ -273,7 +281,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseGrandTotal += item.price) + (item) => (expenseGrandTotal += item.qty * item.price) ); return ( diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index b937c5bc..2b9086e0 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -27,7 +27,7 @@ import { UploadRequestDocumentsFormSchema, UploadRequestDocumentsFormValues, } from '@/components/pages/expense/form/ExpenseRequestForm.schema'; -import { ACCEPTED_FILE_TYPE } from '@/config/constant'; +import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant'; import { ExpenseApi } from '@/services/api/expense'; import { isResponseSuccess } from '@/lib/api-helper'; import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line'; @@ -408,9 +408,13 @@ const ExpenseRequestContent = ({ Kandang : - {initialValues?.kandangs - .map((item) => item.name) - .join(', ')} + {initialValues?.kandangs && + initialValues?.kandangs.some((k) => k.name) + ? initialValues?.kandangs + .filter((item) => item.name) + .map((item) => item.name) + .join(', ') + : '-'} @@ -448,7 +452,14 @@ const ExpenseRequestContent = ({ Nominal Biaya : - {formatCurrency(initialValues?.grand_total ?? 0)} + + {formatCurrency( + initialValues?.latest_approval.step_number === 4 || + initialValues?.latest_approval.step_number === 5 + ? (initialValues?.total_realisasi ?? 0) + : (initialValues?.total_pengajuan ?? 0) + )} + Status Pencairan @@ -482,24 +493,32 @@ const ExpenseRequestContent = ({ initialValues?.documents.length > 0 && (
    {initialValues?.documents.map( - (requestDocument, requestDocumentIdx) => ( -
  • - - {requestDocument.path}{' '} - - -
  • - ) + (requestDocument, requestDocumentIdx) => { + const path = requestDocument.path.startsWith( + '/' + ) + ? requestDocument.path.slice(1) + : requestDocument.path; + const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`; + return ( +
  • + + {requestDocument.path}{' '} + + +
  • + ); + } )}
)} @@ -558,7 +577,7 @@ const ExpenseRequestContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.price) + (item) => (expenseGrandTotal += item.qty * item.price) ); return ( @@ -573,7 +592,9 @@ const ExpenseRequestContent = ({ colSpan={5} className='font-bold text-center text-base-content text-lg' > - Biaya {kandangExpense.name} + {kandangExpense.kandang_id && kandangExpense.name + ? `Biaya ${kandangExpense.name}` + : `Biaya ${initialValues?.location.name || 'Umum'}`} diff --git a/src/components/pages/expense/form/ExpenseKandangsTable.tsx b/src/components/pages/expense/form/ExpenseKandangsTable.tsx index b3c9f46d..7d7f76ca 100644 --- a/src/components/pages/expense/form/ExpenseKandangsTable.tsx +++ b/src/components/pages/expense/form/ExpenseKandangsTable.tsx @@ -20,10 +20,10 @@ interface ExpenseKandangsTableProps { locationId?: number; type: 'add' | 'edit' | 'detail'; selectedKandangs: { - id: number; - name: string; + id?: number; + name?: string; }[]; - onChange: (kandangs: { id: number; name: string }[]) => void; + onChange: (kandangs: { id?: number; name?: string }[]) => void; className?: { wrapper?: string; }; @@ -67,7 +67,11 @@ const ExpenseKandangsTable = ({ ); const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>( - convertRowSelectionArrToObj(selectedKandangs.map((item) => item.id)) + convertRowSelectionArrToObj( + selectedKandangs + .map((item) => item.id) + .filter((id): id is number => id !== undefined) + ) ); const kandangsColumns: ColumnDef[] = [ diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts index 77db761c..1f3682ea 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts @@ -1,6 +1,7 @@ import * as Yup from 'yup'; import { Expense } from '@/types/api/expense'; import { formatDate } from '@/lib/helper'; +import { S3_PUBLIC_BASE_URL } from '@/config/constant'; type ExpenseRealizationFormSchemaType = { category?: { @@ -12,7 +13,7 @@ type ExpenseRealizationFormSchemaType = { label: string; }; realization_date?: string; - kandangs?: { id: number; name: string }[]; + kandangs?: { id?: number; name?: string }[]; supplier?: { value: number; label: string; @@ -20,7 +21,7 @@ type ExpenseRealizationFormSchemaType = { existing_documents?: { name: string; url: string }[]; documents?: File[]; realizations: { - kandang_id: number; + kandang_id?: number; cost_items: { nonstock?: { value: number; @@ -49,12 +50,11 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema ({ - name: doc.path, - url: doc.path, - })), + existing_documents: initialValues?.realization_docs?.map((doc) => { + const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path; + return { + name: doc.path, + url: `${S3_PUBLIC_BASE_URL}/${path}`, + }; + }), documents: [], realizations: initialValues?.kandangs ? initialValues.kandangs.map((kandangExpense) => { diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index d1c7c5f2..6526b1c1 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -150,25 +150,10 @@ const ExpenseRealizationForm = ({ formik.setFieldValue('location', val); formik.setFieldValue('kandangs', []); - formik.setFieldValue('realizations', []); - }; - const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { - formik.setFieldTouched('kandangs', true); - formik.setFieldValue('kandangs', kandangs); - - const newRealizations = [...(formik.values.realizations ?? [])]; - - // add new realizations - kandangs.forEach((kandangItem) => { - const isKandangExistInRealization = newRealizations.find( - (realizationItem) => realizationItem.kandang_id === kandangItem.id - ); - - if (isKandangExistInRealization) return; - - newRealizations.push({ - kandang_id: kandangItem.id, + // Auto-create realization item for location (without kandang) + formik.setFieldValue('realizations', [ + { cost_items: [ { nonstock: undefined, @@ -177,25 +162,57 @@ const ExpenseRealizationForm = ({ notes: '', }, ], + }, + ]); + }; + + const kandangsChangeHandler = ( + kandangs: { id?: number; name?: string }[] + ) => { + formik.setFieldTouched('kandangs', true); + formik.setFieldValue('kandangs', kandangs); + + // If no kandangs selected, create realization item for location + if (kandangs.length === 0) { + formik.setFieldValue('realizations', [ + { + cost_items: [ + { + nonstock: undefined, + quantity: undefined, + price: undefined, + notes: '', + }, + ], + }, + ]); + return; + } + + // Start with empty array when kandangs are selected + const newRealizations: typeof formik.values.realizations = []; + + // add new realizations for each kandang + kandangs.forEach((kandangItem) => { + if (!kandangItem.id) return; + + const existingRealization = formik.values.realizations?.find( + (realizationItem) => realizationItem.kandang_id === kandangItem.id + ); + + newRealizations.push({ + kandang_id: kandangItem.id, + cost_items: existingRealization?.cost_items || [ + { + nonstock: undefined, + quantity: undefined, + price: undefined, + notes: '', + }, + ], }); }); - // prune realizations - const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); - const deletedRealizationsIdx: number[] = []; - - newRealizations.forEach((realization, idx) => { - const isRealizationValid = kandangIds.has(realization.kandang_id); - - if (!isRealizationValid) { - deletedRealizationsIdx.push(idx); - } - }); - - deletedRealizationsIdx.forEach((deletedRealizationIdx) => { - newRealizations.splice(deletedRealizationIdx, 1); - }); - formik.setFieldValue('realizations', newRealizations); }; @@ -338,7 +355,10 @@ const ExpenseRealizationForm = ({ )} ; + supplierId?: number; + location?: { + value: number; + label: string; + }; className?: { wrapper?: string; }; @@ -25,12 +30,18 @@ interface ExpenseRealizationKandangDetailExpenseProps { const ExpenseRealizationKandangDetailExpense: React.FC< ExpenseRealizationKandangDetailExpenseProps -> = ({ type, formik, className }) => { +> = ({ type, formik, supplierId, location, className }) => { const { setInputValue: setNonstockInputValue, options: nonstockOptions, isLoadingOptions: isLoadingNonstockOptions, - } = useSelect(NonstockApi.basePath, 'id', 'name'); + } = useSelect( + NonstockApi.basePath, + 'id', + 'name', + 'search', + supplierId ? { supplier_id: String(supplierId) } : undefined + ); const nonstockChangeHandler = ( kandangExpenseIdx: number, @@ -82,140 +93,159 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
- {formik.values.realizations.length === 0 && ( + {!formik.values.supplier?.value && (

- Pilih kandang terlebih dahulu! + Pilih supplier terlebih dahulu!

)} - {formik.values.realizations.map((kandangExpense, kandangExpenseIdx) => { - const kandangName = formik.values.kandangs?.find( - (kandang) => kandang.id === kandangExpense.kandang_id - ); + {formik.values.realizations.length === 0 && + formik.values.supplier?.value && ( +
+

+ Belum ada item biaya. Silakan pilih lokasi terlebih dahulu. +

+
+ )} - return ( - kandangName?.name && ( -
-
-
- Biaya {kandangName?.name} -
+ {formik.values.realizations.length > 0 && + formik.values.supplier?.value && + formik.values.realizations.map( + (kandangExpense, kandangExpenseIdx) => { + const kandangName = kandangExpense.kandang_id + ? formik.values.kandangs?.find( + (kandang) => kandang.id === kandangExpense.kandang_id + ) + : null; -
- - - - - - - - - + return ( + (kandangName?.name || !kandangExpense.kandang_id) && ( +
+
+
+ {kandangName?.name + ? `Biaya ${kandangName.name}` + : location?.label + ? `Biaya ${location.label}` + : 'Biaya Umum'} +
-
- {kandangExpense.cost_items.map( - (expenseItem, expenseIdx) => ( - - - - - - - - +
+
NonstockTotal KuantitasHarga SatuanCatatan
- { - nonstockChangeHandler( - kandangExpenseIdx, - expenseIdx, - val - ); - }} - options={nonstockOptions} - isLoading={isLoadingNonstockOptions} - onInputChange={setNonstockInputValue} - className={{ wrapper: 'min-w-48' }} - isDisabled - /> - - - - - Rp - - } - className={{ wrapper: 'min-w-24' }} - /> - - -
+ + + + + + - ) - )} - -
NonstockTotal KuantitasHarga SatuanCatatan
+ + + + {kandangExpense.cost_items.map( + (expenseItem, expenseIdx) => ( + + + { + nonstockChangeHandler( + kandangExpenseIdx, + expenseIdx, + val + ); + }} + options={nonstockOptions} + isLoading={isLoadingNonstockOptions} + onInputChange={setNonstockInputValue} + className={{ wrapper: 'min-w-48' }} + isDisabled + /> + + + + + + + + + Rp + + } + className={{ wrapper: 'min-w-24' }} + /> + + + + + + + ) + )} + + +
+
-
- - ) - ); - })} + ) + ); + } + )} ); diff --git a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts index 7758df83..71357361 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts @@ -1,6 +1,7 @@ import * as Yup from 'yup'; import { Expense } from '@/types/api/expense'; import { formatDate } from '@/lib/helper'; +import { S3_PUBLIC_BASE_URL } from '@/config/constant'; type ExpenseFormSchemaType = { category?: { @@ -11,8 +12,9 @@ type ExpenseFormSchemaType = { value: number; label: string; }; + location_id: number; transaction_date?: string; - kandangs?: { id: number; name: string }[]; + kandangs?: { id?: number; name?: string }[]; supplier?: { value: number; label: string; @@ -21,7 +23,7 @@ type ExpenseFormSchemaType = { deleted_documents?: number[]; documents?: File[]; expense_nonstocks: { - kandang_id: number; + kandang_id?: number; cost_items: { nonstock?: { value: number; @@ -46,16 +48,17 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = label: Yup.string().required(), }).required('Lokasi wajib diisi!'), + location_id: Yup.number().min(1).required('Lokasi wajib diisi!'), + transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'), kandangs: Yup.array() .of( Yup.object({ - id: Yup.number().required('Kandang wajib dipilih!'), - name: Yup.string().required('Kandang wajib dipilih!'), + id: Yup.number().optional(), + name: Yup.string().optional(), }) ) - .min(1, 'Kandang wajib dipilih!') - .required('Kandang wajib dipilih!'), + .optional(), supplier: Yup.object({ value: Yup.number().min(1).required(), @@ -77,7 +80,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = expense_nonstocks: Yup.array() .of( Yup.object({ - kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(), + kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(), cost_items: Yup.array() .of( Yup.object({ @@ -128,6 +131,7 @@ export const getExpenseFormInitialValues = ( label: initialValues.location.name, } : undefined, + location_id: Number(initialValues?.location.id || 0), transaction_date: initialValues?.transaction_date ? formatDate(initialValues.transaction_date, 'YYYY-MM-DD') : undefined, @@ -141,11 +145,14 @@ export const getExpenseFormInitialValues = ( label: initialValues.supplier.name, } : undefined, - existing_documents: initialValues?.documents?.map((doc) => ({ - id: doc.id, - name: doc.path, - url: doc.path, - })), + existing_documents: initialValues?.documents?.map((doc) => { + const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path; + return { + id: doc.id, + name: doc.path, + url: `${S3_PUBLIC_BASE_URL}/${path}`, + }; + }), deleted_documents: [], documents: [], expense_nonstocks: initialValues?.kandangs diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index 71160785..60e55397 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -108,18 +108,24 @@ const ExpenseRequestForm = ({ const expensePayload: CreateExpensePayload = { category: formik.values.category?.value as 'BOP' | 'NON-BOP', + location_id: values.location_id as number, transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], - expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({ - kandang_id: expenseNonstock.kandang_id, - cost_items: expenseNonstock.cost_items.map((costItem) => ({ - nonstock_id: costItem.nonstock?.value as number, - quantity: parseFloat(String(costItem.quantity)) as number, - price: parseFloat(String(costItem.price)) as number, - notes: costItem.notes ?? '', - })), - })), + expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => { + const basePayload = { + cost_items: expenseNonstock.cost_items.map((costItem) => ({ + nonstock_id: costItem.nonstock?.value as number, + quantity: parseFloat(String(costItem.quantity)) as number, + price: parseFloat(String(costItem.price)) as number, + notes: costItem.notes ?? '', + })), + }; + + return expenseNonstock.kandang_id + ? { ...basePayload, kandang_id: expenseNonstock.kandang_id } + : basePayload; + }), }; switch (type) { @@ -130,19 +136,25 @@ const ExpenseRequestForm = ({ case 'edit': const expenseUpdatePayload: UpdateExpensePayload = { category: formik.values.category?.value as 'BOP' | 'NON-BOP', + location_id: values.location_id as number, transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], expense_nonstocks: values.expense_nonstocks.map( - (expenseNonstock) => ({ - kandang_id: expenseNonstock.kandang_id, - cost_items: expenseNonstock.cost_items.map((costItem) => ({ - nonstock_id: costItem.nonstock?.value as number, - quantity: parseFloat(String(costItem.quantity)) as number, - price: parseFloat(String(costItem.price)) as number, - notes: costItem.notes ?? '', - })), - }) + (expenseNonstock) => { + const basePayload = { + cost_items: expenseNonstock.cost_items.map((costItem) => ({ + nonstock_id: costItem.nonstock?.value as number, + quantity: parseFloat(String(costItem.quantity)) as number, + price: parseFloat(String(costItem.price)) as number, + notes: costItem.notes ?? '', + })), + }; + + return expenseNonstock.kandang_id + ? { ...basePayload, kandang_id: expenseNonstock.kandang_id } + : basePayload; + } ), }; @@ -179,27 +191,14 @@ const ExpenseRequestForm = ({ formik.setFieldTouched('location', true); formik.setFieldValue('location', val); + const locationId = Array.isArray(val) ? val[0]?.value : val?.value; + formik.setFieldValue('location_id', locationId); + formik.setFieldValue('kandangs', []); - formik.setFieldValue('expense_nonstocks', []); - }; - const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { - formik.setFieldTouched('kandangs', true); - formik.setFieldValue('kandangs', kandangs); - - const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])]; - - // add new expense_nonstocks - kandangs.forEach((kandangItem) => { - const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find( - (expenseNonstockItem) => - expenseNonstockItem.kandang_id === kandangItem.id - ); - - if (isKandangExistInExpenseNonstocks) return; - - newExpenseNonstocks.push({ - kandang_id: kandangItem.id, + // Auto-create expense item for location (without kandang) + formik.setFieldValue('expense_nonstocks', [ + { cost_items: [ { nonstock: undefined, @@ -208,25 +207,56 @@ const ExpenseRequestForm = ({ notes: '', }, ], + }, + ]); + }; + + const kandangsChangeHandler = ( + kandangs: { id?: number; name?: string }[] + ) => { + formik.setFieldTouched('kandangs', true); + formik.setFieldValue('kandangs', kandangs); + + // If no kandangs selected, create expense item for location + if (kandangs.length === 0) { + formik.setFieldValue('expense_nonstocks', [ + { + cost_items: [ + { + nonstock: undefined, + quantity: undefined, + price: undefined, + notes: '', + }, + ], + }, + ]); + return; + } + + const newExpenseNonstocks: typeof formik.values.expense_nonstocks = []; + + kandangs.forEach((kandangItem) => { + if (!kandangItem.id) return; + + const existingExpenseNonstock = formik.values.expense_nonstocks?.find( + (expenseNonstockItem) => + expenseNonstockItem.kandang_id === kandangItem.id + ); + + newExpenseNonstocks.push({ + kandang_id: kandangItem.id, + cost_items: existingExpenseNonstock?.cost_items || [ + { + nonstock: undefined, + quantity: undefined, + price: undefined, + notes: '', + }, + ], }); }); - // prune expense_nonstocks - const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); - const deletedExpenseNonstocksIdx: number[] = []; - - newExpenseNonstocks.forEach((expenseNonstock, idx) => { - const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id); - - if (!isExpenseNonstockValid) { - deletedExpenseNonstocksIdx.push(idx); - } - }); - - deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => { - newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1); - }); - formik.setFieldValue('expense_nonstocks', newExpenseNonstocks); }; @@ -454,7 +484,10 @@ const ExpenseRequestForm = ({ )} ; + supplierId?: number; + location?: { + value: number; + label: string; + }; className?: { wrapper?: string; }; @@ -28,12 +33,18 @@ interface ExpenseRequestKandangDetailExpenseProps { const ExpenseRequestKandangDetailExpense: React.FC< ExpenseRequestKandangDetailExpenseProps -> = ({ type, formik, className }) => { +> = ({ type, formik, supplierId, location, className }) => { const { setInputValue: setNonstockInputValue, options: nonstockOptions, isLoadingOptions: isLoadingNonstockOptions, - } = useSelect(NonstockApi.basePath, 'id', 'name'); + } = useSelect( + NonstockApi.basePath, + 'id', + 'name', + 'search', + supplierId ? { supplier_id: String(supplierId) } : undefined + ); const nonstockChangeHandler = ( kandangExpenseIdx: number, @@ -113,41 +124,57 @@ const ExpenseRequestKandangDetailExpense: React.FC<
- {(formik.values.expense_nonstocks.length === 0 || - !formik.values.supplier?.value) && ( + {!formik.values.supplier?.value && (

- Pilih kandang terlebih dahulu! + Pilih supplier terlebih dahulu!

)} + {formik.values.expense_nonstocks.length === 0 && + formik.values.supplier?.value && ( +
+

+ Belum ada item biaya. Silakan pilih lokasi terlebih dahulu. +

+
+ )} + {formik.values.expense_nonstocks.length > 0 && formik.values.supplier?.value && formik.values.expense_nonstocks.map( (kandangExpense, kandangExpenseIdx) => { - const kandangName = formik.values.kandangs?.find( - (kandang) => kandang.id === kandangExpense.kandang_id - ); + const kandangName = kandangExpense.kandang_id + ? formik.values.kandangs?.find( + (kandang) => kandang.id === kandangExpense.kandang_id + ) + : null; return ( - kandangName?.name && ( + (kandangName?.name || !kandangExpense.kandang_id) && (
- Biaya {kandangName?.name} + Biaya {kandangName?.name || location?.label || 'Umum'}
- - - + + + {type !== 'detail' && } diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx index 5b107127..ef1c7d8b 100644 --- a/src/components/pages/expense/pdf/ExpensePDF.tsx +++ b/src/components/pages/expense/pdf/ExpensePDF.tsx @@ -219,7 +219,13 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { { label: 'Lokasi', value: expense?.location.name }, { label: 'Kandang', - value: expense?.kandangs.map((item) => item.name).join(', '), + value: + expense?.kandangs && expense?.kandangs.some((k) => k.name) + ? expense?.kandangs + .filter((item) => item.name) + .map((item) => item.name) + .join(', ') + : '-', }, { label: 'Vendor', value: expense?.supplier.name }, { @@ -235,7 +241,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { { label: 'Nama Pengaju', value: expense?.created_user.name }, { label: 'Nominal Biaya', - value: formatCurrency(expense?.grand_total ?? 0), + value: formatCurrency( + expense?.latest_approval.step_number === 4 || + expense?.latest_approval.step_number === 5 + ? (expense?.total_realisasi ?? 0) + : (expense?.total_pengajuan ?? 0) + ), }, { label: 'Nominal Pengajuan', @@ -326,7 +337,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { let expenseRequestTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseRequestTotal += item.price) + (item) => (expenseRequestTotal += item.qty * item.price) ); return ( @@ -335,7 +346,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { style={ExpensePDFStyle.kandangExpenseContainer} > - {kandangExpense.name} + {kandangExpense.kandang_id && kandangExpense.name + ? `Biaya ${kandangExpense.name}` + : `Biaya ${expense?.location.name || 'Umum'}`} @@ -484,7 +497,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { let expenseRealizationTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseRealizationTotal += item.price) + (item) => (expenseRealizationTotal += item.qty * item.price) ); return ( @@ -493,7 +506,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { style={ExpensePDFStyle.kandangExpenseContainer} > - {kandangExpense.name} + {kandangExpense.kandang_id && kandangExpense.name + ? `Biaya ${kandangExpense.name}` + : `Biaya ${expense?.location.name || 'Umum'}`} diff --git a/src/components/pages/finance/add/FormFinanceAdd.tsx b/src/components/pages/finance/add/FormFinanceAdd.tsx index 9b8259be..f7fdf446 100644 --- a/src/components/pages/finance/add/FormFinanceAdd.tsx +++ b/src/components/pages/finance/add/FormFinanceAdd.tsx @@ -39,6 +39,12 @@ interface FormFinanceAddProps { initialValues?: Finance; } +interface PartyCommonProps { + id: number; + name: string; + account_number: string; +} + const FormFinanceAdd = ({ type = 'add', initialValues, @@ -52,10 +58,12 @@ const FormFinanceAdd = ({ FINANCE_PARTY_TYPE_OPTIONS.find( (option) => option.value === initialValues?.party.type ) || null, - party_id_option: { - label: initialValues?.party.name || '', - value: initialValues?.party.id || 0, - }, + party_id_option: initialValues?.party + ? { + label: initialValues?.party.name || '', + value: initialValues?.party.id || 0, + } + : null, payment_date: initialValues?.payment_date || '', payment_method_option: FINANCE_PAYMENT_METHOD_OPTIONS.find( @@ -97,16 +105,19 @@ const FormFinanceAdd = ({ }); // ===== Options ===== - const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } = - useSelect( - formik.values.party_type_option?.value === 'CUSTOMER' - ? CustomerApi.basePath - : SupplierApi.basePath, - 'id', - 'name', - '', - { limit: 'limit' } - ); + const { + options: partyOptions, + isLoadingOptions: isLoadingPartyOptions, + rawData: partyRawData, + } = useSelect( + formik.values.party_type_option?.value === 'CUSTOMER' + ? CustomerApi.basePath + : SupplierApi.basePath, + 'id', + 'name', + '', + { limit: 'limit' } + ); const { options: bankOptions, rawData: bankRawData, @@ -204,6 +215,14 @@ const FormFinanceAdd = ({ value={formik.values.party_id_option} onChange={(value) => { formik.setFieldValue('party_id_option', value); + if (isResponseSuccess(partyRawData) && value) { + formik.setFieldValue( + 'party_account_number', + partyRawData.data?.find( + (item) => item.id === (value as OptionType)?.value + )?.account_number || '' + ); + } }} isLoading={isLoadingPartyOptions} isError={Boolean( @@ -312,6 +331,7 @@ const FormFinanceAdd = ({ : '' } required + readOnly /> Edit diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index f169eb3c..a0eed811 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -174,19 +174,6 @@ const DeliveryOrderProductForm = ({ }} onReset={handleResetForm} > - {/* - {JSON.stringify(exisitingValues)} - - - {JSON.stringify(formik.values)} - */} - {/* - {JSON.stringify(formik.errors)} - -
- {JSON.stringify(formik.values.marketing_product)} -
*/} - {formikErrorMessage && (
setFormErrorMessage('')} className='my-3 w-full'> {formikErrorMessage} diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index ad50a927..75aa3ba6 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -11,7 +11,7 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { Kandang } from '@/types/api/master-data/kandang'; -import { KandangApi } from '@/services/api/master-data'; +import { KandangApi, WarehouseApi } from '@/services/api/master-data'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouseApi } from '@/services/api/inventory'; import NumberInput from '@/components/input/NumberInput'; @@ -61,7 +61,7 @@ const SalesOrderProductForm = ({ const { options: kandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions, - } = useSelect(KandangApi.basePath, 'id', 'name'); + } = useSelect(WarehouseApi.basePath, 'id', 'name'); const { options: warehouseSourceOptions, diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 47875902..af72f22f 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -79,14 +79,14 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { uomId: initialValues?.uom_id ?? 0, uom: initialValues?.uom ? { - value: initialValues?.uom.id, - label: initialValues?.uom.name, + value: initialValues?.uom?.id, + label: initialValues?.uom?.name, } : null, supplierIds: - initialValues?.suppliers.map((supplier) => supplier.id) ?? [], + initialValues?.suppliers?.map((supplier) => supplier.id) ?? [], suppliers: - initialValues?.suppliers.map((supplier) => ({ + initialValues?.suppliers?.map((supplier) => ({ value: supplier.id, label: supplier.name, })) ?? [], diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts index 6fc3799b..13183e71 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts @@ -18,6 +18,7 @@ const LayingRepeaterFormSchema = Yup.object({ ), target_egg_weight: Yup.number().required('Berat telur wajib diisi!'), target_egg_mass: Yup.number().required('Massa telur wajib diisi!'), + standard_fcr: Yup.number().required('FCR wajib diisi!'), }).required(), }); @@ -35,6 +36,7 @@ const GrowingRepeaterFormSchema = Yup.object({ target_hen_house_production: Yup.number().optional(), target_egg_weight: Yup.number().optional(), target_egg_mass: Yup.number().optional(), + standard_fcr: Yup.number().optional(), }).optional(), }); @@ -68,7 +70,9 @@ export const createProductionStandardRepeaterFormSchema = ( export const createProductionStandardFormSchema = (category: string) => { return Yup.object({ name: Yup.string().required('Nama wajib diisi!'), - project_category: Yup.string().required('Kategori proyek wajib diisi!'), + project_category: Yup.string() + .min(1, 'Kategori proyek wajib diisi!') + .required('Kategori proyek wajib diisi!'), details: Yup.array().of( createProductionStandardRepeaterFormSchema(category) ), diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx index 99edb852..640ded51 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx @@ -29,6 +29,8 @@ import toast from 'react-hot-toast'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useModal } from '@/components/Modal'; import RequirePermission from '@/components/helper/RequirePermission'; +import Tooltip from '@/components/Tooltip'; +import Alert from '@/components/Alert'; type TableRowsType = { customRow: boolean; @@ -41,6 +43,7 @@ type ProductionDetailsErrors = { target_hen_house_production?: string; target_egg_weight?: string; target_egg_mass?: string; + standard_fcr?: string; }; type ProductionDetailsTouched = { @@ -48,6 +51,7 @@ type ProductionDetailsTouched = { target_hen_house_production?: boolean; target_egg_weight?: boolean; target_egg_mass?: boolean; + standard_fcr?: boolean; }; const getProductionDetailsError = ( @@ -91,6 +95,9 @@ const convertPayloadToNumberTypes = (payload: ProductionStandardFormValues) => { target_egg_mass: Number( detail.production_standard_details.target_egg_mass ), + standard_fcr: Number( + detail.production_standard_details.standard_fcr + ), } : undefined, production_standard_uniformity_details: { @@ -131,6 +138,9 @@ const convertStandardValueToFormValues = ( target_egg_mass: Number( detail.egg_production_standard_detail.target_egg_mass ), + standard_fcr: Number( + detail.egg_production_standard_detail.standard_fcr + ), } : undefined, production_standard_uniformity_details: { @@ -175,13 +185,15 @@ const ProductionStandardForm = ({ } = useFormStore(); // ===== Formik ===== + // Initial values - only recalculate when initialValue changes (for edit/detail mode) + // For add mode, we load from cache via useEffect instead to avoid race conditions const formikInitialValues = useMemo(() => { - // For add mode, merge cached data with initial values - if (formType === 'add' && formData) { + if (formType === 'add') { + // Don't use formData here - will be loaded via useEffect return { - name: formData.name || '', - project_category: formData.project_category || '', - details: formData.details || [], + name: '', + project_category: '', + details: [], } as ProductionStandardFormValues; } @@ -190,10 +202,11 @@ const ProductionStandardForm = ({ project_category: initialValue?.project_category || '', details: convertStandardValueToFormValues(initialValue?.details || []), } as ProductionStandardFormValues; - }, [initialValue, formData, formType]); + }, [initialValue, formType]); const formik = useFormik({ initialValues: formikInitialValues as ProductionStandardFormValues, - enableReinitialize: true, + // Only enable reinitialize for edit/detail mode, not add mode + enableReinitialize: formType !== 'add', onSubmit: (values) => { switch (formType) { case 'add': @@ -222,6 +235,7 @@ const ProductionStandardForm = ({ target_hen_house_production: '' as unknown as number, target_egg_weight: '' as unknown as number, target_egg_mass: '' as unknown as number, + standard_fcr: '' as unknown as number, }, production_standard_uniformity_details: { target_mean_bw: '' as unknown as number, @@ -255,36 +269,38 @@ const ProductionStandardForm = ({ const { setValues: repeaterFormikSetValues } = repeaterFormik; // ===== Effect ===== - // Load initial values only when component mounts or when initialValue changes (for edit mode) - // This allows: - // 1. Add mode: Load cached data from formData store - // 2. Edit mode: Load existing data from initialValue - // We use initialValue?.id as dependency to avoid infinite loops + // Load cached data only once on mount for add mode + const [isInitialized, setIsInitialized] = useState(false); + useEffect(() => { - if (formType === 'add' && formData) { - // For add mode, load from cache + if (formType === 'add' && formData && !isInitialized) { + // For add mode, load from cache only on initial mount formikSetValues({ name: formData.name || '', project_category: formData.project_category || '', details: formData.details || [], } as ProductionStandardFormValues); - } else if (formType === 'detail' && initialValue) { - // For detail mode, load from initialValue and convert the details + setIsInitialized(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Only run once on mount + + // For edit/detail mode, update when initialValue changes + useEffect(() => { + if (formType === 'detail' && initialValue) { formikSetValues({ name: initialValue.name || '', project_category: initialValue.project_category || '', details: convertStandardValueToFormValues(initialValue.details || []), } as ProductionStandardFormValues); } else if (formType === 'edit' && initialValue) { - // For edit mode, load from initialValue and convert the details formikSetValues({ name: initialValue.name || '', project_category: initialValue.project_category || '', details: convertStandardValueToFormValues(initialValue.details || []), } as ProductionStandardFormValues); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [formData, initialValue?.id]); // Trigger when formData or initialValue.id changes + }, [initialValue?.id, formType]); // ===== Data Table ===== const tableRows = useMemo(() => { @@ -323,11 +339,6 @@ const ProductionStandardForm = ({ }, [formik.values.details]); const columns = useMemo[]>(() => { const baseColumns: ColumnDef[] = [ - { - header: 'No', - accessorFn: (row, index) => index + 1, - enableSorting: false, - }, { header: 'Minggu', accessorKey: 'week', @@ -363,6 +374,12 @@ const ProductionStandardForm = ({ row.production_standard_details?.target_egg_mass, enableSorting: false, }, + { + header: 'FCR', + accessorFn: (row) => + row.production_standard_details?.standard_fcr, + enableSorting: false, + }, ] : []; @@ -407,6 +424,7 @@ const ProductionStandardForm = ({ variant='outline' color='warning' className='p-2' + type='button' onClick={() => handleEditClick(row.row.original.week)} > @@ -415,6 +433,7 @@ const ProductionStandardForm = ({ variant='outline' color='error' className='p-2' + type='button' onClick={() => handleRemoveRow(row.row.original.week)} > @@ -430,7 +449,7 @@ const ProductionStandardForm = ({ ...uniformityColumns, ...(formType !== 'detail' ? [actionColumn] : []), ]; - }, [formik.values.project_category, formType]); + }, [formik.values, formType]); // ===== Handler ===== const handleAddRow = async ( @@ -488,9 +507,11 @@ const ProductionStandardForm = ({ setIsAddingRow(false); }; - const handleRemoveRow = (week: number) => { - const newValues = (formik.values.details || []).filter( - (detail) => detail.week !== week + const handleRemoveRow = async (week: number) => { + // Access formik.values directly to get the latest values + const currentDetails = formik.values.details || []; + const newValues = currentDetails.filter( + (detail) => Number(detail.week) !== Number(week) ); const updatedFormValues = { @@ -671,6 +692,7 @@ const ProductionStandardForm = ({ target_hen_house_production: 0, target_egg_weight: 0, target_egg_mass: 0, + standard_fcr: 0, }, })); } @@ -745,6 +767,7 @@ const ProductionStandardForm = ({ } required isDisabled={formType === 'detail'} + isClearable />
@@ -803,7 +826,7 @@ const ProductionStandardForm = ({ className={cn( 'grid gap-4 items-start', formik.values.project_category === 'LAYING' - ? 'grid-cols-9' + ? 'grid-cols-10' : 'grid-cols-5' )} > @@ -962,6 +985,41 @@ const ProductionStandardForm = ({ ) } /> + + gr + + } + errorMessage={getProductionDetailsError( + repeaterFormik.errors + .production_standard_details, + 'standard_fcr' + )} + isError={ + Boolean( + getProductionDetailsError( + repeaterFormik.errors + .production_standard_details, + 'standard_fcr' + ) + ) && + getProductionDetailsTouched( + repeaterFormik.touched + .production_standard_details, + 'standard_fcr' + ) + } + /> )} Batal )} - + + {/* Should not be absolute */}
NonstockTotal KuantitasHarga Satuan + Nonstock + + Total Kuantitas + + Harga Satuan + CatatanAksi