mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE-137): integrate approve and reject functionality in RecordingForm with loading states and modal confirmations
This commit is contained in:
@@ -45,45 +45,41 @@ 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[]>([]);
|
||||||
|
|
||||||
// Track which average weight field is being edited to prevent auto-calculation override
|
|
||||||
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>(null);
|
const [editingAverageIndex, setEditingAverageIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
// Track which rows have been manually edited to prevent auto-calculation override
|
|
||||||
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(new Set());
|
const [manuallyEditedRows, setManuallyEditedRows] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// State for Location search and selection
|
|
||||||
const [locationSearchValue, setLocationSearchValue] = useState('');
|
const [locationSearchValue, setLocationSearchValue] = useState('');
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(null);
|
||||||
|
|
||||||
// State for Project Flock search
|
|
||||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
||||||
|
|
||||||
// Fetch Locations data
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
|
||||||
|
const approveModal = useModal();
|
||||||
|
const rejectModal = useModal();
|
||||||
|
|
||||||
|
// ===== API DATA FETCHING =====
|
||||||
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
||||||
search: locationSearchValue || '',
|
search: locationSearchValue || '',
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
|
|
||||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
||||||
locationsUrl,
|
locationsUrl,
|
||||||
LocationApi.getAllFetcher
|
LocationApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch Project Flocks data with location filter
|
|
||||||
const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({
|
const projectFlocksUrl = `${ProjectFlockApi.basePath}?${new URLSearchParams({
|
||||||
search: projectFlockSearchValue || '',
|
search: projectFlockSearchValue || '',
|
||||||
...(selectedLocation ? { location_id: selectedLocation.value.toString() } : {}),
|
...(selectedLocation ? { location_id: selectedLocation.value.toString() } : {}),
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
|
|
||||||
const { data: projectFlocks, isLoading: isLoadingProjectFlocks } = useSWR(
|
const { data: projectFlocks, isLoading: isLoadingProjectFlocks } = useSWR(
|
||||||
projectFlocksUrl,
|
projectFlocksUrl,
|
||||||
ProjectFlockApi.getAllFetcher
|
ProjectFlockApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch Products with location filter (both PAKAN and OVK) - using selectedLocation for now
|
|
||||||
const stockProductsUrl = useMemo(() => {
|
const stockProductsUrl = useMemo(() => {
|
||||||
if (!selectedLocation) return null;
|
if (!selectedLocation) return null;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
flags: 'PAKAN,OVK', // Fetch both flags in one request
|
flags: 'PAKAN,OVK',
|
||||||
search: '',
|
search: '',
|
||||||
location_id: selectedLocation.value.toString(),
|
location_id: selectedLocation.value.toString(),
|
||||||
});
|
});
|
||||||
@@ -95,22 +91,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
ProductWarehouseApi.getAllFetcher
|
ProductWarehouseApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extract location options from locations data
|
// ===== DATA PROCESSING =====
|
||||||
const locationOptions = useMemo(() => {
|
const locationOptions = useMemo(() => {
|
||||||
if (!isResponseSuccess(locations)) return [];
|
if (!isResponseSuccess(locations)) return [];
|
||||||
|
|
||||||
return locations?.data.map((location) => ({
|
return locations?.data.map((location) => ({
|
||||||
value: location.id,
|
value: location.id,
|
||||||
label: location.name,
|
label: location.name,
|
||||||
})) || [];
|
})) || [];
|
||||||
}, [locations]);
|
}, [locations]);
|
||||||
|
|
||||||
// Extract kandang options from project_flocks data
|
|
||||||
const projectFlockKandangOptions = useMemo(() => {
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
if (!isResponseSuccess(projectFlocks)) return [];
|
if (!isResponseSuccess(projectFlocks)) return [];
|
||||||
|
|
||||||
const options: OptionType[] = [];
|
const options: OptionType[] = [];
|
||||||
|
|
||||||
projectFlocks?.data.forEach((projectFlock) => {
|
projectFlocks?.data.forEach((projectFlock) => {
|
||||||
projectFlock.kandangs.forEach((kandang) => {
|
projectFlock.kandangs.forEach((kandang) => {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -119,11 +112,68 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [projectFlocks]);
|
}, [projectFlocks]);
|
||||||
|
|
||||||
|
const unifiedStockProducts = useMemo(() => {
|
||||||
|
const options: OptionType[] = [];
|
||||||
|
if (isResponseSuccess(stockProducts)) {
|
||||||
|
stockProducts.data.forEach((product) => {
|
||||||
|
const warehouse = product.warehouse;
|
||||||
|
const stockText = product.quantity.toLocaleString('id-ID');
|
||||||
|
|
||||||
|
const hasPakanFlag = product.product.flags?.includes('PAKAN');
|
||||||
|
const hasOvkFlag = product.product.flags?.includes('OVK');
|
||||||
|
|
||||||
|
if (hasPakanFlag) {
|
||||||
|
options.push({
|
||||||
|
value: product.id,
|
||||||
|
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOvkFlag) {
|
||||||
|
options.push({
|
||||||
|
value: product.id,
|
||||||
|
label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialValues && 'stocks' in initialValues && initialValues.stocks && type !== 'add') {
|
||||||
|
const initialValuesWithStocks = initialValues as Recording & {
|
||||||
|
stocks?: Array<{
|
||||||
|
product_warehouse_id: number;
|
||||||
|
usage_amount: number;
|
||||||
|
notes: string;
|
||||||
|
product_warehouse?: {
|
||||||
|
id: number;
|
||||||
|
product_id: number;
|
||||||
|
product_name: string;
|
||||||
|
warehouse_id: number;
|
||||||
|
warehouse_name: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
initialValuesWithStocks.stocks?.forEach((stock) => {
|
||||||
|
if (stock.product_warehouse && stock.product_warehouse.product_name) {
|
||||||
|
const existingOption = options.find(opt => opt.value === stock.product_warehouse_id);
|
||||||
|
if (!existingOption) {
|
||||||
|
options.push({
|
||||||
|
value: stock.product_warehouse_id,
|
||||||
|
label: `${stock.product_warehouse.product_name} - ${stock.product_warehouse.warehouse_name}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [stockProducts, initialValues, type]);
|
||||||
|
|
||||||
|
// ===== FORM HANDLERS =====
|
||||||
const {
|
const {
|
||||||
deleteModal,
|
deleteModal,
|
||||||
recordingFormErrorMessage,
|
recordingFormErrorMessage,
|
||||||
@@ -134,73 +184,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
confirmationModalDeleteClickHandler,
|
confirmationModalDeleteClickHandler,
|
||||||
} = useRecordingFormHandlers(initialValues?.id);
|
} = useRecordingFormHandlers(initialValues?.id);
|
||||||
|
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
|
||||||
|
|
||||||
// Modals for approve/reject actions
|
|
||||||
const approveModal = useModal();
|
|
||||||
const rejectModal = useModal();
|
|
||||||
|
|
||||||
// Approve handler
|
|
||||||
const approveHandler = async () => {
|
|
||||||
setIsApproveLoading(true);
|
|
||||||
|
|
||||||
const approveResponse = await RecordingApi.customRequest<
|
|
||||||
BaseApiResponse<Recording[]>
|
|
||||||
>('approvals', {
|
|
||||||
method: 'POST',
|
|
||||||
payload: {
|
|
||||||
action: 'APPROVED',
|
|
||||||
approvable_ids: [initialValues?.id as number],
|
|
||||||
notes: 'Approved via Form',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isResponseSuccess(approveResponse)) {
|
|
||||||
toast.success('Recording berhasil disetujui!');
|
|
||||||
approveModal.closeModal();
|
|
||||||
// Optional: redirect or refresh data
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.location.href = '/production/recording';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(approveResponse?.message as string || 'Gagal menyetujui recording');
|
|
||||||
approveModal.closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsApproveLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reject handler
|
|
||||||
const rejectHandler = async () => {
|
|
||||||
setIsRejectLoading(true);
|
|
||||||
|
|
||||||
const rejectResponse = await RecordingApi.customRequest<
|
|
||||||
BaseApiResponse<Recording[]>
|
|
||||||
>('approvals', {
|
|
||||||
method: 'POST',
|
|
||||||
payload: {
|
|
||||||
action: 'REJECTED',
|
|
||||||
approvable_ids: [initialValues?.id as number],
|
|
||||||
notes: 'Rejected via Form',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isResponseSuccess(rejectResponse)) {
|
|
||||||
toast.success('Recording berhasil ditolak!');
|
|
||||||
rejectModal.closeModal();
|
|
||||||
// Optional: redirect or refresh data
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.location.href = '/production/recording';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(rejectResponse?.message as string || 'Gagal menolak recording');
|
|
||||||
rejectModal.closeModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsRejectLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formikInitialValues = useMemo<RecordingFormValues>(
|
const formikInitialValues = useMemo<RecordingFormValues>(
|
||||||
() => getRecordingFormInitialValues(initialValues),
|
() => getRecordingFormInitialValues(initialValues),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -254,12 +237,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get location from selected project flock for stock filtering
|
// ===== HELPER FUNCTIONS =====
|
||||||
const getProjectFlockLocation = useCallback((): OptionType | null => {
|
useCallback((): OptionType | null => {
|
||||||
if (!formik.values.project_flock_kandang || !isResponseSuccess(projectFlocks)) {
|
if (!formik.values.project_flock_kandang || !isResponseSuccess(projectFlocks)) {
|
||||||
return selectedLocation; // Fallback to manual location selection
|
return selectedLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kandangId = formik.values.project_flock_kandang.value;
|
const kandangId = formik.values.project_flock_kandang.value;
|
||||||
for (const projectFlock of projectFlocks.data) {
|
for (const projectFlock of projectFlocks.data) {
|
||||||
const kandang = projectFlock.kandangs.find(k => k.id === kandangId);
|
const kandang = projectFlock.kandangs.find(k => k.id === kandangId);
|
||||||
@@ -270,82 +252,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return selectedLocation;
|
||||||
return selectedLocation; // Fallback to manual location selection
|
|
||||||
}, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]);
|
}, [formik.values.project_flock_kandang, projectFlocks, selectedLocation]);
|
||||||
|
|
||||||
// EVENT HANDLERS - Select Inputs
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
setSelectedLocation(val as OptionType);
|
|
||||||
|
|
||||||
// Reset project flock selection when location changes
|
|
||||||
formik.setFieldValue('project_flock_kandang', null);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
|
||||||
formik.setFieldTouched('project_flock_kandang', true);
|
|
||||||
formik.setFieldValue('project_flock_kandang', val);
|
|
||||||
formik.setFieldTouched('project_flock_kandang_id', true);
|
|
||||||
formik.setFieldValue('project_flock_kandang_id', (val as OptionType)?.value || 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// EVENT HANDLERS - Date Time
|
|
||||||
const recordDateTimeChangeHandler = (datetime: Date | null) => {
|
|
||||||
formik.setFieldValue('record_datetime', datetime, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-calculate average weight when weight or qty changes (but not when editing average weight manually)
|
|
||||||
useEffect(() => {
|
|
||||||
// Only run auto-calculation if no field is being edited
|
|
||||||
if (formik.values.body_weights && editingAverageIndex === null) {
|
|
||||||
const updatedBodyWeights = formik.values.body_weights.map((weight, idx) => {
|
|
||||||
// Skip the field that's being edited or has been manually edited
|
|
||||||
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
|
|
||||||
return weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...weight,
|
|
||||||
average_weight:
|
|
||||||
weight.qty > 0 && weight.weight > 0
|
|
||||||
? parseFloat((weight.weight / weight.qty).toFixed(2))
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only update if values are different to avoid infinite loops
|
|
||||||
const hasChanges = updatedBodyWeights.some(
|
|
||||||
(updated, idx) =>
|
|
||||||
idx !== editingAverageIndex && // Skip the field being edited
|
|
||||||
!manuallyEditedRows.has(idx) && // Skip manually edited rows
|
|
||||||
updated.average_weight !==
|
|
||||||
(formik.values.body_weights[idx]?.average_weight || 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasChanges) {
|
|
||||||
// Use false to prevent triggering validation and other side effects
|
|
||||||
formik.setFieldValue('body_weights', updatedBodyWeights, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
formik.values.body_weights?.map((w) => w.weight),
|
|
||||||
formik.values.body_weights?.map((w) => w.qty),
|
|
||||||
editingAverageIndex, // Include editing index in dependencies
|
|
||||||
manuallyEditedRows, // Include manually edited rows in dependencies
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Stock validation functions - Following MovementForm pattern
|
|
||||||
const getAvailableStock = useCallback(
|
const getAvailableStock = useCallback(
|
||||||
(productWarehouseId: number) => {
|
(productWarehouseId: number) => {
|
||||||
if (type === 'detail') return 0;
|
if (type === 'detail') return 0;
|
||||||
|
|
||||||
if (!isResponseSuccess(stockProducts)) return 0;
|
if (!isResponseSuccess(stockProducts)) return 0;
|
||||||
|
|
||||||
const productWarehouse = stockProducts.data.find(
|
const productWarehouse = stockProducts.data.find(
|
||||||
(pw) => pw.id === productWarehouseId
|
(pw) => pw.id === productWarehouseId
|
||||||
);
|
);
|
||||||
|
|
||||||
return productWarehouse?.quantity ?? 0;
|
return productWarehouse?.quantity ?? 0;
|
||||||
},
|
},
|
||||||
[stockProducts, type]
|
[stockProducts, type]
|
||||||
@@ -354,17 +270,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const getStockUsageError = useCallback(
|
const getStockUsageError = useCallback(
|
||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
if (type === 'detail') return null;
|
if (type === 'detail') return null;
|
||||||
|
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
||||||
const requestedUsage = Number(stock.usage_amount) || 0;
|
const requestedUsage = Number(stock.usage_amount) || 0;
|
||||||
|
|
||||||
if (requestedUsage > availableStock) {
|
if (requestedUsage > availableStock) {
|
||||||
return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`;
|
return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('id-ID')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.stocks, getAvailableStock, type]
|
[formik.values.stocks, getAvailableStock, type]
|
||||||
@@ -373,14 +285,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const getStockUsageAdornment = useCallback(
|
const getStockUsageAdornment = useCallback(
|
||||||
(stockIdx: number) => {
|
(stockIdx: number) => {
|
||||||
if (type === 'detail') return null;
|
if (type === 'detail') return null;
|
||||||
|
|
||||||
const stock = formik.values.stocks?.[stockIdx];
|
const stock = formik.values.stocks?.[stockIdx];
|
||||||
if (!stock || !stock.product_warehouse_id) return null;
|
if (!stock || !stock.product_warehouse_id) return null;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
const availableStock = getAvailableStock(stock.product_warehouse_id);
|
||||||
const requestedUsage = Number(stock.usage_amount) || 0;
|
const requestedUsage = Number(stock.usage_amount) || 0;
|
||||||
const remainingStock = availableStock - requestedUsage;
|
const remainingStock = availableStock - requestedUsage;
|
||||||
|
|
||||||
if (requestedUsage > 0) {
|
if (requestedUsage > 0) {
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
@@ -388,7 +297,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
<span className='text-sm text-gray-600 whitespace-nowrap'>
|
||||||
(tersedia: {availableStock.toLocaleString('id-ID')})
|
(tersedia: {availableStock.toLocaleString('id-ID')})
|
||||||
@@ -400,7 +308,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const hasExceededStock = useMemo(() => {
|
const hasExceededStock = useMemo(() => {
|
||||||
if (type === 'detail') return false;
|
if (type === 'detail') return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
formik.values.stocks?.some((stock, idx) => {
|
formik.values.stocks?.some((stock, idx) => {
|
||||||
return getStockUsageError(idx) !== null;
|
return getStockUsageError(idx) !== null;
|
||||||
@@ -408,273 +315,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.stocks, getStockUsageError, type]);
|
}, [formik.values.stocks, getStockUsageError, type]);
|
||||||
|
|
||||||
// EVENT HANDLERS - Body Weights
|
|
||||||
const addBodyWeight = () => {
|
|
||||||
const newBodyWeights = [
|
|
||||||
...(formik.values.body_weights || []),
|
|
||||||
{
|
|
||||||
weight: 0,
|
|
||||||
qty: 1,
|
|
||||||
average_weight: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
formik.setFieldValue('body_weights', newBodyWeights);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle calculation when weight changes
|
|
||||||
const handleWeightChange = (idx: number, value: number) => {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.weight`, value);
|
|
||||||
|
|
||||||
// Reset manual edit flag when weight changes (user wants auto-calculation)
|
|
||||||
setManuallyEditedRows(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(idx);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentWeight = formik.values.body_weights?.[idx];
|
|
||||||
if (currentWeight) {
|
|
||||||
const qty = currentWeight.qty;
|
|
||||||
if (qty > 0 && value > 0) {
|
|
||||||
const averageWeight = parseFloat((value / qty).toFixed(2));
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight);
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle calculation when qty changes
|
|
||||||
const handleQtyChange = (idx: number, value: number) => {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.qty`, value);
|
|
||||||
|
|
||||||
// Reset manual edit flag when qty changes (user wants auto-calculation)
|
|
||||||
setManuallyEditedRows(prev => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(idx);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentWeight = formik.values.body_weights?.[idx];
|
|
||||||
if (currentWeight) {
|
|
||||||
const weight = currentWeight.weight;
|
|
||||||
if (value > 0 && weight > 0) {
|
|
||||||
const averageWeight = parseFloat((weight / value).toFixed(2));
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight);
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle calculation when average_weight changes
|
|
||||||
const handleAverageWeightChange = (idx: number, value: number) => {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.average_weight`, value);
|
|
||||||
|
|
||||||
const currentWeight = formik.values.body_weights?.[idx];
|
|
||||||
if (currentWeight) {
|
|
||||||
const qty = currentWeight.qty;
|
|
||||||
if (qty > 0 && value > 0) {
|
|
||||||
const totalWeight = value * qty;
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create wrapper handlers that match NumberInput's onChange signature
|
|
||||||
const handleWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// Parse the value more carefully to handle decimal numbers properly
|
|
||||||
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
|
||||||
// Convert comma thousand separator to nothing, but keep decimal point
|
|
||||||
const normalizedValue = rawValue.replace(/,/g, '');
|
|
||||||
const value = parseFloat(normalizedValue) || 0;
|
|
||||||
handleWeightChange(idx, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQtyChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// Parse the value more carefully to handle decimal numbers properly
|
|
||||||
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
|
||||||
// Convert comma thousand separator to nothing, but keep decimal point
|
|
||||||
const normalizedValue = rawValue.replace(/,/g, '');
|
|
||||||
const value = parseFloat(normalizedValue) || 0;
|
|
||||||
handleQtyChange(idx, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAverageWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// Set focus state to prevent auto-calculation override
|
|
||||||
setEditingAverageIndex(idx);
|
|
||||||
|
|
||||||
// Mark this row as manually edited
|
|
||||||
setManuallyEditedRows(prev => new Set(prev).add(idx));
|
|
||||||
|
|
||||||
// Parse the value more carefully to handle decimal numbers properly
|
|
||||||
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
|
||||||
// Convert comma thousand separator to nothing, but keep decimal point
|
|
||||||
const normalizedValue = rawValue.replace(/,/g, '');
|
|
||||||
const value = parseFloat(normalizedValue) || 0;
|
|
||||||
handleAverageWeightChange(idx, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAverageWeightBlur = (idx: number) => {
|
|
||||||
// Clear focus state when user leaves the field to re-enable auto-calculation
|
|
||||||
setEditingAverageIndex(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeBodyWeight = (idx: number) => {
|
|
||||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
|
||||||
(_, i) => i !== idx
|
|
||||||
);
|
|
||||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSelectedBodyWeights = () => {
|
|
||||||
const updatedBodyWeights = formik.values.body_weights?.filter(
|
|
||||||
(_, idx) => !selectedBodyWeights.includes(idx)
|
|
||||||
);
|
|
||||||
formik.setFieldValue('body_weights', updatedBodyWeights);
|
|
||||||
setSelectedBodyWeights([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// EVENT HANDLERS - Stocks
|
|
||||||
const addStock = () => {
|
|
||||||
const newStocks = [
|
|
||||||
...(formik.values.stocks || []),
|
|
||||||
{
|
|
||||||
product_warehouse_id: 0,
|
|
||||||
usage_amount: 0,
|
|
||||||
notes: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
formik.setFieldValue('stocks', newStocks);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Memoized unified products for stock selection
|
|
||||||
const unifiedStockProducts = useMemo(() => {
|
|
||||||
const options: OptionType[] = [];
|
|
||||||
|
|
||||||
// Add products from API stockProducts
|
|
||||||
if (isResponseSuccess(stockProducts)) {
|
|
||||||
stockProducts.data.forEach((product) => {
|
|
||||||
const warehouse = product.warehouse;
|
|
||||||
const stockText = product.quantity.toLocaleString('id-ID');
|
|
||||||
|
|
||||||
// Check if product has any of the flags
|
|
||||||
const hasPakanFlag = product.product.flags?.includes('PAKAN');
|
|
||||||
const hasOvkFlag = product.product.flags?.includes('OVK');
|
|
||||||
|
|
||||||
// Add products with warehouse and location grouping in label (similar to projectFlockKandangOptions pattern)
|
|
||||||
if (hasPakanFlag) {
|
|
||||||
options.push({
|
|
||||||
value: product.id,
|
|
||||||
label: `[PAKAN] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasOvkFlag) {
|
|
||||||
options.push({
|
|
||||||
value: product.id,
|
|
||||||
label: `[OVK] ${product.product.name} - ${warehouse?.name || ''} (${stockText})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add existing stock products from initialValues (for detail/edit mode)
|
|
||||||
if (initialValues && 'stocks' in initialValues && initialValues.stocks && type !== 'add') {
|
|
||||||
const initialValuesWithStocks = initialValues as Recording & {
|
|
||||||
stocks?: Array<{
|
|
||||||
product_warehouse_id: number;
|
|
||||||
usage_amount: number;
|
|
||||||
notes: string;
|
|
||||||
product_warehouse?: {
|
|
||||||
id: number;
|
|
||||||
product_id: number;
|
|
||||||
product_name: string;
|
|
||||||
warehouse_id: number;
|
|
||||||
warehouse_name: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
initialValuesWithStocks.stocks?.forEach((stock) => {
|
|
||||||
if (stock.product_warehouse && stock.product_warehouse.product_name) {
|
|
||||||
const existingOption = options.find(opt => opt.value === stock.product_warehouse_id);
|
|
||||||
if (!existingOption) {
|
|
||||||
options.push({
|
|
||||||
value: stock.product_warehouse_id,
|
|
||||||
label: `${stock.product_warehouse.product_name} - ${stock.product_warehouse.warehouse_name}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [stockProducts, getProjectFlockLocation(), initialValues, type]);
|
|
||||||
|
|
||||||
|
|
||||||
// Handle stock usage amount change - simplified following MovementForm pattern
|
|
||||||
const handleStockUsageAmountChangeWrapper = useCallback(
|
|
||||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
|
|
||||||
formik.setFieldValue(`stocks.${idx}.usage_amount`, value);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Unified Stock remove handlers
|
|
||||||
const removeStock = (idx: number) => {
|
|
||||||
const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx);
|
|
||||||
formik.setFieldValue('stocks', updatedStocks);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSelectedStocks = () => {
|
|
||||||
const updatedStocks = formik.values.stocks?.filter(
|
|
||||||
(_, idx) => !selectedStocks.includes(idx)
|
|
||||||
);
|
|
||||||
formik.setFieldValue('stocks', updatedStocks);
|
|
||||||
setSelectedStocks([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// EVENT HANDLERS - Depletions
|
|
||||||
const addDepletion = () => {
|
|
||||||
const newDepletions = [
|
|
||||||
...(formik.values.depletions || []),
|
|
||||||
{
|
|
||||||
total: 0,
|
|
||||||
notes: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
formik.setFieldValue('depletions', newDepletions);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle depletion total change
|
|
||||||
const handleDepletionTotalChangeWrapper = useCallback(
|
|
||||||
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
|
|
||||||
formik.setFieldValue(`depletions.${idx}.total`, value);
|
|
||||||
},
|
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeDepletion = (idx: number) => {
|
|
||||||
const updatedDepletions = formik.values.depletions?.filter(
|
|
||||||
(_, i) => i !== idx
|
|
||||||
);
|
|
||||||
formik.setFieldValue('depletions', updatedDepletions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSelectedDepletions = () => {
|
|
||||||
const updatedDepletions = formik.values.depletions?.filter(
|
|
||||||
(_, idx) => !selectedDepletions.includes(idx)
|
|
||||||
);
|
|
||||||
formik.setFieldValue('depletions', updatedDepletions);
|
|
||||||
setSelectedDepletions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
const isRepeaterInputError = <
|
const isRepeaterInputError = <
|
||||||
T extends 'body_weights' | 'stocks' | 'depletions',
|
T extends 'body_weights' | 'stocks' | 'depletions',
|
||||||
>(
|
>(
|
||||||
@@ -713,6 +353,290 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== EVENT HANDLERS =====
|
||||||
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
setSelectedLocation(val as OptionType);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched('project_flock_kandang', true);
|
||||||
|
formik.setFieldValue('project_flock_kandang', val);
|
||||||
|
formik.setFieldTouched('project_flock_kandang_id', true);
|
||||||
|
formik.setFieldValue('project_flock_kandang_id', (val as OptionType)?.value || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const approveHandler = async () => {
|
||||||
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
|
const approveResponse = await RecordingApi.customRequest<
|
||||||
|
BaseApiResponse<Recording[]>
|
||||||
|
>('approvals', {
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'APPROVED',
|
||||||
|
approvable_ids: [initialValues?.id as number],
|
||||||
|
notes: 'Approved via Form',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseSuccess(approveResponse)) {
|
||||||
|
toast.success('Recording berhasil disetujui!');
|
||||||
|
approveModal.closeModal();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/production/recording';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(approveResponse?.message as string || 'Gagal menyetujui recording');
|
||||||
|
approveModal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApproveLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectHandler = async () => {
|
||||||
|
setIsRejectLoading(true);
|
||||||
|
|
||||||
|
const rejectResponse = await RecordingApi.customRequest<
|
||||||
|
BaseApiResponse<Recording[]>
|
||||||
|
>('approvals', {
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
action: 'REJECTED',
|
||||||
|
approvable_ids: [initialValues?.id as number],
|
||||||
|
notes: 'Rejected via Form',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isResponseSuccess(rejectResponse)) {
|
||||||
|
toast.success('Recording berhasil ditolak!');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = '/production/recording';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(rejectResponse?.message as string || 'Gagal menolak recording');
|
||||||
|
rejectModal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRejectLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Body Weights Handlers
|
||||||
|
const addBodyWeight = () => {
|
||||||
|
const newBodyWeights = [
|
||||||
|
...(formik.values.body_weights || []),
|
||||||
|
{
|
||||||
|
weight: 0,
|
||||||
|
qty: 1,
|
||||||
|
average_weight: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('body_weights', newBodyWeights);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWeightChange = (idx: number, value: number) => {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.weight`, value);
|
||||||
|
|
||||||
|
setManuallyEditedRows(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(idx);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentWeight = formik.values.body_weights?.[idx];
|
||||||
|
if (currentWeight) {
|
||||||
|
const qty = currentWeight.qty;
|
||||||
|
if (qty > 0 && value > 0) {
|
||||||
|
const averageWeight = parseFloat((value / qty).toFixed(2));
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQtyChange = (idx: number, value: number) => {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.qty`, value);
|
||||||
|
|
||||||
|
setManuallyEditedRows(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(idx);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentWeight = formik.values.body_weights?.[idx];
|
||||||
|
if (currentWeight) {
|
||||||
|
const weight = currentWeight.weight;
|
||||||
|
if (value > 0 && weight > 0) {
|
||||||
|
const averageWeight = parseFloat((weight / value).toFixed(2));
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.average_weight`, averageWeight);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.average_weight`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAverageWeightChange = (idx: number, value: number) => {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.average_weight`, value);
|
||||||
|
|
||||||
|
const currentWeight = formik.values.body_weights?.[idx];
|
||||||
|
if (currentWeight) {
|
||||||
|
const qty = currentWeight.qty;
|
||||||
|
if (qty > 0 && value > 0) {
|
||||||
|
const totalWeight = value * qty;
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue(`body_weights.${idx}.weight`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
||||||
|
const normalizedValue = rawValue.replace(/,/g, '');
|
||||||
|
const value = parseFloat(normalizedValue) || 0;
|
||||||
|
handleWeightChange(idx, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQtyChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
||||||
|
const normalizedValue = rawValue.replace(/,/g, '');
|
||||||
|
const value = parseFloat(normalizedValue) || 0;
|
||||||
|
handleQtyChange(idx, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAverageWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEditingAverageIndex(idx);
|
||||||
|
setManuallyEditedRows(prev => new Set(prev).add(idx));
|
||||||
|
|
||||||
|
const rawValue = e.target.value.replace(/[^\d,.-]/g, '');
|
||||||
|
const normalizedValue = rawValue.replace(/,/g, '');
|
||||||
|
const value = parseFloat(normalizedValue) || 0;
|
||||||
|
handleAverageWeightChange(idx, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAverageWeightBlur = (idx: number) => {
|
||||||
|
setEditingAverageIndex(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeBodyWeight = (idx: number) => {
|
||||||
|
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedBodyWeights = () => {
|
||||||
|
const updatedBodyWeights = formik.values.body_weights?.filter(
|
||||||
|
(_, idx) => !selectedBodyWeights.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('body_weights', updatedBodyWeights);
|
||||||
|
setSelectedBodyWeights([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stocks Handlers
|
||||||
|
const addStock = () => {
|
||||||
|
const newStocks = [
|
||||||
|
...(formik.values.stocks || []),
|
||||||
|
{
|
||||||
|
product_warehouse_id: 0,
|
||||||
|
usage_amount: 0,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('stocks', newStocks);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStockUsageAmountChangeWrapper = useCallback(
|
||||||
|
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
|
||||||
|
formik.setFieldValue(`stocks.${idx}.usage_amount`, value);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeStock = (idx: number) => {
|
||||||
|
const updatedStocks = formik.values.stocks?.filter((_, i) => i !== idx);
|
||||||
|
formik.setFieldValue('stocks', updatedStocks);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedStocks = () => {
|
||||||
|
const updatedStocks = formik.values.stocks?.filter(
|
||||||
|
(_, idx) => !selectedStocks.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('stocks', updatedStocks);
|
||||||
|
setSelectedStocks([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Depletions Handlers
|
||||||
|
const addDepletion = () => {
|
||||||
|
const newDepletions = [
|
||||||
|
...(formik.values.depletions || []),
|
||||||
|
{
|
||||||
|
total: 0,
|
||||||
|
notes: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
formik.setFieldValue('depletions', newDepletions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDepletionTotalChangeWrapper = useCallback(
|
||||||
|
(idx: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(e.target.value.replace(/[^\d.-]/g, '')) || 0;
|
||||||
|
formik.setFieldValue(`depletions.${idx}.total`, value);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeDepletion = (idx: number) => {
|
||||||
|
const updatedDepletions = formik.values.depletions?.filter(
|
||||||
|
(_, i) => i !== idx
|
||||||
|
);
|
||||||
|
formik.setFieldValue('depletions', updatedDepletions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedDepletions = () => {
|
||||||
|
const updatedDepletions = formik.values.depletions?.filter(
|
||||||
|
(_, idx) => !selectedDepletions.includes(idx)
|
||||||
|
);
|
||||||
|
formik.setFieldValue('depletions', updatedDepletions);
|
||||||
|
setSelectedDepletions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== EFFECTS =====
|
||||||
|
useEffect(() => {
|
||||||
|
if (formik.values.body_weights && editingAverageIndex === null) {
|
||||||
|
const updatedBodyWeights = formik.values.body_weights.map((weight, idx) => {
|
||||||
|
if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) {
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...weight,
|
||||||
|
average_weight:
|
||||||
|
weight.qty > 0 && weight.weight > 0
|
||||||
|
? parseFloat((weight.weight / weight.qty).toFixed(2))
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const hasChanges = updatedBodyWeights.some(
|
||||||
|
(updated, idx) =>
|
||||||
|
idx !== editingAverageIndex &&
|
||||||
|
!manuallyEditedRows.has(idx) &&
|
||||||
|
updated.average_weight !==
|
||||||
|
(formik.values.body_weights[idx]?.average_weight || 0)
|
||||||
|
);
|
||||||
|
if (hasChanges) {
|
||||||
|
formik.setFieldValue('body_weights', updatedBodyWeights, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
formik.values.body_weights?.map((w) => w.weight),
|
||||||
|
formik.values.body_weights?.map((w) => w.qty),
|
||||||
|
editingAverageIndex,
|
||||||
|
manuallyEditedRows,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -774,7 +698,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
{/* Body Weights Table */}
|
{/* Body Weights Table */}
|
||||||
<Card
|
<Card
|
||||||
title='Bobot Badan'
|
title='Bobot Badan'
|
||||||
@@ -839,7 +762,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
data-tip='Otomatis dihitung: Total Berat ÷ Jumlah Ayam'
|
data-tip='Otomatis dihitung: Total Berat ÷ Jumlah Ayam'
|
||||||
>
|
>
|
||||||
<Icon icon="material-symbols:info-outline" width={16} height={16} />
|
<Icon icon="material-symbols:info-outline" width={16} height={16} />
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
{type !== 'detail' && <th>Action</th>}
|
{type !== 'detail' && <th>Action</th>}
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1096,7 +1019,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
option?.value || 0
|
option?.value || 0
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-populate notes with product name by finding it in stockProducts data
|
|
||||||
if (option?.value && isResponseSuccess(stockProducts)) {
|
if (option?.value && isResponseSuccess(stockProducts)) {
|
||||||
const selectedProduct = stockProducts.data.find(
|
const selectedProduct = stockProducts.data.find(
|
||||||
(product) => product.id === option.value
|
(product) => product.id === option.value
|
||||||
@@ -1486,4 +1408,4 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RecordingForm;
|
export default RecordingForm;
|
||||||
Reference in New Issue
Block a user