Merge branch 'development' into fix/transfer-to-laying

This commit is contained in:
ValdiANS
2026-01-24 11:22:48 +07:00
20 changed files with 753 additions and 336 deletions
@@ -71,7 +71,6 @@ 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,
@@ -423,7 +422,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
hasMore: hasMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
@@ -432,7 +430,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
rawData: projectFlocksRawData,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
hasMore: hasMoreProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: selectedProjectFlockLocationId,
});
@@ -531,7 +528,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
rawData: stockProducts,
isLoadingOptions: isLoadingStockProducts,
loadMore: loadMoreStockProducts,
hasMore: hasMoreStockProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', {
flags: 'PAKAN,OVK',
location_id: stockProductsLocationId,
@@ -539,11 +535,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
});
const {
options: depletionProductOptions,
rawData: depletionProductsData,
isLoadingOptions: isLoadingDepletionProducts,
loadMore: loadMoreDepletionProducts,
hasMore: hasMoreDepletionProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', {
location_id: depletionProductsLocationId,
kandang_id: depletionProductsKandangId,
@@ -584,11 +578,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}, [nextDayRecordingData]);
const {
options: eggProductOptions,
rawData: eggProductsData,
isLoadingOptions: isLoadingEggProducts,
loadMore: loadMoreEggProducts,
hasMore: hasMoreEggProducts,
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
search: 'telur',
location_id: eggProductsLocationId,
@@ -51,6 +51,13 @@ import { generateUniformityExcel } from '@/components/pages/production/uniformit
import Dropdown from '@/components/Dropdown';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import { useFormik } from 'formik';
import {
UniformityTableFilterSchema,
type UniformityTableFilterValues,
} from '@/components/pages/production/uniformity/UniformityTableFilter.schema';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
const UniformityConfirmationPreview = ({
uniformity,
@@ -241,7 +248,6 @@ const UniformityTable = () => {
options: filterLocationOptions,
isLoadingOptions: isLoadingFilterLocations,
loadMore: loadMoreFilterLocations,
hasMore: hasMoreFilterLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
@@ -251,7 +257,6 @@ const UniformityTable = () => {
rawData: filterProjectFlocksRawData,
isLoadingOptions: isLoadingFilterProjectFlocks,
loadMore: loadMoreFilterProjectFlocks,
hasMore: hasMoreFilterProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: filterProjectFlockLocationId,
});
@@ -316,6 +321,34 @@ const UniformityTable = () => {
}
}, [projectFlockKandangLookup]);
// ===== FORMIK FILTER =====
const filterFormik = useFormik<UniformityTableFilterValues>({
initialValues: {
start_date: filterStartDate,
end_date: filterEndDate,
location: filterLocation,
project_flock: filterProjectFlock,
project_flock_kandang_id: filterProjectFlockKandangId,
kandang: filterKandang,
},
validationSchema: UniformityTableFilterSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setFilterStartDate(values.start_date);
setFilterEndDate(values.end_date);
setFilterLocation(values.location ?? null);
setFilterProjectFlock(values.project_flock ?? null);
setFilterKandang(values.kandang ?? null);
setIsSubmitted(true);
filterModal.closeModal();
},
});
// ===== FORMIK ERROR LIST =====
const { formErrorList, close, handleFormSubmit } =
useFormikErrorList(filterFormik);
// ===== BUILD SWR KEY WITH FILTERS =====
const uniformitySwrKey = useMemo(() => {
const basePath = UniformityApi.basePath;
@@ -372,29 +405,54 @@ const UniformityTable = () => {
const handleFilterLocationChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const location = val as OptionType | null;
const locationId = Number(location?.value) || 0;
filterFormik.setFieldValue('location', location);
filterFormik.setFieldValue('location_id', locationId);
setFilterLocation(location);
setFilterProjectFlock(null);
setFilterKandang(null);
setFilterProjectFlockLocationId(
location ? location.value.toString() : ''
);
filterFormik.setFieldValue('project_flock', null);
filterFormik.setFieldValue('project_flock_id', 0);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterProjectFlockChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterProjectFlock(val as OptionType | null);
const projectFlock = val as OptionType | null;
const projectFlockId = Number(projectFlock?.value) || 0;
filterFormik.setFieldValue('project_flock', projectFlock);
filterFormik.setFieldValue('project_flock_id', projectFlockId);
setFilterProjectFlock(projectFlock);
setFilterKandang(null);
filterFormik.setFieldValue('kandang', null);
filterFormik.setFieldValue('kandang_id', 0);
},
[]
[filterFormik]
);
const handleFilterKandangChange = useCallback(
(val: OptionType | OptionType[] | null) => {
setFilterKandang(val as OptionType | null);
const kandang = val as OptionType | null;
const kandangId = Number(kandang?.value) || 0;
filterFormik.setFieldValue('kandang', kandang);
filterFormik.setFieldValue('kandang_id', kandangId);
setFilterKandang(kandang);
},
[]
[filterFormik]
);
const handleResetFilters = useCallback(() => {
@@ -405,41 +463,34 @@ const UniformityTable = () => {
setFilterProjectFlockKandangId(undefined);
setFilterStartDate('');
setFilterEndDate('');
}, []);
setFilterErrors({});
filterFormik.resetForm();
}, [filterFormik]);
const handleFilterStartDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterStartDate(value);
filterFormik.setFieldValue('start_date', value);
},
[filterFormik]
);
const handleFilterEndDateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setFilterEndDate(value);
filterFormik.setFieldValue('end_date', value);
},
[filterFormik]
);
const handleApplyFilters = useCallback(() => {
const errors: Record<string, string> = {};
if (!filterStartDate) {
errors.start_date = 'Tanggal mulai wajib diisi';
}
if (!filterEndDate) {
errors.end_date = 'Tanggal akhir wajib diisi';
}
if (!filterLocation) {
errors.location = 'Lokasi wajib dipilih';
}
if (!filterProjectFlock) {
errors.project_flock = 'Project Flock wajib dipilih';
}
if (!filterKandang) {
errors.kandang = 'Kandang wajib dipilih';
}
setFilterErrors(errors);
if (Object.keys(errors).length === 0) {
setIsSubmitted(true);
filterModal.closeModal();
}
}, [
filterModal,
filterStartDate,
filterEndDate,
filterLocation,
filterProjectFlock,
filterKandang,
]);
handleFormSubmit(
new Event('submit') as unknown as React.FormEvent<HTMLFormElement>
);
}, [handleFormSubmit]);
const selectedRowIds = useMemo(() => {
return Object.keys(rowSelection)
@@ -1136,108 +1187,117 @@ const UniformityTable = () => {
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
{/* Error List Alert */}
{formErrorList.length > 0 && (
<div className='w-full px-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
)}
<div className='space-y-4 px-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 sm:gap-4'>
<div>
<DateInput
label='Tanggal'
required
label='Tanggal mulai'
name='start_date'
value={filterStartDate}
onChange={(e) => {
setFilterStartDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}}
value={filterFormik.values.start_date}
onChange={handleFilterStartDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.start_date &&
Boolean(filterFormik.errors.start_date)
}
errorMessage={filterFormik.errors.start_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div>
<div>
<DateInput
label=' '
required
label='Tanggal akhir'
name='end_date'
value={filterEndDate}
onChange={(e) => {
setFilterEndDate(e.target.value);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}}
value={filterFormik.values.end_date}
onChange={handleFilterEndDateChange}
onBlur={filterFormik.handleBlur}
isError={
filterFormik.touched.end_date &&
Boolean(filterFormik.errors.end_date)
}
errorMessage={filterFormik.errors.end_date}
className={{ wrapper: 'w-full' }}
/>
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div>
</div>
<div>
<SelectInput
required
label='Lokasi'
placeholder='Pilih Lokasi...'
value={filterLocation}
value={filterFormik.values.location}
onChange={(value) => {
handleFilterLocationChange(value);
setFilterErrors((prev) => ({ ...prev, location: '' }));
}}
options={filterLocationOptions}
onInputChange={setFilterLocationInputValue}
isLoading={isLoadingFilterLocations}
onMenuScrollToBottom={loadMoreFilterLocations}
isError={
filterFormik.touched.location &&
Boolean(filterFormik.errors.location)
}
errorMessage={filterFormik.errors.location}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.location && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.location}
</p>
)}
</div>
<div>
<SelectInput
required
label='Project Flock'
placeholder='Pilih Project Flock...'
value={filterProjectFlock}
value={filterFormik.values.project_flock}
onChange={(value) => {
handleFilterProjectFlockChange(value);
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
}}
options={filterProjectFlockOptions}
onInputChange={setFilterProjectFlockSearchValue}
isLoading={isLoadingFilterProjectFlocks}
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
isDisabled={!filterLocation}
isDisabled={!filterFormik.values.location}
isError={
filterFormik.touched.project_flock &&
Boolean(filterFormik.errors.project_flock)
}
errorMessage={filterFormik.errors.project_flock}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.project_flock && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.project_flock}
</p>
)}
</div>
<div>
<SelectInput
required
label='Kandang'
placeholder='Pilih Kandang...'
value={filterKandang}
value={filterFormik.values.kandang}
onChange={(value) => {
handleFilterKandangChange(value);
setFilterErrors((prev) => ({ ...prev, kandang: '' }));
}}
options={filterKandangOptions}
isDisabled={!filterProjectFlock}
isDisabled={!filterFormik.values.project_flock}
isError={
filterFormik.touched.kandang &&
Boolean(filterFormik.errors.kandang)
}
errorMessage={filterFormik.errors.kandang}
isClearable
className={{ wrapper: 'w-full' }}
/>
{filterErrors.kandang && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.kandang}
</p>
)}
</div>
</div>
@@ -0,0 +1,59 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type UniformityTableFilterType = {
start_date: string;
end_date: string;
location: OptionType | null;
project_flock: OptionType | null;
project_flock_kandang_id: number | undefined;
kandang: OptionType | null;
};
export const UniformityTableFilterSchema = yup.object({
start_date: yup.string().required('Tanggal mulai wajib diisi'),
end_date: yup
.string()
.required('Tanggal akhir wajib diisi')
.test(
'is-greater-than-start',
'Tanggal akhir tidak boleh masa lampau',
function (value) {
const { start_date } = this.parent;
if (!start_date || !value) return true;
return new Date(value) >= new Date(start_date);
}
),
location: yup
.mixed<OptionType>()
.required('Lokasi wajib dipilih')
.test('is-not-empty', 'Lokasi wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
project_flock: yup
.mixed<OptionType>()
.required('Project Flock wajib dipilih')
.test('is-not-empty', 'Project Flock wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
project_flock_kandang_id: yup.number().optional(),
kandang: yup
.mixed<OptionType>()
.required('Kandang wajib dipilih')
.test('is-not-empty', 'Kandang wajib dipilih', (value) => {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}),
}) as yup.ObjectSchema<UniformityTableFilterType>;
export type UniformityTableFilterValues = yup.InferType<
typeof UniformityTableFilterSchema
>;
@@ -49,7 +49,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
<div className='flex flex-col gap-2 mt-2'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
<div className='w-5 h-5 bg-primary rounded-md'></div>
<span className='text-sm'>Ideal</span>
</div>
<span className='text-sm font-medium'>
@@ -84,7 +84,7 @@ function CustomTooltip({ payload, label, active }: CustomTooltipProps) {
<p className='m-0 font-bold text-white/50'>Uniformity 2025</p>
<div className='flex items-center gap-2 mt-2 justify-between'>
<div className='flex items-center gap-2'>
<div className='w-5 h-5 bg-[#0069E0] rounded-md'></div>
<div className='w-5 h-5 bg-primary rounded-md'></div>
<span className='text-sm'>Ideal</span>
</div>
<span className='text-sm font-medium'>{chartData.idealRange}</span>
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts';
import Card from '@/components/Card';
import { formatNumber } from '@/lib/helper';
@@ -5,7 +5,6 @@ import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { Icon } from '@iconify/react';
import { toast } from 'react-hot-toast';
import moment from 'moment';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { useUiStore } from '@/stores/ui/ui.store';
import { useUniformityStore } from '@/stores/uniformity/uniformity.store';
@@ -28,6 +27,7 @@ import { LocationApi } from '@/services/api/master-data';
import {
ProjectFlockApi,
ProjectFlockKandangApi,
RecordingApi,
} from '@/services/api/production';
import { UniformityApi } from '@/services/api/uniformity';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
@@ -40,6 +40,7 @@ import {
ProjectFlockKandangLookup,
ProjectFlock,
} from '@/types/api/production/project-flock';
import { Recording } from '@/types/api/production/recording';
import { Kandang } from '@/types/api/master-data/kandang';
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
@@ -87,9 +88,7 @@ const UniformityForm = ({
const fileInputRef = useRef<HTMLInputElement>(null);
// ===== SELECT INPUT DATA =====
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
null
);
const [, setSelectedLocation] = useState<OptionType | null>(null);
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
useState<string>('');
@@ -106,7 +105,6 @@ const UniformityForm = ({
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
hasMore: hasMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const {
@@ -115,7 +113,6 @@ const UniformityForm = ({
rawData: projectFlocksRawData,
isLoadingOptions: isLoadingProjectFlocks,
loadMore: loadMoreProjectFlocks,
hasMore: hasMoreProjectFlocks,
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
location_id: selectedProjectFlockLocationId,
});
@@ -204,6 +201,20 @@ const UniformityForm = ({
? projectFlockKandangLookupData.data
: undefined;
// ===== RECORDINGS DATA (FOR WEEK CALCULATION) =====
const recordingsUrl = useMemo(() => {
const params = new URLSearchParams({
page: '1',
limit: '100',
});
return `${RecordingApi.basePath}?${params.toString()}`;
}, []);
const { data: recordingsData } = useSWR(
recordingsUrl,
RecordingApi.getAllFetcher
);
// ===== FORM CONFIGURATION =====
const formikInitialValues = useMemo<UniformityFormValues>(
() => getUniformityFormInitialValues(initialValues),
@@ -387,14 +398,24 @@ const UniformityForm = ({
// ===== SIDE EFFECTS =====
useEffect(() => {
if (formik.values.date) {
const date = moment(formik.values.date);
const weekNumber = date.week() - moment(date).startOf('month').week() + 1;
const adjustedWeekNumber = weekNumber <= 0 ? weekNumber + 52 : weekNumber;
if (
projectFlockKandangLookup?.project_flock_kandang_id &&
isResponseSuccess(recordingsData) &&
recordingsData.data
) {
const matchingRecording = recordingsData.data.find(
(recording: Recording) =>
recording.project_flock?.project_flock_kandang_id ===
projectFlockKandangLookup.project_flock_kandang_id
);
formik.setFieldValue('week', adjustedWeekNumber);
if (matchingRecording?.project_flock?.production_standart?.week) {
const weekValue =
matchingRecording.project_flock.production_standart.week;
formik.setFieldValue('week', weekValue);
}
}
}, [formik.values.date]);
}, [projectFlockKandangLookup?.project_flock_kandang_id, recordingsData]);
useEffect(() => {
const unsub = subscribeValidate(() => {
@@ -598,7 +619,7 @@ const UniformityForm = ({
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button
type='button'
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
className='rounded-2xl border border-sky-500 bg-primary text-white'
onClick={(e) => {
e.stopPropagation();
document.getElementById('file-upload-input')?.click();
@@ -622,7 +643,7 @@ const UniformityForm = ({
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button
type='button'
className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'
className='rounded-2xl border border-sky-500 bg-primary text-white'
onClick={(e) => {
e.stopPropagation();
document
@@ -68,7 +68,7 @@ const EmptyState = () => {
<>
<div className='absolute inset-0 flex flex-col items-center justify-center z-10 gap-2'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center my-2'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon icon={'heroicons:funnel'} className='text-4xl text-whitd' />
</Button>
</div>
@@ -29,7 +29,7 @@ const UniformityGaugeChartSkeleton: React.FC<
return (
<div className='flex flex-col w-full items-center'>
<div className='h-64 w-full relative flex justify-center min-h-[256px]'>
<div className='h-64 w-full relative flex justify-center min-h-64'>
<div className='relative w-full h-full flex flex-col items-center justify-end min-w-0'>
<ResponsiveContainer width='100%' height={256}>
<PieChart>
@@ -57,7 +57,7 @@ const UniformityGaugeChartSkeleton: React.FC<
</ResponsiveContainer>
<div className='absolute inset-x-0 top-24 flex flex-col items-center justify-center'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center mt-5'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon
icon={'heroicons:funnel'}
className='text-4xl text-whitd'
@@ -5,7 +5,7 @@ const UniformityTableSkeleton = () => {
return (
<div className='flex flex-col items-center justify-center gap-2 my-20'>
<div className='border border-[#18181B]/25 rounded-2xl p-1 flex items-center justify-center'>
<Button className='rounded-2xl border border-sky-500 bg-[#0069E0] text-white'>
<Button className='rounded-2xl border border-sky-500 bg-primary text-white'>
<Icon
icon={'heroicons-outline:chart-bar'}
className='text-4xl text-whitd'