{type !== 'detail' && (
- |
+ |
{
|
Produk
*
@@ -1320,7 +1386,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Qty
*
@@ -1329,7 +1395,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Supplier
*
@@ -1338,7 +1404,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Plat Nomor
*
@@ -1348,7 +1414,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Biaya Pengiriman (Rp.)
*
@@ -1357,7 +1423,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Biaya Per Item (Rp.)
*
@@ -1366,7 +1432,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
Nama Sopir
*
@@ -1379,7 +1445,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{formik.values.deliveries?.map((delivery, idx) => (
{type !== 'detail' && (
- |
+ |
{
disabled={
hasInvalidQty ||
hasExceededStock ||
- !formik.isValid ||
formik.isSubmitting ||
(formik.values.source_warehouse_id ===
formik.values.destination_warehouse_id &&
@@ -1760,17 +1825,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
-
- {movementFormErrorMessage && (
-
-
- {movementFormErrorMessage}
-
- )}
>
diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx
index 1c5322e1..51c20d8e 100644
--- a/src/components/pages/marketing/form/MarketingForm.tsx
+++ b/src/components/pages/marketing/form/MarketingForm.tsx
@@ -48,6 +48,8 @@ import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import RequirePermission from '@/components/helper/RequirePermission';
+import { getUniqueFormikErrors } from '@/lib/formik-helper';
+import AlertErrorList from '@/components/helper/form/FormErrors';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
@@ -217,6 +219,7 @@ const MarketingForm = ({
const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>(
'add'
);
+ const [formErrorList, setFormErrorList] = useState([]);
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
DeliveryOrderProductFormValues[]
>(
@@ -558,11 +561,28 @@ const MarketingForm = ({
);
}, [memoSalesOrder]);
+ const handleValidateForm = async () => {
+ const errors = await formik.validateForm();
+
+ if (Object.keys(errors).length > 0) {
+ // Parse and display errors
+ const errorMessages = getUniqueFormikErrors(errors);
+ setFormErrorList(errorMessages);
+ return; // Stop submission
+ }
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleValidateForm();
+ formik.handleSubmit();
+ };
+
return (
<>
diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts
index 37881636..9dcf713e 100644
--- a/src/components/pages/master-data/product/form/ProductForm.schema.ts
+++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts
@@ -29,36 +29,38 @@ export const ProductFormSchema: Yup.ObjectSchema =
sku: Yup.string().required('SKU wajib diisi!'),
uom: Yup.object({
- value: Yup.number().min(1).required(),
- label: Yup.string().required(),
- })
- .nullable()
- .required('Satuan wajib diisi!'),
+ value: Yup.number()
+ .min(1, 'Satuan wajib dipilih!')
+ .required('Satuan wajib dipilih!'),
+ label: Yup.string().required('Satuan wajib dipilih!'),
+ }).nullable(),
uom_id: Yup.number()
- .required('Satuan wajib diisi!')
- .typeError('Satuan wajib diisi!'),
+ .min(1, 'Satuan wajib dipilih!')
+ .required('Satuan wajib dipilih!')
+ .typeError('Satuan wajib dipilih!'),
product_category: Yup.object({
- value: Yup.number().min(1).required(),
- label: Yup.string().required(),
- })
- .nullable()
- .required('Kategori produk wajib diisi!'),
+ value: Yup.number()
+ .min(1, 'Kategori produk wajib dipilih!')
+ .required('Kategori produk wajib dipilih!'),
+ label: Yup.string().required('Kategori produk wajib dipilih!'),
+ }).nullable(),
product_category_id: Yup.number()
- .required('Kategori produk wajib diisi!')
- .typeError('Kategori produk wajib diisi!'),
+ .min(1, 'Kategori produk wajib dipilih!')
+ .required('Kategori produk wajib dipilih!')
+ .typeError('Kategori produk wajib dipilih!'),
product_price: Yup.number()
.required('Harga produk wajib diisi!')
.typeError('Harga produk wajib diisi!')
- .min(0, 'Harga produk tidak boleh kurang dari 0!'),
+ .min(1, 'Harga produk tidak boleh kurang dari 1!'),
selling_price: Yup.number()
.required('Harga jual wajib diisi!')
.typeError('Harga jual wajib diisi!')
- .min(0, 'Harga jual tidak boleh kurang dari 0!'),
+ .min(1, 'Harga jual tidak boleh kurang dari 1!'),
tax: Yup.number()
.required('Pajak wajib diisi!')
@@ -69,7 +71,7 @@ export const ProductFormSchema: Yup.ObjectSchema =
expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!')
.typeError('Periode kadaluarsa wajib diisi!')
- .min(0, 'Periode kadaluarsa tidak boleh kurang dari 0!'),
+ .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
supplier_ids: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!'))
diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx
index 5b304419..bf4cf1ee 100644
--- a/src/components/pages/master-data/product/form/ProductForm.tsx
+++ b/src/components/pages/master-data/product/form/ProductForm.tsx
@@ -17,6 +17,8 @@ import SelectInput, {
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
+import { getUniqueFormikErrors } from '@/lib/formik-helper';
+import AlertErrorList from '@/components/helper/form/FormErrors';
import {
ProductFormSchema,
@@ -48,6 +50,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
const deleteModal = useModal();
const [productFormErrorMessage, setProductFormErrorMessage] = useState('');
+ const [formErrorList, setFormErrorList] = useState([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createProductHandler = useCallback(
@@ -201,6 +204,22 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]);
+ const handleValidateForm = async () => {
+ const errors = await formik.validateForm();
+
+ if (Object.keys(errors).length > 0) {
+ const errorMessages = getUniqueFormikErrors(errors);
+ setFormErrorList(errorMessages);
+ return;
+ }
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleValidateForm();
+ formik.handleSubmit(e);
+ };
+
return (
<>
@@ -220,11 +239,30 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
{type !== 'add' && (
diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx
index 7d8a4c7c..b5b1dc4d 100644
--- a/src/components/pages/production/chickin/form/ChickinForm.tsx
+++ b/src/components/pages/production/chickin/form/ChickinForm.tsx
@@ -18,6 +18,7 @@ import { Icon } from '@iconify/react';
import Badge from '@/components/Badge';
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
import RequirePermission from '@/components/helper/RequirePermission';
+import { BaseApproval } from '@/types/api/api-general';
const ChickinFormKandang = ({
formType = 'add',
initialValues,
@@ -33,11 +34,16 @@ const ChickinFormKandang = ({
approvals,
isLoading: approvalsLoading,
refresh: refreshApprovals,
+ rawDataApprovals,
} = useApprovalSteps({
- latestApproval: initialValues?.approval,
+ latestApproval: initialValues?.chickin_approval,
approvalLines: CHICKINS_APPROVAL_LINE,
moduleName: 'CHICKINS',
moduleId: initialValues?.id.toString() ?? '',
+ params: {
+ limit: 'limit',
+ group_step_number: false,
+ },
});
const afterSubmitFormChickin = () => {
@@ -180,6 +186,7 @@ const ChickinFormKandang = ({
{openChickin && (
diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
index 17c76822..e800ee68 100644
--- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
+++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx
@@ -8,6 +8,7 @@ import PillBadge from '@/components/PillBadge';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin';
+import { BaseApproval } from '@/types/api/api-general';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react';
import { useState } from 'react';
@@ -16,9 +17,11 @@ import toast from 'react-hot-toast';
const ChickinLogsView = ({
initialValues,
afterSubmit,
+ rawDataApprovals,
}: {
initialValues: ProjectFlockKandang;
afterSubmit?: () => void;
+ rawDataApprovals: BaseApproval[];
}) => {
const confirmModal = useModal();
const [isApproveLoading, setIsApproveLoading] = useState(false);
@@ -60,8 +63,15 @@ const ChickinLogsView = ({
) : (
(initialValues?.chickins || []).map((chickin, index) => {
- const isApproved = chickin.usage_qty !== 0;
- const isPending = chickin.pending_usage_qty !== 0;
+ const latestApproval = rawDataApprovals[0];
+ const isApproved =
+ index == (initialValues?.chickins || []).length - 1
+ ? latestApproval?.step_number === 2
+ : true;
+ const isPending =
+ index == (initialValues?.chickins || []).length - 1
+ ? latestApproval?.step_number === 1
+ : false;
const quantity = isApproved
? chickin.usage_qty
: isPending
@@ -81,7 +91,7 @@ const ChickinLogsView = ({
{/* Header with Status Badge */}
- Chick In #{index + 1}
+ Chick In #{index + 1} - {latestApproval?.step_number}
-
-
- )}
+ {initialValues.chickin_approval &&
+ initialValues?.chickin_approval?.step_number < 2 && (
+
+
+
+ )}
{chickinErrorMessage && (
setChickinErrorMessage('')}>
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
index 5ed74b5a..dc972b14 100644
--- a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts
@@ -64,9 +64,9 @@ export const ProjectFlockBudgetsSchema: Yup.ObjectSchema =
diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
index 36ea90ca..7e90c94b 100644
--- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
+++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx
@@ -6,6 +6,8 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import { getUniqueFormikErrors } from '@/lib/formik-helper';
+import AlertErrorList from '@/components/helper/form/FormErrors';
import {
AreaApi,
FcrApi,
@@ -64,8 +66,10 @@ const ProjectFlockForm = ({
const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] =
useState('');
+ const [formErrorList, setFormErrorList] = useState([]);
const [selectedArea, setSelectedArea] = useState('');
const [selectedLocation, setSelectedLocation] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('');
const [disabledLocation, setDisabledLocation] = useState(
initialValues?.location?.id ? false : true
);
@@ -125,11 +129,15 @@ const ProjectFlockForm = ({
const {
options: optionsProductionStandards,
isLoadingOptions: isLoadingProductionStandards,
- } = useSelect(ProductionStandardApi.basePath, 'id', 'name');
+ } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
+ search: '',
+ project_category: selectedCategory,
+ });
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
search: '',
location_id: selectedLocation == '' ? '0' : selectedLocation,
+ limit: 'limit',
}).toString()}`;
const {
data: kandang,
@@ -237,9 +245,19 @@ const ProjectFlockForm = ({
};
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
- formik.setFieldValue('category', (val as OptionType)?.value);
+ // Reset production standard when category is changed
+ formik.setFieldValue('production_standard_id', '');
+ formik.setFieldValue('production_standard', '');
+
formik.setFieldValue('category_option', val);
- if (val == null) {
+ formik.setFieldValue('category', val ? (val as OptionType)?.value : '');
+
+ setSelectedCategory((val as OptionType)?.value as string);
+
+ if (Boolean(val)) {
+ formik.setFieldTouched('category', false);
+ formik.setFieldError('category', '');
+ } else {
formik.setFieldTouched('category', true);
}
};
@@ -378,8 +396,6 @@ const ProjectFlockForm = ({
validationSchema:
formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema,
validateOnBlur: true,
- // validateOnChange: true,
- // validateOnMount: true,
onSubmit: async (values) => {
setProjectFlockFormErrorMessage('');
const payload: CreateProjectFlockPayload = {
@@ -626,6 +642,17 @@ const ProjectFlockForm = ({
return !isNonstockAlreadyInBudgets;
});
+ const handleValidateForm = async () => {
+ const errors = await formik.validateForm();
+
+ if (Object.keys(errors).length > 0) {
+ // Parse and display errors
+ const errorMessages = getUniqueFormikErrors(errors);
+ setFormErrorList(errorMessages);
+ return; // Stop submission
+ }
+ };
+
return (
<>
@@ -685,7 +712,11 @@ const ProjectFlockForm = ({
+ {/* Error List Alert */}
+ {formErrorList.length > 0 && (
+ setFormErrorList([])}
+ />
+ )}
+
{formType !== 'detail' && (
diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx
index 3a6f8071..4a9d6c13 100644
--- a/src/components/pages/production/recording/form/RecordingForm.tsx
+++ b/src/components/pages/production/recording/form/RecordingForm.tsx
@@ -17,6 +17,7 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { useModal } from '@/components/Modal';
+import AlertErrorList from '@/components/helper/form/FormErrors';
import {
ProjectFlockKandangApi,
@@ -52,6 +53,7 @@ import {
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatDate, formatNumber } from '@/lib/helper';
+import { getUniqueFormikErrors } from '@/lib/formik-helper';
import toast from 'react-hot-toast';
import ApprovalSteps, {
useApprovalSteps,
@@ -60,7 +62,6 @@ import {
GROWING_RECORDING_APPROVAL_LINE,
LAYING_RECORDING_APPROVAL_LINE,
} from '@/config/approval-line';
-import Table from '@/components/Table';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -92,6 +93,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [, setApprovalNotes] = useState('');
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
+ const [formErrorList, setFormErrorList] = useState([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [, setNewRecordingData] = useState(null);
const [nextDayRecording, setNextDayRecording] =
@@ -758,6 +760,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
},
});
+ const handleValidateForm = async () => {
+ const errors = await formik.validateForm();
+
+ if (Object.keys(errors).length > 0) {
+ const errorMessages = getUniqueFormikErrors(errors);
+ setFormErrorList(errorMessages);
+ return;
+ }
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleValidateForm();
+ formik.handleSubmit(e);
+ };
+
// ===== HELPER FUNCTIONS =====
useCallback((): OptionType | null => {
if (
@@ -1323,9 +1341,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)}
- {recordingFormErrorMessage && (
-
-
- {recordingFormErrorMessage}
-
- )}
diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx
index 77d1608d..52e7a24b 100644
--- a/src/components/pages/production/uniformity/UniformityChart.tsx
+++ b/src/components/pages/production/uniformity/UniformityChart.tsx
@@ -1,93 +1,86 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useState } from 'react';
import Card from '@/components/Card';
import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart';
import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart';
import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton';
import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton';
-import {
- UniformityDetailItem,
- Uniformity,
-} from '@/types/api/production/uniformity';
+import { Uniformity, type ChartData } from '@/types/api/production/uniformity';
interface UniformityChartProps {
uniformityData?: Uniformity | null;
- uniformityDetails?: UniformityDetailItem[];
+ isFiltered?: boolean;
}
const UniformityChart = ({
uniformityData,
- uniformityDetails,
+ isFiltered = false,
}: UniformityChartProps) => {
- const defaultUniformityDetails: UniformityDetailItem[] = [
- { id: 1, weight: 61, range: 'Ideal' },
- { id: 2, weight: 62, range: 'Ideal' },
- { id: 3, weight: 63, range: 'Ideal' },
- { id: 4, weight: 64, range: 'Ideal' },
- { id: 5, weight: 65, range: 'Ideal' },
- { id: 6, weight: 66, range: 'Ideal' },
- { id: 7, weight: 67, range: 'Ideal' },
- ];
+ const [currentWeekIndex, setCurrentWeekIndex] = useState(0);
- const detailsToUse = uniformityDetails || defaultUniformityDetails;
+ const chartData = useMemo((): ChartData | undefined => {
+ if (!uniformityData?.chart_data) return undefined;
+ return uniformityData.chart_data;
+ }, [uniformityData]);
const barChartData = useMemo(() => {
- if (!uniformityData) {
+ if (!chartData?.bar_chart) {
return [];
}
- if (!detailsToUse || detailsToUse.length === 0) {
+ const { bar_chart } = chartData;
+ const currentWeekStr = String(bar_chart.current_week);
+ const weekData = bar_chart.all_weeks[currentWeekStr];
+
+ if (!weekData || !weekData.has_data) {
return [];
}
- const weights = detailsToUse.map((d) => d.weight);
- const minWeight = Math.floor(Math.min(...weights) / 5) * 5;
- const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5;
-
- const rangeSize = maxWeight - minWeight < 11 ? 4 : 5;
- const ranges: string[] = [];
-
- for (let start = minWeight; start <= maxWeight; start += rangeSize) {
- const end = start + rangeSize;
- ranges.push(`${start}-${end}`);
- }
-
- const totalIdealCount = detailsToUse.filter(
- (d) => d.range === 'Ideal'
- ).length;
-
- return ranges.map((range) => {
- const [minStr, maxStr] = range.split('-').map(Number);
- const min = minStr;
- const max = maxStr;
-
- const birdsInRange = detailsToUse.filter(
- (d) => d.weight >= min && d.weight < max
- ).length;
-
- const hasIdeal = detailsToUse.some(
- (d) => d.range === 'Ideal' && d.weight >= min && d.weight < max
- );
-
- return {
- name: range,
- uv: birdsInRange,
- isIdeal: hasIdeal,
- idealCount: hasIdeal ? totalIdealCount : undefined,
- };
- });
- }, [uniformityData, detailsToUse]);
+ return weekData.weight_distribution.map((range) => ({
+ name: range.range,
+ uv: range.bird_count,
+ isIdeal: range.is_ideal_range,
+ idealCount: range.is_ideal_range
+ ? weekData.ideal_range.total_ideal_birds
+ : undefined,
+ }));
+ }, [chartData]);
const gaugeChartData = useMemo(() => {
- if (!uniformityData) return undefined;
+ if (!chartData?.gauge_chart || !uniformityData) return undefined;
+
+ const { gauge_chart } = chartData;
+ const currentWeekData = gauge_chart.available_weeks[currentWeekIndex];
+
+ if (!currentWeekData || !currentWeekData.has_data) {
+ return undefined;
+ }
return {
- value: uniformityData.uniformity,
+ value: currentWeekData.uniformity_percentage,
label: 'Uniformity',
- week: `Week ${uniformityData.week}`,
- currentValue: uniformityData.uniform_qty,
- totalValue: uniformityData.chick_qty_of_weight,
+ week: `Week ${currentWeekData.week}`,
+ currentValue: currentWeekData.ideal_count,
+ totalValue: currentWeekData.total_count,
+ hasPrevWeek: gauge_chart.week_info.has_prev_week,
+ hasNextWeek: gauge_chart.week_info.has_next_week,
};
- }, [uniformityData]);
+ }, [chartData, currentWeekIndex, uniformityData]);
+
+ const handleWeekChange = (direction: 'prev' | 'next') => {
+ if (!chartData?.gauge_chart) return;
+
+ const { available_weeks, week_info } = chartData.gauge_chart;
+
+ if (direction === 'prev' && week_info.has_prev_week) {
+ setCurrentWeekIndex((prev) => Math.max(0, prev - 1));
+ } else if (direction === 'next' && week_info.has_next_week) {
+ setCurrentWeekIndex((prev) =>
+ Math.min(available_weeks.length - 1, prev + 1)
+ );
+ }
+ };
+
+ const shouldShowEmptyState = !isFiltered;
return (
@@ -100,14 +93,16 @@ const UniformityChart = ({
}}
>
- {!uniformityData || barChartData.length === 0 ? (
+ {shouldShowEmptyState ||
+ !uniformityData ||
+ barChartData.length === 0 ? (
) : (
)}
- {!uniformityData || !gaugeChartData ? (
+ {shouldShowEmptyState || !uniformityData || !gaugeChartData ? (
)}
diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx
index 0c0c3f70..9caa98a9 100644
--- a/src/components/pages/production/uniformity/UniformityTable.tsx
+++ b/src/components/pages/production/uniformity/UniformityTable.tsx
@@ -151,8 +151,10 @@ const UniformityConfirmationPreview = ({
const UniformityChartWrapper = ({
uniformitySwrKey,
+ isFiltered,
}: {
uniformitySwrKey: string;
+ isFiltered: boolean;
}) => {
const { data: uniformities } = useSWR(
uniformitySwrKey,
@@ -166,31 +168,8 @@ const UniformityChartWrapper = ({
return null;
}, [uniformities]);
- const shouldFetchDetails = !!uniformityData;
- const uniformityDetailSwrKey = useMemo(() => {
- if (!uniformityData) return null;
- return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`;
- }, [uniformityData]);
-
- const { data: uniformityDetailResponse } = useSWR(
- uniformityDetailSwrKey,
- shouldFetchDetails ? UniformityApi.getAllFetcher : null
- );
-
- const uniformityDetails = useMemo(() => {
- if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) {
- const detailData =
- uniformityDetailResponse.data as unknown as UniformityDetail;
- return detailData.uniformity_details;
- }
- return undefined;
- }, [shouldFetchDetails, uniformityDetailResponse]);
-
return (
-
+
);
};
@@ -374,6 +353,7 @@ const UniformityTable = () => {
if (filterEndDate) {
queryParams.append('end_date', filterEndDate);
}
+ queryParams.append('with_chart', 'true');
}
const tableQueryString = getTableFilterQueryString();
@@ -433,6 +413,7 @@ const UniformityTable = () => {
);
const handleResetFilters = useCallback(() => {
+ setIsSubmitted(false);
setFilterLocation(null);
setFilterProjectFlock(null);
setFilterKandang(null);
@@ -896,7 +877,10 @@ const UniformityTable = () => {
= ({
const setExpandedDrawerContent = useUiStore(
(s) => s.setExpandedDrawerContent
);
+ const [shouldFetchDetails, setShouldFetchDetails] = useState(false);
+ const [hasFetchedDetails, setHasFetchedDetails] = useState(false);
+
+ const { data: uniformityDetailResponse, isLoading } = useSWR(
+ shouldFetchDetails
+ ? `uniformity-detail-${initialValues.id}-with-details`
+ : null,
+ () => UniformityApi.getUniformityDetail(initialValues.id, true)
+ );
+
+ const uniformity_details = useMemo(() => {
+ if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) {
+ return uniformityDetailResponse.data.uniformity_details;
+ }
+ return initialValues.uniformity_details;
+ }, [shouldFetchDetails, uniformityDetailResponse, initialValues]);
const handleApprove = () => {
router.push(`/production/uniformity?action=approve&id=${initialValues.id}`);
@@ -43,12 +62,15 @@ const UniformityDetail: React.FC = ({
};
const handleViewUniformityDetails = () => {
+ if (!uniformity_details || uniformity_details.length === 0) {
+ setShouldFetchDetails(true);
+ return;
+ }
+
setExpandedDrawerContent(
);
@@ -58,6 +80,28 @@ const UniformityDetail: React.FC = ({
}, 0);
};
+ useEffect(() => {
+ if (
+ shouldFetchDetails &&
+ uniformity_details &&
+ uniformity_details.length > 0 &&
+ !hasFetchedDetails
+ ) {
+ setExpandedDrawerContent(
+
+ );
+
+ setHasFetchedDetails(true);
+ setTimeout(() => {
+ setExpandedDrawerOpen(true);
+ }, 0);
+ }
+ }, [shouldFetchDetails, uniformity_details, hasFetchedDetails]);
+
useEffect(() => {
return () => {
setExpandedDrawerOpen(false);
@@ -154,12 +198,22 @@ const UniformityDetail: React.FC = ({
return (
{valueMap[id]}
-
+
@@ -173,6 +227,92 @@ const UniformityDetail: React.FC = ({
[initialValues]
);
+ const samplingTableData: DetailOptionType[] = useMemo(() => {
+ if (!initialValues.sampling) return [];
+
+ return [
+ {
+ id: 'sampling-size',
+ label: 'Sampling size',
+ value: `${formatNumber(initialValues.sampling.chick_qty_of_weight)} of Birds`,
+ },
+ {
+ id: 'mean-weight',
+ label: 'Mean Weight',
+ value: `${initialValues.sampling.mean_weight} g`,
+ },
+ {
+ id: 'min-limit',
+ label: 'Min Limit (-10%)',
+ value: `${initialValues.sampling.mean_down} g`,
+ },
+ {
+ id: 'max-limit',
+ label: 'Max Limit (+10%)',
+ value: `${initialValues.sampling.mean_up} g`,
+ },
+ ];
+ }, [initialValues.sampling]);
+
+ const columnsSampling: ColumnDef[] = useMemo(
+ () => [
+ {
+ accessorKey: 'label',
+ header: 'Label',
+ cell: (props) => props.row.original.label,
+ },
+ {
+ accessorKey: 'value',
+ header: 'Value',
+ cell: (props) => {props.row.original.value},
+ },
+ ],
+ []
+ );
+
+ const resultTableData: DetailOptionType[] = useMemo(() => {
+ if (!initialValues.result) return [];
+
+ return [
+ {
+ id: 'ideal-birds',
+ label: 'Ideal Birds',
+ value: `${formatNumber(initialValues.result.uniform_qty)} of Birds`,
+ },
+ {
+ id: 'outside-range',
+ label: 'Outside Range',
+ value: `${formatNumber(initialValues.result.outside_qty)} of Birds`,
+ },
+ {
+ id: 'uniformity',
+ label: 'Uniformity',
+ value: `${initialValues.result.uniformity} %`,
+ },
+ {
+ id: 'cv',
+ label: 'CV',
+ value: `${initialValues.result.cv} %`,
+ },
+ ];
+ }, [initialValues.result]);
+
+ const resultColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ accessorKey: 'label',
+ header: 'Label',
+ cell: (props) => props.row.original.label,
+ },
+ {
+ accessorKey: 'value',
+ header: 'Value',
+ cell: (props) => {props.row.original.value},
+ },
+ ],
+ []
+ );
+
return (
{/* Header */}
@@ -185,7 +325,7 @@ const UniformityDetail: React.FC = ({
{/* Form Section */}
-
+
{initialValues ? (
{/* Info Umum */}
@@ -200,23 +340,55 @@ const UniformityDetail: React.FC = ({
paginationClassName: 'hidden',
}}
/>
-
- {/* Approve/Reject Buttons */}
- {initialValues.result &&
- initialValues.latest_approval?.step_name === 'CREATED' ? (
- <>
-
-
-
-
-
-
-
- >
- ) : null}
+
+ {/* Sampling and Range */}
+ {initialValues.sampling && (
+
+ Sampling and Range
+
+ data={samplingTableData}
+ columns={columnsSampling}
+ pageSize={4}
+ className={{
+ containerClassName: 'mb-0',
+ paginationClassName: 'hidden',
+ }}
+ />
+
+ )}
+
+ {/* Result */}
+ {initialValues.result && (
+
+ Result
+
+ data={resultTableData}
+ columns={resultColumns}
+ pageSize={4}
+ className={{
+ containerClassName: 'mb-0',
+ paginationClassName: 'hidden',
+ }}
+ />
+
+ )}
+
+ {/* Approve/Reject Buttons */}
+ {initialValues.result &&
+ initialValues.latest_approval?.step_name === 'CREATED' ? (
+ <>
+
+
+
+
+
+
+
+ >
+ ) : null}
) : (
diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx
index 21be03d7..05d21535 100644
--- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx
+++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx
@@ -1,152 +1,39 @@
'use client';
-import React, { useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import { Icon } from '@iconify/react';
import { ColumnDef } from '@tanstack/react-table';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { useUiStore } from '@/stores/ui/ui.store';
import {
UniformityDetailItem,
- UniformitySampling,
- UniformityResult,
UniformityInfoUmum,
} from '@/types/api/production/uniformity';
import Table from '@/components/Table';
import Badge from '@/components/Badge';
-import { formatNumber } from '@/lib/helper';
-import { DetailOptionType } from '@/types/api/production/uniformity';
import {
getWeightStatusColor,
getWeightStatusIndicatorColor,
getWeightStatusText,
} from '@/components/pages/production/uniformity/uniformity-utils';
import { BodyWeightData } from '@/types/api/production/uniformity';
-import Button from '@/components/Button';
-import { UniformityApi } from '@/services/api/uniformity';
-import useSWR from 'swr';
-import { isResponseSuccess } from '@/lib/api-helper';
interface UniformityDetailsPreviewProps {
info_umum: UniformityInfoUmum;
- sampling: UniformitySampling;
- result: UniformityResult;
uniformity_details?: UniformityDetailItem[];
uniformityId: number;
}
const UniformityDetailsPreview = ({
info_umum,
- uniformity_details: initialUniformityDetails,
- sampling,
- result,
- uniformityId,
+ uniformity_details,
}: UniformityDetailsPreviewProps) => {
const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen);
- const [shouldFetchDetails, setShouldFetchDetails] = useState(false);
-
- const { data: uniformityDetailResponse, isLoading } = useSWR(
- shouldFetchDetails
- ? `uniformity-detail-${uniformityId}-with-details`
- : null,
- () => UniformityApi.getUniformityDetail(uniformityId, true)
- );
-
- const uniformity_details = useMemo(() => {
- if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) {
- return uniformityDetailResponse.data.uniformity_details;
- }
- return initialUniformityDetails;
- }, [shouldFetchDetails, uniformityDetailResponse, initialUniformityDetails]);
const handleClose = () => {
setExpandedDrawerOpen(false);
};
- const fetchWeightData = () => {
- setShouldFetchDetails(true);
- };
-
- const samplingTableData: DetailOptionType[] = useMemo(() => {
- if (!sampling) return [];
-
- return [
- {
- id: 'sampling-size',
- label: 'Sampling size',
- value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`,
- },
- {
- id: 'mean-weight',
- label: 'Mean Weight',
- value: `${sampling.mean_weight} g`,
- },
- {
- id: 'min-limit',
- label: 'Min Limit (-10%)',
- value: `${sampling.mean_down} g`,
- },
- {
- id: 'max-limit',
- label: 'Max Limit (+10%)',
- value: `${sampling.mean_up} g`,
- },
- ];
- }, [sampling]);
-
- const columnsSampling: ColumnDef [] = useMemo(
- () => [
- {
- accessorKey: 'label',
- header: 'Label',
- cell: (props) => props.row.original.label,
- },
- {
- accessorKey: 'value',
- header: 'Value',
- cell: (props) => {props.row.original.value},
- },
- ],
- []
- );
-
- const resultTableData: DetailOptionType[] = useMemo(() => {
- if (!result) return [];
-
- return [
- {
- id: 'ideal-birds',
- label: 'Ideal Birds',
- value: `${formatNumber(result.uniform_qty)} of Birds`,
- },
- {
- id: 'outside-range',
- label: 'Outside Range',
- value: `${formatNumber(result.outside_qty)} of Birds`,
- },
- {
- id: 'uniformity',
- label: 'Uniformity',
- value: `${result.uniformity} %`,
- },
- ];
- }, [result]);
-
- const resultColumns: ColumnDef[] = useMemo(
- () => [
- {
- accessorKey: 'label',
- header: 'Label',
- cell: (props) => props.row.original.label,
- },
- {
- accessorKey: 'value',
- header: 'Value',
- cell: (props) => {props.row.original.value},
- },
- ],
- []
- );
-
const tableData = useMemo(() => {
if (!uniformity_details) return [];
@@ -229,55 +116,10 @@ const UniformityDetailsPreview = ({
{/* Form Section */}
- {info_umum || sampling || result ? (
+ {info_umum ? (
- {/* Sampling and Range */}
- {sampling && (
-
- Sampling and Range
-
- data={samplingTableData}
- columns={columnsSampling}
- pageSize={4}
- className={{
- containerClassName: 'mb-0',
- paginationClassName: 'hidden',
- }}
- />
-
- )}
-
- {/* Result */}
- {result && (
-
- Result
-
- data={resultTableData}
- columns={resultColumns}
- pageSize={4}
- className={{
- containerClassName: 'mb-0',
- paginationClassName: 'hidden',
- }}
- />
-
- )}
-
- {!uniformity_details || uniformity_details.length === 0 ? (
-
-
-
- ) : null}
-
{/* Body Weight Details */}
- {uniformity_details && uniformity_details.length > 0 && (
+ {uniformity_details && uniformity_details.length > 0 ? (
data={tableData}
@@ -286,6 +128,17 @@ const UniformityDetailsPreview = ({
className={{ containerClassName: 'mb-5' }}
/>
+ ) : (
+
+
+ No data available
+ Body weight details not found
+
)}
) : (
diff --git a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts
index 273e5326..037180f0 100644
--- a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts
+++ b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts
@@ -24,9 +24,9 @@ type UniformityFormSchemaType = {
};
const FileSchema = Yup.mixed()
- .test('documentSize', 'Ukuran file maksimal 2 MB', (value): boolean => {
+ .test('documentSize', 'Ukuran file maksimal 5 MB', (value): boolean => {
if (!value) return true;
- if (value instanceof File) return value.size <= 2 * 1024 * 1024;
+ if (value instanceof File) return value.size <= 5 * 1024 * 1024;
return false;
})
.test('documentType', 'Format file harus Excel', (value): boolean => {
diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx
index bbca72f8..54b4ee2b 100644
--- a/src/components/pages/production/uniformity/form/UniformityForm.tsx
+++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx
@@ -43,7 +43,9 @@ import UniformityResultForm from '@/components/pages/production/uniformity/form/
import { generateUniformityTemplate } from '@/components/pages/production/uniformity/export/UniformityTemplate';
import useSWR from 'swr';
import { cn, formatNumber } from '@/lib/helper';
+import { getUniqueFormikErrors } from '@/lib/formik-helper';
import Tooltip from '@/components/Tooltip';
+import AlertErrorList from '@/components/helper/form/FormErrors';
interface UniformityFormProps {
formType?: 'add' | 'edit';
@@ -77,6 +79,7 @@ const UniformityForm = ({
const [uniformityFormErrorMessage, setUniformityFormErrorMessage] =
useState('');
+ const [formErrorList, setFormErrorList] = useState([]);
const fileInputRef = useRef(null);
@@ -282,6 +285,22 @@ const UniformityForm = ({
},
});
+ const handleValidateForm = async () => {
+ const errors = await formik.validateForm();
+
+ if (Object.keys(errors).length > 0) {
+ const errorMessages = getUniqueFormikErrors(errors);
+ setFormErrorList(errorMessages);
+ return;
+ }
+ };
+
+ const handleFormSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ handleValidateForm();
+ formik.handleSubmit(e);
+ };
+
// ===== FORM HANDLERS =====
const handleLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
@@ -339,8 +358,8 @@ const UniformityForm = ({
return;
}
- if (document.size > 2 * 1024 * 1024) {
- toast.error(`Ukuran file ${document.name} maksimal 2 MB!`);
+ if (document.size > 5 * 1024 * 1024) {
+ toast.error(`Ukuran file ${document.name} maksimal 5 MB!`);
return;
}
@@ -454,7 +473,7 @@ const UniformityForm = ({
Informasi Umum
- | | |