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 (
projectFlocks?.data.map((projectFlock) => ({
value: projectFlock.id,
label: projectFlock.flock.name,
})) || []
);
}, [projectFlocks]);
const options: OptionType[] = []; const kandangOptions = useMemo(() => {
projectFlocks?.data.forEach((projectFlock) => { if (!selectedProjectFlock || !isResponseSuccess(projectFlocks)) return [];
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({ const selectedProjectFlockData = projectFlocks.data.find(
value: projectFlock.id, (pf) => pf.id === selectedProjectFlock.value
label: label, );
});
}); 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 options;
}, [projectFlocks, recordedProjectFlockIds, type]); return recordedIds;
}, [existingRecordings, today]);
const unifiedStockProducts = useMemo(() => { const unifiedStockProducts = useMemo(() => {
const options: OptionType[] = []; const options: OptionType[] = [];
@@ -432,39 +485,38 @@ 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; let color: 'neutral' | 'success' | 'warning' | 'error' = 'neutral';
const isAlreadyRecorded = recordedProjectFlockIds.has(projectFlockId); if (isAlreadyRecorded) {
let color: 'neutral' | 'success' | 'warning' | 'error' = 'neutral'; color = 'warning';
} else {
color = 'success';
}
if (isAlreadyRecorded) { return (
color = 'warning'; <Badge
} else { variant='soft'
color = 'success'; color={color}
} size='sm'
className={{
return ( badge: 'whitespace-nowrap font-semibold text-xs px-2',
<Badge }}
variant='soft' >
color={color} Periode {projectFlockKandangLookup.project_flock?.period}
size='sm' </Badge>
className={{ );
badge: 'whitespace-nowrap font-semibold text-xs px-2', }, [
}} projectFlocks,
> recordedProjectFlockKandangIds,
Periode {projectFlock.period} projectFlockKandangLookup,
</Badge> ]);
);
},
[projectFlocks, recordedProjectFlockIds]
);
const getProductFlagBadgeAdornment = useCallback( const getProductFlagBadgeAdornment = useCallback(
(productWarehouseId: number) => { (productWarehouseId: number) => {
@@ -563,31 +615,53 @@ 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);
if ( formik.setFieldValue('project_flock_kandang', null);
type === 'add' && formik.setFieldValue('project_flock_kandangs_id', 0);
val &&
recordedProjectFlockIds.has((val as OptionType).value as number)
) {
toast.error('Project Flock ini sudah direcord hari ini!');
return;
}
formik.setFieldTouched('project_flock_kandang', true);
formik.setFieldValue('project_flock_kandang', val);
formik.setFieldTouched('project_flock_kandangs_id', true);
formik.setFieldValue(
'project_flock_kandangs_id',
(val as OptionType)?.value || 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 (
type === 'add' &&
recordedProjectFlockKandangIds.has(projectFlockKandangId)
) {
toast.error('Project Flock Kandang ini sudah direcord hari ini!');
return;
}
formik.setFieldValue('project_flock_kandangs_id', projectFlockKandangId);
formik.setFieldValue('project_flock_kandang', {
value: projectFlockKandangId,
label: `${selectedProjectFlock?.label || ''} - ${selectedKandang?.label || ''}`,
});
}
}, [
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,58 +973,78 @@ 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 <>
required <SelectInput
label='Lokasi' required
value={selectedLocation} label='Lokasi'
onChange={locationChangeHandler} value={selectedLocation}
options={locationOptions} onChange={locationChangeHandler}
onInputChange={setLocationSearchValue} options={locationOptions}
isLoading={isLoadingLocations} onInputChange={setLocationSearchValue}
placeholder='Pilih Lokasi' isLoading={isLoadingLocations}
isClearable placeholder='Pilih Lokasi'
isSearchable isClearable
/> isSearchable
/>
<SelectInput
required
label='Project Flock'
value={selectedProjectFlock}
onChange={projectFlockChangeHandler}
options={projectFlockOptions}
onInputChange={setProjectFlockSearchValue}
isLoading={isLoadingProjectFlocks}
isDisabled={!selectedLocation}
placeholder={
selectedLocation
? 'Pilih Project Flock'
: 'Pilih Lokasi terlebih dahulu'
}
isClearable
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={
projectFlockKandangLookup
? getProjectFlockBadgeAdornment()
: undefined
}
/>
</>
)} )}
<div> {type === 'detail' && formik.values.project_flock_kandang && (
<SelectInput <div className='form-control'>
required <label className='label'>
key={`project-flock-${formik.values.project_flock_kandangs_id}`} <span className='label-text font-semibold'>
label='Project Flock' Project Flock - Kandang
value={formik.values.project_flock_kandang ?? undefined} </span>
onChange={projectFlockKandangChangeHandler} </label>
options={projectFlockKandangOptions} <div className='input input-bordered bg-gray-50'>
onInputChange={setProjectFlockSearchValue} {formik.values.project_flock_kandang.label}
isLoading={isLoadingProjectFlocks} </div>
isError={ </div>
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={
selectedLocation
? 'Pilih Project Flock - Kandang'
: 'Pilih Lokasi terlebih dahulu'
}
isClearable
isSearchable
startAdornment={
formik.values.project_flock_kandangs_id
? getProjectFlockBadgeAdornment(
formik.values.project_flock_kandangs_id
)
: undefined
}
/>
</div>
</div> </div>
</Card> </Card>
+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;
};