feat(FE-170,174): implement project flock kandang selection and validation in daily recording form

This commit is contained in:
rstubryan
2025-10-30 21:41:14 +07:00
parent 87295252aa
commit b7de8b40d8
2 changed files with 234 additions and 115 deletions
@@ -29,12 +29,16 @@ import { ProjectFlockApi } from '@/services/api/production';
import { LocationApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory'; import { ProductWarehouseApi } from '@/services/api/inventory';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { PeriodFlock } from '@/types/api/production/project-flock'; import {
PeriodFlock,
ProjectFlockKandangLookup,
} from '@/types/api/production/project-flock';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import Card from '@/components/Card'; import Card from '@/components/Card';
import Badge from '@/components/Badge'; import Badge from '@/components/Badge';
import { Kandang } from '@/types/api/master-data/kandang';
interface RecordingFormProps { interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -46,9 +50,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [selectedStocks, setSelectedStocks] = useState<number[]>([]); const [selectedStocks, setSelectedStocks] = useState<number[]>([]);
const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]); const [selectedDepletions, setSelectedDepletions] = useState<number[]>([]);
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>( const [editingAverageIndex] = useState<number | null>(null);
null
);
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>( const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(
new Set() new Set()
); );
@@ -58,6 +60,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
null null
); );
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
const [selectedProjectFlock, setSelectedProjectFlock] =
useState<OptionType | null>(null);
const [selectedKandang, setSelectedKandang] = useState<OptionType | null>(
null
);
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false);
@@ -85,6 +92,30 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
ProjectFlockApi.getAllFetcher ProjectFlockApi.getAllFetcher
); );
const projectFlockKandangLookupUrl = useMemo(() => {
if (!selectedProjectFlock || !selectedKandang) return null;
const params = new URLSearchParams({
project_flock_id: selectedProjectFlock.value.toString(),
kandang_id: selectedKandang.value.toString(),
});
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
}, [selectedProjectFlock, selectedKandang]);
const { data: projectFlockKandangLookupData } = useSWR(
projectFlockKandangLookupUrl,
projectFlockKandangLookupUrl
? () =>
ProjectFlockApi.getAllFetcher(
projectFlockKandangLookupUrl
) as Promise<BaseApiResponse<ProjectFlockKandangLookup>>
: null
);
const projectFlockKandangLookup =
projectFlockKandangLookupData?.status === 'success'
? projectFlockKandangLookupData.data
: undefined;
const stockProductsUrl = useMemo(() => { const stockProductsUrl = useMemo(() => {
if (!selectedLocation) return null; if (!selectedLocation) return null;
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -199,25 +230,47 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
}, [locations]); }, [locations]);
const projectFlockKandangOptions = useMemo(() => { const projectFlockOptions = useMemo(() => {
if (!isResponseSuccess(projectFlocks)) return []; if (!isResponseSuccess(projectFlocks)) return [];
return (
const options: OptionType[] = []; projectFlocks?.data.map((projectFlock) => ({
projectFlocks?.data.forEach((projectFlock) => {
projectFlock.kandangs.forEach((kandang) => {
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlock.id);
const label = isAlreadyRecorded
? `${projectFlock.flock.name} - ${kandang.name} (Sudah Direcord)`
: `${projectFlock.flock.name} - ${kandang.name}`;
options.push({
value: projectFlock.id, value: projectFlock.id,
label: label, label: projectFlock.flock.name,
})) || []
);
}, [projectFlocks]);
const kandangOptions = useMemo(() => {
if (!selectedProjectFlock || !isResponseSuccess(projectFlocks)) return [];
const selectedProjectFlockData = projectFlocks.data.find(
(pf) => pf.id === selectedProjectFlock.value
);
if (!selectedProjectFlockData || !selectedProjectFlockData.kandangs)
return [];
return selectedProjectFlockData.kandangs.map((kandang: Kandang) => ({
value: kandang.id,
label: kandang.name,
}));
}, [selectedProjectFlock, projectFlocks]);
const recordedProjectFlockKandangIds = useMemo(() => {
if (!isResponseSuccess(existingRecordings)) return new Set<number>();
const todayRecordings = existingRecordings?.data || [];
const recordedIds = new Set<number>();
todayRecordings.forEach((recording) => {
const recordingDate = recording.record_datetime?.split('T')[0];
if (recordingDate === today) {
recordedIds.add(recording.project_flock_kandangs_id);
}
}); });
});
}); return recordedIds;
return options; }, [existingRecordings, today]);
}, [projectFlocks, recordedProjectFlockIds, type]);
const unifiedStockProducts = useMemo(() => { const unifiedStockProducts = useMemo(() => {
const options: OptionType[] = []; const options: OptionType[] = [];
@@ -432,16 +485,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik.values.stocks, getAvailableStock, type] [formik.values.stocks, getAvailableStock, type]
); );
const getProjectFlockBadgeAdornment = useCallback( const getProjectFlockBadgeAdornment = useCallback(() => {
(projectFlockId: number) => { if (!isResponseSuccess(projectFlocks) || !projectFlockKandangLookup)
if (!isResponseSuccess(projectFlocks)) return null; return null;
const projectFlock = projectFlocks.data.find( const isAlreadyRecorded = recordedProjectFlockKandangIds.has(
(pf) => pf.id === projectFlockId projectFlockKandangLookup.id
); );
if (!projectFlock) return null;
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlockId);
let color: 'neutral' | 'success' | 'warning' | 'error' = 'neutral'; let color: 'neutral' | 'success' | 'warning' | 'error' = 'neutral';
if (isAlreadyRecorded) { if (isAlreadyRecorded) {
@@ -459,12 +509,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
badge: 'whitespace-nowrap font-semibold text-xs px-2', badge: 'whitespace-nowrap font-semibold text-xs px-2',
}} }}
> >
Periode {projectFlock.period} Periode {projectFlockKandangLookup.project_flock?.period}
</Badge> </Badge>
); );
}, }, [
[projectFlocks, recordedProjectFlockIds] projectFlocks,
); recordedProjectFlockKandangIds,
projectFlockKandangLookup,
]);
const getProductFlagBadgeAdornment = useCallback( const getProductFlagBadgeAdornment = useCallback(
(productWarehouseId: number) => { (productWarehouseId: number) => {
@@ -563,30 +615,52 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
// ===== EVENT HANDLERS ===== // ===== EVENT HANDLERS =====
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedLocation(val as OptionType); setSelectedLocation(val as OptionType);
setSelectedProjectFlock(null);
setSelectedKandang(null);
formik.setFieldValue('project_flock_kandang', null); formik.setFieldValue('project_flock_kandang', null);
formik.setFieldValue('project_flock_kandangs_id', 0); formik.setFieldValue('project_flock_kandangs_id', 0);
}; };
const projectFlockKandangChangeHandler = ( const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => {
val: OptionType | OptionType[] | null setSelectedProjectFlock(val as OptionType);
) => { setSelectedKandang(null);
formik.setFieldValue('project_flock_kandang', null);
formik.setFieldValue('project_flock_kandangs_id', 0);
};
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedKandang(val as OptionType);
formik.setFieldTouched('project_flock_kandang', true);
formik.setFieldTouched('project_flock_kandangs_id', true);
};
useEffect(() => {
if (projectFlockKandangLookup?.project_flock_kandang_id) {
const projectFlockKandangId =
projectFlockKandangLookup.project_flock_kandang_id;
if ( if (
type === 'add' && type === 'add' &&
val && recordedProjectFlockKandangIds.has(projectFlockKandangId)
recordedProjectFlockIds.has((val as OptionType).value as number)
) { ) {
toast.error('Project Flock ini sudah direcord hari ini!'); toast.error('Project Flock Kandang ini sudah direcord hari ini!');
return; return;
} }
formik.setFieldTouched('project_flock_kandang', true); formik.setFieldValue('project_flock_kandangs_id', projectFlockKandangId);
formik.setFieldValue('project_flock_kandang', val);
formik.setFieldTouched('project_flock_kandangs_id', true); formik.setFieldValue('project_flock_kandang', {
formik.setFieldValue( value: projectFlockKandangId,
'project_flock_kandangs_id', label: `${selectedProjectFlock?.label || ''} - ${selectedKandang?.label || ''}`,
(val as OptionType)?.value || 0 });
); }
}; }, [
projectFlockKandangLookup,
selectedProjectFlock,
selectedKandang,
type,
recordedProjectFlockKandangIds,
]);
const approveHandler = async () => { const approveHandler = async () => {
setIsApproveLoading(true); setIsApproveLoading(true);
@@ -868,6 +942,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
title='Recording' title='Recording'
backUrl='/production/recording' backUrl='/production/recording'
/> />
{/* Project Flock Info Card */}
{projectFlockKandangLookup && (
<Badge
variant='soft'
color='info'
size='md'
className={{
badge: 'whitespace-nowrap font-semibold text-xs px-2',
}}
>
{projectFlockKandangLookup.project_flock.category}
</Badge>
)}
<form <form
onSubmit={formik.handleSubmit} onSubmit={formik.handleSubmit}
className='w-full mt-8 flex flex-col gap-6' className='w-full mt-8 flex flex-col gap-6'
@@ -884,10 +973,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
className={ className={
type === 'detail' type === 'detail'
? 'flex flex-col gap-6' ? 'flex flex-col gap-6'
: 'grid grid-cols-1 md:grid-cols-2 gap-6' : 'grid grid-cols-3 gap-4'
} }
> >
{type === 'detail' ? null : ( {type === 'detail' ? null : (
<>
<SelectInput <SelectInput
required required
label='Lokasi' label='Lokasi'
@@ -900,43 +990,62 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
isClearable isClearable
isSearchable isSearchable
/> />
)}
<div>
<SelectInput <SelectInput
required required
key={`project-flock-${formik.values.project_flock_kandangs_id}`}
label='Project Flock' label='Project Flock'
value={formik.values.project_flock_kandang ?? undefined} value={selectedProjectFlock}
onChange={projectFlockKandangChangeHandler} onChange={projectFlockChangeHandler}
options={projectFlockKandangOptions} options={projectFlockOptions}
onInputChange={setProjectFlockSearchValue} onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks} isLoading={isLoadingProjectFlocks}
isError={ isDisabled={!selectedLocation}
formik.touched.project_flock_kandangs_id &&
Boolean(formik.errors.project_flock_kandangs_id)
}
errorMessage={
formik.errors.project_flock_kandangs_id as string
}
isDisabled={type === 'detail' || !selectedLocation}
placeholder={ placeholder={
selectedLocation selectedLocation
? 'Pilih Project Flock - Kandang' ? 'Pilih Project Flock'
: 'Pilih Lokasi terlebih dahulu' : 'Pilih Lokasi terlebih dahulu'
} }
isClearable isClearable
isSearchable isSearchable
/>
<SelectInput
required
label='Kandang'
value={selectedKandang}
onChange={kandangChangeHandler}
options={kandangOptions}
isLoading={false}
isDisabled={!selectedProjectFlock}
placeholder={
selectedProjectFlock
? 'Pilih Kandang'
: 'Pilih Project Flock terlebih dahulu'
}
isClearable
isSearchable={false}
startAdornment={ startAdornment={
formik.values.project_flock_kandangs_id projectFlockKandangLookup
? getProjectFlockBadgeAdornment( ? getProjectFlockBadgeAdornment()
formik.values.project_flock_kandangs_id
)
: undefined : undefined
} }
/> />
</>
)}
{type === 'detail' && formik.values.project_flock_kandang && (
<div className='form-control'>
<label className='label'>
<span className='label-text font-semibold'>
Project Flock - Kandang
</span>
</label>
<div className='input input-bordered bg-gray-50'>
{formik.values.project_flock_kandang.label}
</div> </div>
</div> </div>
)}
</div>
</Card> </Card>
{/* Body Weights Table */} {/* Body Weights Table */}
+10
View File
@@ -47,3 +47,13 @@ export type ProjectFlockApprovalPayload = {
action: 'APPROVED' | 'REJECTED'; action: 'APPROVED' | 'REJECTED';
approvable_ids: number[]; approvable_ids: number[];
}; };
export type ProjectFlockKandangLookup = {
id: number;
project_flock_kandang_id: number;
project_flock_id: number;
kandang_id: number;
kandang: Kandang;
project_flock: ProjectFlock;
quantity: number;
};