mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-170,174): implement project flock kandang selection and validation in daily recording form
This commit is contained in:
@@ -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
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user