Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu

This commit is contained in:
rstubryan
2026-01-27 13:23:51 +07:00
8 changed files with 586 additions and 168 deletions
@@ -0,0 +1,294 @@
'use client';
import { ChangeEventHandler, RefObject, useId, useState } from 'react';
import useSWR from 'swr';
import ConfirmationModal, {
ConfirmationModalProps,
} from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import TextArea from '@/components/input/TextArea';
import { ColumnDef } from '@tanstack/react-table';
import { cn, formatDate, formatNumber } from '@/lib/helper';
import { TransferToLayingFormValues } from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema';
import { Color } from '@/types/theme';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseSuccess } from '@/lib/api-helper';
interface TransferToLayingConfirmationModalProps
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error';
transferToLayingIds?: number[];
transferToLayingForm?: TransferToLayingFormValues;
onClose?: () => void;
withNote?: boolean;
noteLabel?: string;
rows?: number;
placeholder?: string;
primaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: (notes: string) => void;
};
}
interface TransferToLayingConfirmationTableDataType {
label: string;
value: string;
subRows?: TransferToLayingConfirmationTableDataType[];
}
const TransferToLayingConfirmationModalTable = ({
transferToLayingForm,
transferToLayingId,
}: {
transferToLayingForm?: TransferToLayingFormValues;
transferToLayingId?: number;
}) => {
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
useSWR(
transferToLayingId
? ['detail-transfer-to-laying', String(transferToLayingId)]
: undefined,
([_, id]) => TransferToLayingApi.getSingle(Number(id))
);
const confirmationTableColumns: ColumnDef<TransferToLayingConfirmationTableDataType>[] =
[
{
header: 'Label',
accessorKey: 'label',
enableSorting: false,
cell: ({ row }) => {
const isSubRow = row.depth > 0;
return (
<>
{!isSubRow && row.original.label}
{isSubRow && (
<div
className={cn('w-full min-h-full flex items-stretch gap-0')}
>
<div className='w-px mx-4 bg-base-content/10' />
<span className='p-3'>{row.original.label}</span>
</div>
)}
</>
);
},
},
{
header: 'Value',
accessorKey: 'value',
enableSorting: false,
},
];
const confirmationTableData: TransferToLayingConfirmationTableDataType[] = [
{
label: 'Tanggal',
value: formatDate(
transferToLayingId && isResponseSuccess(transferToLaying)
? transferToLaying.data.transfer_date
: transferToLayingForm?.transfer_date,
'DD MMMM YYYY'
),
},
{
label: 'Flock Asal',
value:
transferToLayingId && isResponseSuccess(transferToLaying)
? transferToLaying.data.from_project_flock.flock_name
: (transferToLayingForm?.flockSource?.label ?? '-'),
subRows:
transferToLayingId && isResponseSuccess(transferToLaying)
? (transferToLaying.data.sources?.map(
(sourceProjectFlockKandang) => ({
label:
sourceProjectFlockKandang.source_project_flock_kandang.kandang
.name,
value: formatNumber(
Number(sourceProjectFlockKandang.qty),
'en-US'
),
})
) ?? [])
: (transferToLayingForm?.flockSourceKandangs?.map((kandang) => ({
label: kandang.kandang.label,
value: formatNumber(Number(kandang.quantity), 'en-US'),
})) ?? []),
},
{
label: 'Flock Tujuan',
value:
transferToLayingId && isResponseSuccess(transferToLaying)
? transferToLaying.data.to_project_flock.flock_name
: (transferToLayingForm?.flockDestination?.label ?? '-'),
subRows:
transferToLayingId && isResponseSuccess(transferToLaying)
? (transferToLaying.data.targets?.map(
(targetProjectFlockKandang) => ({
label:
targetProjectFlockKandang.target_project_flock_kandang.kandang
.name,
value: formatNumber(
Number(targetProjectFlockKandang.qty),
'en-US'
),
})
) ?? [])
: (transferToLayingForm?.flockDestinationKandangs?.map((kandang) => ({
label: kandang.kandang.label,
value: formatNumber(Number(kandang.quantity), 'en-US'),
})) ?? []),
},
{
label: 'Jumlah Transfer',
value: formatNumber(
transferToLayingId && isResponseSuccess(transferToLaying)
? Number(
transferToLaying.data.sources.reduce(
(total, source) => total + Number(source.qty),
0
)
)
: Number(transferToLayingForm?.totalQuantity),
'en-US'
),
},
{
label: 'Notes',
value:
transferToLayingId && isResponseSuccess(transferToLaying)
? (transferToLaying.data.notes ?? '-')
: (transferToLayingForm?.reason ?? '-'),
},
];
return (
<Table<TransferToLayingConfirmationTableDataType>
columns={confirmationTableColumns}
data={confirmationTableData}
withPagination={false}
pageSize={10000}
expanded={true}
getSubRows={(row) => row.subRows}
className={{
headerRowClassName: 'border-b border-base-content/10',
bodyRowClassName: 'border-none',
bodySubRowClassName: () => 'border-none',
bodySubRowColumnClassName: () => 'first:p-0',
}}
/>
);
};
const TransferToLayingConfirmationModal = ({
ref,
type = 'success',
transferToLayingForm,
transferToLayingIds,
onClose,
withNote,
rows = 4,
noteLabel,
placeholder = 'Alasan Transfer',
primaryButton,
secondaryButton,
...props
}: TransferToLayingConfirmationModalProps) => {
const randomId = useId();
const [notes, setNotes] = useState('');
const notesChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setNotes(e.target.value);
};
const closeModalHandler = () => {
onClose?.();
ref.current?.close();
};
return (
<ConfirmationModal
ref={ref}
iconPosition='left'
type={type}
primaryButton={{
...primaryButton,
text: primaryButton?.text ?? 'Oke',
color: primaryButton?.color ?? 'primary',
className: 'rounded-lg',
onClick: (e) => {
if (withNote) {
primaryButton?.onClick?.(notes);
} else if (primaryButton && primaryButton?.onClick) {
primaryButton?.onClick?.('');
} else {
closeModalHandler();
}
setNotes('');
},
}}
secondaryButton={
secondaryButton
? {
text: secondaryButton?.text ?? 'Cancel',
color: secondaryButton?.color ?? 'none',
onClick: (e) => {
if (secondaryButton && secondaryButton?.onClick) {
secondaryButton.onClick?.(e);
} else {
closeModalHandler();
}
setNotes('');
},
}
: undefined
}
className={{
modalBox: 'max-h-full',
}}
{...props}
>
<div className='flex flex-col gap-4'>
{!transferToLayingIds && transferToLayingForm && (
<TransferToLayingConfirmationModalTable
transferToLayingForm={transferToLayingForm}
/>
)}
{transferToLayingIds &&
!transferToLayingForm &&
transferToLayingIds.map((transferToLayingId, idx) => (
<TransferToLayingConfirmationModalTable
key={idx}
transferToLayingId={transferToLayingId}
/>
))}
{withNote && (
<TextArea
name={randomId}
label={noteLabel}
placeholder={placeholder}
value={notes}
onChange={notesChangeHandler}
rows={rows}
/>
)}
</div>
</ConfirmationModal>
);
};
export default TransferToLayingConfirmationModal;
@@ -7,6 +7,7 @@ import {
useMemo,
useState,
} from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR, { useSWRConfig } from 'swr';
import toast from 'react-hot-toast';
@@ -19,8 +20,8 @@ import { OptionType, useSelect } from '@/components/input/SelectInput';
import NumberInput from '@/components/input/NumberInput';
import TextArea from '@/components/input/TextArea';
import AlertErrorList from '@/components/helper/form/FormErrors';
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
import { useRouter, useSearchParams } from 'next/navigation';
import { ProjectFlockApi } from '@/services/api/production';
import { getIn, useFormik } from 'formik';
import {
@@ -58,8 +59,11 @@ const TransferToLayingFormModal = () => {
};
const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
useSWR(transferToLayingId ? transferToLayingId : undefined, (id: number) =>
TransferToLayingApi.getSingle(id)
useSWR(
transferToLayingId
? ['detail-transfer-to-laying', transferToLayingId]
: undefined,
([, id]) => TransferToLayingApi.getSingle(Number(id))
);
/**
@@ -71,7 +75,11 @@ const TransferToLayingFormModal = () => {
const [step, setStep] = useState(1);
const formModal = useModal();
const successModal = useModal();
const [formikLastValues, setFormikLastValues] = useState<
TransferToLayingFormValues | undefined
>(undefined);
const [formErrorMessage, setFormErrorMessage] = useState<string | null>(null);
// Flock Source
@@ -133,6 +141,7 @@ const TransferToLayingFormModal = () => {
toast.success(createTransferToLayingRes?.message as string);
router.push('/production/transfer-to-laying');
closeModalHandler(false);
successModal.openModal();
},
[router]
);
@@ -156,6 +165,7 @@ const TransferToLayingFormModal = () => {
toast.success(updateKandangRes?.message as string);
router.push('/production/transfer-to-laying');
closeModalHandler(false);
successModal.openModal();
},
[router]
);
@@ -187,6 +197,8 @@ const TransferToLayingFormModal = () => {
reason: values.reason as string,
};
setFormikLastValues(values);
switch (modalAction) {
case 'add':
await createTransferToLayingHandler(transferToLayingPayload);
@@ -244,7 +256,7 @@ const TransferToLayingFormModal = () => {
? selectedFlockSourceRawData.kandangs.map((kandang) => {
const availability =
flockSourceKandangsAvailability[kandang.project_flock_kandang_id]
.available_qty;
?.available_qty;
return {
kandang_name: kandang.name,
@@ -375,6 +387,12 @@ const TransferToLayingFormModal = () => {
formik.setValues(filledInitialValues);
setStep(3);
}
if (isResponseError(transferToLaying)) {
router.push('/production/transfer-to-laying');
closeModalHandler();
toast.error(transferToLaying.message);
}
};
const getFlockSourceData = async () => {
@@ -437,7 +455,7 @@ const TransferToLayingFormModal = () => {
<div className='w-px border-none bg-base-content/10' />
<h4 className='text-sm font-medium text-base-content/50'>
Add Transfer to Laying
{modalAction === 'add' ? 'Add' : 'Edit'} Transfer to Laying
</h4>
</div>
@@ -771,7 +789,12 @@ const TransferToLayingFormModal = () => {
<div className='flex flex-col gap-3'>
{formik.values.flockSourceKandangs.map((item, index) => {
const isInvalid =
item.quantity === ''
!Boolean(
getIn(
formik.touched,
`flockSourceKandangs[${index}].quantity`
)
) && item.quantity === ''
? false
: Boolean(
getIn(
@@ -833,7 +856,8 @@ const TransferToLayingFormModal = () => {
: 'neutral'
}
text={`Sisa transfer: ${formatNumber(
totalAvailableChickenForTransfer
totalAvailableChickenForTransfer,
'en-US'
)} ekor`}
className={{
badge: 'text-nowrap',
@@ -852,7 +876,12 @@ const TransferToLayingFormModal = () => {
{formik.values.flockDestinationKandangs.map(
(item, index) => {
const isInvalid =
item.quantity === ''
!Boolean(
getIn(
formik.touched,
`flockDestinationKandangs.${index}.quantity`
)
) && item.quantity === ''
? false
: Boolean(
getIn(
@@ -968,6 +997,16 @@ const TransferToLayingFormModal = () => {
)}
</form>
</Modal>
<TransferToLayingConfirmationModal
ref={successModal.ref}
type='success'
text='Data Berhasil Ditambahkan'
subtitleText='Data transfer to laying telah berhasil disimpan.'
transferToLayingForm={formikLastValues}
onClose={() => setFormikLastValues(undefined)}
secondaryButton={undefined}
/>
</>
);
};
@@ -14,9 +14,7 @@ import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Button from '@/components/Button';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission';
import PopoverButton from '@/components/popover/PopoverButton';
import Badge from '@/components/Badge';
@@ -24,13 +22,14 @@ import PopoverContent from '@/components/popover/PopoverContent';
import Dropdown from '@/components/Dropdown';
import StatusBadge from '@/components/helper/StatusBadge';
import TransferToLayingFilterModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFilterModal';
import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal';
import {
TransferToLaying,
TransferToLayingFilter,
} from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { cn, formatDate } from '@/lib/helper';
import { cn, formatDate, formatNumber } from '@/lib/helper';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Color } from '@/types/theme';
@@ -38,14 +37,11 @@ import { Color } from '@/types/theme';
const RowOptionsMenu = ({
props,
popoverPosition = 'bottom',
approveClickHandler,
rejectClickHandler,
deleteClickHandler,
}: {
props: CellContext<TransferToLaying, unknown>;
popoverPosition: 'bottom' | 'top';
approveClickHandler: () => void;
rejectClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
const showEditButton =
@@ -54,9 +50,6 @@ const RowOptionsMenu = ({
const showDeleteButton = showEditButton;
// const showApproveButton = showEditButton;
// const showRejectButton = showEditButton;
const popoverId = `transferToLaying#${props.row.original.id}`;
const popoverAnchorName = `--anchor-transferToLaying#${props.row.original.id}`;
@@ -260,7 +253,14 @@ const TransferToLayingsTable = () => {
{
accessorKey: 'usage_qty',
header: 'Kuantitas',
cell: (props) => props.getValue() ?? props.row.original.pending_usage_qty,
cell: (props) => {
const totalQuantity = props.row.original.targets.reduce(
(total, target) => total + target.qty,
0
);
return formatNumber(totalQuantity, 'en-US');
},
},
{
accessorKey: 'notes',
@@ -304,38 +304,20 @@ const TransferToLayingsTable = () => {
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const approveClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
approveModal.openModal();
};
const rejectClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
rejectModal.openModal();
};
const deleteClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
// Set row selection
setRowSelection({
[String(props.row.original.id)]: true,
});
deleteModal.openModal();
};
return (
<RowOptionsMenu
props={props}
approveClickHandler={approveClickHandler}
rejectClickHandler={rejectClickHandler}
deleteClickHandler={deleteClickHandler}
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
/>
@@ -377,6 +359,8 @@ const TransferToLayingsTable = () => {
refreshTransferToLayings();
setRowSelection({});
setSelectedTransferToLaying(undefined);
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
setIsDeleteLoading(false);
@@ -646,53 +630,70 @@ const TransferToLayingsTable = () => {
onReset={filterResetHandler}
/>
<ConfirmationModal
<TransferToLayingConfirmationModal
ref={deleteModal.ref}
iconPosition='left'
type='error'
text='Delete This Data?'
subtitleText='Are you sure you want to delete this data? '
transferToLayingIds={selectedRowIds}
primaryButton={{
isLoading: isDeleteLoading,
color: 'error',
onClick: confirmationModalDeleteClickHandler,
}}
secondaryButton={{
text: 'Cancel',
}}
primaryButton={{
text: 'Delete',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
color: 'none',
onClick: () => {
setRowSelection({});
deleteModal.closeModal();
},
}}
/>
<ConfirmationModalWithNotes
{/* Approve Modal */}
<TransferToLayingConfirmationModal
ref={approveModal.ref}
type='success'
iconPosition='left'
text='Approve This Submission?'
subtitleText='Are you sure you want to approve this submission?'
secondaryButton={{
text: 'Cancel',
}}
type='success'
transferToLayingIds={selectedRowIds}
withNote
noteLabel='Notes Approval'
primaryButton={{
text: 'Approve',
color: 'success',
isLoading: isApproveLoading,
onClick: confirmationModalApproveClickHandler,
}}
/>
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
iconPosition='left'
text='Reject This Submission?'
subtitleText='Are you sure you want to reject this submission?'
secondaryButton={{
text: 'Cancel',
color: 'none',
onClick: () => {
setRowSelection({});
approveModal.closeModal();
},
}}
/>
{/* Reject Modal */}
<TransferToLayingConfirmationModal
ref={rejectModal.ref}
type='error'
text='Reject This Submission?'
subtitleText='Are you sure you want to reject this submission?'
transferToLayingIds={selectedRowIds}
withNote
noteLabel='Notes Reject'
secondaryButton={{
text: 'Cancel',
color: 'none',
onClick: () => {
setRowSelection({});
rejectModal.closeModal();
},
}}
primaryButton={{
text: 'Reject',
color: 'error',
isLoading: isRejectLoading,
color: 'error',
onClick: confirmationModalRejectClickHandler,
}}
/>