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 (
-
- );
+ 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
+
+
+
+
+
+
+
+ >
+ );
+};
+
+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)
+
+
+
+ );
+ }
+
+ 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)
+
+
+
+ );
+ }
+
+ 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)
+ }
+ />
+
+
+
+ );
+};
+
+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}`}
+ />
+
+
+
+ );
+};
+
+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;
-
-
-
-
- | Nonstock |
- Total Kuantitas |
- Harga Satuan |
- Catatan |
-
-
+ return (
+ (kandangName?.name || !kandangExpense.kandang_id) && (
+
+
+
+ {kandangName?.name
+ ? `Biaya ${kandangName.name}`
+ : location?.label
+ ? `Biaya ${location.label}`
+ : 'Biaya Umum'}
+
-
- {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' }}
- />
- |
-
-
-
- |
+
+
+
+
+ | Nonstock |
+ Total Kuantitas |
+ Harga Satuan |
+ Catatan |
- )
- )}
-
-
+
+
+
+ {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'}
- | Nonstock |
- Total Kuantitas |
- Harga Satuan |
+
+ Nonstock
+ |
+
+ Total Kuantitas
+ |
+
+ Harga Satuan
+ |
Catatan |
{type !== 'detail' && Aksi | }
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 */}