Merge branch 'fix/adjustment-confirmation-modal-and-recording-form' into 'development'

[HOTFIX/FE] Adjustment Confirmation Modal with Notes Usage and Recording Form

See merge request mbugroup/lti-web-client!298
This commit is contained in:
Rivaldi A N S
2026-02-02 08:54:41 +00:00
5 changed files with 248 additions and 75 deletions
@@ -13,6 +13,7 @@ interface ConfirmationModalWithNotesProps
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
rows?: number;
placeholder?: string;
onClose?: () => void;
primaryButton?: {
text?: string;
@@ -32,6 +33,7 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
className,
rows = 3,
placeholder = 'Catatan...',
onClose,
...props
}) => {
const randomId = useId();
@@ -41,6 +43,11 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
setNotes(e.target.value);
};
const closeModalHandler = () => {
onClose?.();
ref.current?.close();
};
return (
<ConfirmationModal
ref={ref}
@@ -49,12 +56,32 @@ const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
closeOnBackdrop={closeOnBackdrop}
primaryButton={{
...primaryButton,
onClick: () => {
primaryButton?.onClick?.(notes);
onClick: (e) => {
if (primaryButton && primaryButton?.onClick) {
primaryButton?.onClick?.(notes);
} else {
closeModalHandler();
}
setNotes('');
},
}}
secondaryButton={secondaryButton}
secondaryButton={
secondaryButton
? {
text: secondaryButton?.text ?? 'Tidak',
onClick: (e) => {
if (secondaryButton && secondaryButton?.onClick) {
secondaryButton.onClick?.(e);
} else {
closeModalHandler();
}
setNotes('');
},
}
: undefined
}
className={className}
{...props}
>
@@ -100,6 +100,7 @@ const ExpenseRequestContent = ({
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [, setApprovalNotes] = useState('');
const formik = useFormik<UploadRequestDocumentsFormValues>({
initialValues: {
@@ -130,10 +131,12 @@ const ExpenseRequestContent = ({
};
const approveClickHandler = () => {
setApprovalNotes('');
approveModal.openModal();
};
const rejectClickHandler = () => {
setApprovalNotes('');
rejectModal.openModal();
};
@@ -200,6 +203,7 @@ const ExpenseRequestContent = ({
approveModal.closeModal();
toast.success(approveResponse?.message);
setApprovalNotes('');
router.push('/expense');
} else {
approveModal.closeModal();
@@ -234,6 +238,7 @@ const ExpenseRequestContent = ({
rejectModal.closeModal();
toast.success(rejectResponse.message);
setApprovalNotes('');
router.push('/expense');
} else {
rejectModal.closeModal();
@@ -710,6 +715,10 @@ const ExpenseRequestContent = ({
text='Apakah anda yakin ingin approve data biaya operasional ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
setApprovalNotes('');
approveModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -725,6 +734,10 @@ const ExpenseRequestContent = ({
text='Apakah anda yakin ingin reject data biaya operasional ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
setApprovalNotes('');
rejectModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -185,6 +185,7 @@ const ExpensesTable = () => {
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const [, setApprovalNotes] = useState('');
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
@@ -342,6 +343,7 @@ const ExpensesTable = () => {
[String(props.row.original.id)]: true,
});
setApprovalNotes('');
approveModal.openModal();
};
@@ -353,6 +355,7 @@ const ExpensesTable = () => {
[String(props.row.original.id)]: true,
});
setApprovalNotes('');
rejectModal.openModal();
};
@@ -412,10 +415,12 @@ const ExpensesTable = () => {
// };
const bulkApproveClickHandler = () => {
setApprovalNotes('');
approveModal.openModal();
};
const bulkRejectClickHandler = () => {
setApprovalNotes('');
rejectModal.openModal();
};
@@ -468,6 +473,7 @@ const ExpensesTable = () => {
`Berhasil approve ${selectedRowIds.length} data biaya operasional!`
);
setApprovalNotes('');
setRowSelection({});
} else {
approveModal.closeModal();
@@ -509,6 +515,7 @@ const ExpensesTable = () => {
toast.success(
`Berhasil reject ${selectedRowIds.length} data biaya operasional!`
);
setApprovalNotes('');
setRowSelection({});
} else {
rejectModal.closeModal();
@@ -787,6 +794,10 @@ const ExpensesTable = () => {
text='Apakah anda yakin ingin approve data biaya operasional ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
setApprovalNotes('');
approveModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -802,6 +813,10 @@ const ExpensesTable = () => {
text='Apakah anda yakin ingin reject data biaya operasional ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => {
setApprovalNotes('');
rejectModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -241,6 +241,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
new Date().toISOString().split('T')[0]
);
const [duplicateErrorShown, setDuplicateErrorShown] = useState(false);
const [nextDayErrorShown, setNextDayErrorShown] = useState(false);
useEffect(() => {
return () => {
@@ -553,6 +554,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const nextDayRecordingUrl = useMemo(() => {
if (!projectFlockKandangLookup) return null;
if (!selectedRecordDate) return null;
const projectFlockKandangId = projectFlockKandangLookup.id;
return `${RecordingApi.basePath}/next-day?project_flock_kandang_id=${projectFlockKandangId}&record_date=${selectedRecordDate}`;
}, [projectFlockKandangLookup, selectedRecordDate]);
@@ -575,10 +577,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setNextDayRecording(
nextDayRecordingData.data as unknown as NextDayRecording
);
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
} else if (nextDayRecordingData?.status === 'error') {
setNextDayRecording(null);
if (!nextDayErrorShown) {
toast.error(
nextDayRecordingData.message ||
'Terjadi kesalahan saat memuat data hari berikutnya',
{ duration: Infinity }
);
setNextDayErrorShown(true);
}
} else {
setNextDayRecording(null);
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
}
}, [nextDayRecordingData]);
}, [nextDayRecordingData, nextDayErrorShown]);
const {
rawData: eggProductsData,
@@ -1196,6 +1216,66 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[stockProducts, depletionProductsData, eggProductsData, initialValues, type]
);
const getAvailableStockProductOptions = useCallback(
(currentIdx: number) => {
const selectedProductIds =
formik.values.stocks
?.filter((s, idx) => {
return (
idx !== currentIdx &&
s.product_warehouse_id &&
s.product_warehouse_id !== 0
);
})
.map((s) => s.product_warehouse_id) || [];
return unifiedStockProducts.filter(
(opt) => !selectedProductIds.includes(Number(opt.value))
);
},
[formik.values.stocks, unifiedStockProducts]
);
const getAvailableDepletionProductOptions = useCallback(
(currentIdx: number) => {
const selectedProductIds =
formik.values.depletions
?.filter((d, idx) => {
return (
idx !== currentIdx &&
d.product_warehouse_id &&
d.product_warehouse_id !== 0
);
})
.map((d) => d.product_warehouse_id) || [];
return depletionProducts.filter(
(opt) => !selectedProductIds.includes(Number(opt.value))
);
},
[formik.values.depletions, depletionProducts]
);
const getAvailableEggProductOptions = useCallback(
(currentIdx: number) => {
const selectedProductIds =
(formik.values as RecordingLayingFormValues).eggs
?.filter((e, idx) => {
return (
idx !== currentIdx &&
e.product_warehouse_id &&
e.product_warehouse_id !== 0
);
})
.map((e) => e.product_warehouse_id) || [];
return eggProducts.filter(
(opt) => !selectedProductIds.includes(Number(opt.value))
);
},
[formik.values, eggProducts]
);
const hasExceededStock = useMemo(() => {
if ((type as 'add' | 'edit' | 'detail') === 'detail') return false;
return (
@@ -1255,6 +1335,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
toast.dismiss();
setDuplicateErrorShown(false);
}
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
setSelectedProjectFlockLocationId(
location ? location.value.toString() : ''
);
@@ -1275,6 +1359,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
toast.dismiss();
setDuplicateErrorShown(false);
}
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
};
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -1291,6 +1379,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
toast.dismiss();
setDuplicateErrorShown(false);
}
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
if (selectedLocation && kandang) {
setStockProductsLocationId(selectedLocation.value.toString());
setStockProductsKandangId(kandang.value.toString());
@@ -1320,11 +1412,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
toast.dismiss();
setDuplicateErrorShown(false);
}
if (nextDayErrorShown) {
toast.dismiss();
setNextDayErrorShown(false);
}
setTimeout(() => {
formik.validateField('project_flock_kandang_id');
}, 0);
},
[formik, duplicateErrorShown]
[formik, duplicateErrorShown, nextDayErrorShown]
);
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
@@ -1350,28 +1446,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
setDuplicateErrorShown(false);
}
}
if (
nextDayRecording &&
nextDayRecording.project_flock_kandang_id === projectFlockKandangId
) {
const hasSameDayRecording = isResponseSuccess(existingRecordings)
? existingRecordings.data?.some(
(recording: Recording) =>
recording.project_flock.project_flock_kandang_id ===
projectFlockKandangId &&
recording.day === nextDayRecording.next_day
)
: false;
if (hasSameDayRecording) {
toast.error(
`Recording untuk hari ke-${nextDayRecording.next_day} sudah ada datanya.
Tidak bisa membuat recording di hari yang sama dengan project flock yang sama, mohon perbarui recording yang sudah ada terlebih dahulu.`
);
return;
}
}
}
if (formik.values.project_flock_kandang_id !== projectFlockKandangId) {
@@ -1831,8 +1905,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
<div>
<span className='text-sm text-gray-600'>Umur</span>
<p className='font-semibold'>
{nextDayRecording
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
{type === 'add'
? nextDayRecording
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
: '-'
: initialValues?.day
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
: '-'}
@@ -2398,7 +2474,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
option?.value || 0
);
}}
options={unifiedStockProducts}
options={getAvailableStockProductOptions(idx)}
placeholder={
!formik.values.project_flock_kandang_id
? 'Pilih kandang terlebih dahulu'
@@ -2619,7 +2695,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
option?.value || 0
);
}}
options={depletionProducts}
options={getAvailableDepletionProductOptions(idx)}
placeholder='Pilih Kondisi'
isLoading={isLoadingDepletionProducts}
onMenuScrollToBottom={loadMoreDepletionProducts}
@@ -2837,7 +2913,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
option?.value || 0
);
}}
options={eggProducts}
options={getAvailableEggProductOptions(idx)}
placeholder='Pilih Kondisi Telur'
isLoading={isLoadingEggProducts}
onMenuScrollToBottom={loadMoreEggProducts}
@@ -3116,7 +3192,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
text='Apakah anda yakin ingin menyetujui data Recording ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
onClick: () => {
setApprovalNotes('');
approveModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -3138,7 +3217,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
text='Apakah anda yakin ingin menolak data Recording ini?'
secondaryButton={{
text: 'Tidak',
onClick: () => setApprovalNotes(''),
onClick: () => {
setApprovalNotes('');
rejectModal.closeModal();
},
}}
primaryButton={{
text: 'Ya',
@@ -105,6 +105,7 @@ const PurchaseOrderDetail = ({
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
const [, setApprovalNotes] = useState('');
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
@@ -207,12 +208,15 @@ const PurchaseOrderDetail = ({
switch (approvalStep) {
case 1:
setApprovalNotes('');
staffApprovalModal.openModal();
break;
case 2:
setApprovalNotes('');
confirmationModalWithNotes.openModal();
break;
case 3:
setApprovalNotes('');
acceptApprovalModal.openModal();
break;
default:
@@ -225,12 +229,15 @@ const PurchaseOrderDetail = ({
switch (approvalStep) {
case 1:
setApprovalNotes('');
staffRejectionModal.openModal();
break;
case 2:
setApprovalNotes('');
managerRejectionModal.openModal();
break;
case 3:
setApprovalNotes('');
acceptRejectionModal.openModal();
break;
default:
@@ -406,6 +413,56 @@ const PurchaseOrderDetail = ({
refetchData,
]);
// ===== APPROVAL/REJECTION HANDLERS =====
const managerApprovalHandler = async (notes: string) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'APPROVED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refreshApprovals();
await refetchData?.();
setApprovalNotes('');
confirmationModalWithNotes.closeModal();
};
const staffRejectionHandler = async (notes: string) => {
const payload: CreateStaffApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createStaffApprovalHandler(payload);
await refetchData?.();
setApprovalNotes('');
staffRejectionModal.closeModal();
};
const acceptRejectionHandler = async (notes: string) => {
const payload: CreateAcceptApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createAcceptApprovalHandler(payload);
await refetchData?.();
setApprovalNotes('');
acceptRejectionModal.closeModal();
};
const managerRejectionHandler = async (notes: string) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refetchData?.();
setApprovalNotes('');
managerRejectionModal.closeModal();
};
if (!initialValues) {
return null;
}
@@ -969,20 +1026,14 @@ const PurchaseOrderDetail = ({
primaryButton={{
text: 'Ya, Lanjutkan',
color: 'success',
onClick: async (notes) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'APPROVED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refreshApprovals();
await refetchData?.();
confirmationModalWithNotes.closeModal();
},
onClick: managerApprovalHandler,
}}
secondaryButton={{
text: 'Batal',
onClick: () => {
setApprovalNotes('');
confirmationModalWithNotes.closeModal();
},
}}
/>
@@ -1071,19 +1122,14 @@ const PurchaseOrderDetail = ({
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateStaffApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createStaffApprovalHandler(payload);
await refetchData?.();
staffRejectionModal.closeModal();
},
onClick: staffRejectionHandler,
}}
secondaryButton={{
text: 'Batal',
onClick: () => {
setApprovalNotes('');
staffRejectionModal.closeModal();
},
}}
/>
@@ -1098,19 +1144,14 @@ const PurchaseOrderDetail = ({
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateAcceptApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createAcceptApprovalHandler(payload);
await refetchData?.();
acceptRejectionModal.closeModal();
},
onClick: acceptRejectionHandler,
}}
secondaryButton={{
text: 'Batal',
onClick: () => {
setApprovalNotes('');
acceptRejectionModal.closeModal();
},
}}
/>
@@ -1125,19 +1166,14 @@ const PurchaseOrderDetail = ({
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refetchData?.();
managerRejectionModal.closeModal();
},
onClick: managerRejectionHandler,
}}
secondaryButton={{
text: 'Batal',
onClick: () => {
setApprovalNotes('');
managerRejectionModal.closeModal();
},
}}
/>