fix(resolve): resolve MR

This commit is contained in:
rstubryan
2025-11-13 13:06:18 +07:00
16 changed files with 1075 additions and 1316 deletions
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
// TODO: delete dummy data
const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = {
id: 1,
transfer_date: '2025-10-14',
flock_source: {
id: 1,
name: 'Flock asal test',
},
flock_destination: {
id: 2,
name: 'Flock tujuan destination',
},
quantity: 10,
kandangs: [
{
kandang: {
id: 1,
name: 'Kandang test',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 8,
},
{
kandang: {
id: 1,
name: 'Kandang test 2',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 2,
},
],
reason: 'Test alasan',
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
};
const TransferToLayingEdit = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -114,33 +29,33 @@ const TransferToLayingEdit = () => {
);
}
// TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
(!transferToLaying ||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
(!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
}
if (
isResponseSuccess(transferToLaying) &&
transferToLaying.data.approval.step_number === 2
) {
router.replace('/production/transfer-to-laying');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' />
)}
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm
type='detail'
type='edit'
initialValues={transferToLaying.data}
/>
)} */}
{/* TODO: remove this dummy data and integrate to real API */}
<TransferToLayingForm
type='edit'
initialValues={DUMMY_TRANSFER_TO_LAYING_EDIT}
/>
)}
</div>
);
};
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
// TODO: delete dummy data
const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
id: 1,
transfer_date: '2025-10-14',
flock_source: {
id: 1,
name: 'Flock asal test',
},
flock_destination: {
id: 2,
name: 'Flock tujuan destination',
},
quantity: 10,
kandangs: [
{
kandang: {
id: 1,
name: 'Kandang test',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 8,
},
{
kandang: {
id: 1,
name: 'Kandang test 2',
status: 'ACTIVE',
location: {
id: 1,
name: 'test location',
address: 'test address 1',
area: { id: 1, name: 'test area 1' },
},
pic: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
},
quantity: 2,
},
],
reason: 'Test alasan',
created_user: {
id: 1,
id_user: 2,
email: 'test@gmail.com',
name: 'test',
},
created_at: '14-10-2025',
updated_at: '14-10-2025',
};
const TransferToLayingDetail = () => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -114,11 +29,9 @@ const TransferToLayingDetail = () => {
);
}
// TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
(!transferToLaying ||
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
(!transferToLaying || isResponseError(transferToLaying))
) {
router.replace('/404');
return;
@@ -129,18 +42,13 @@ const TransferToLayingDetail = () => {
{isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' />
)}
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm
type='detail'
initialValues={transferToLaying.data}
/>
)} */}
{/* TODO: remove this dummy data and integrate to real API */}
<TransferToLayingForm
type='detail'
initialValues={DUMMY_TRANSFER_TO_LAYING_DETAIL}
/>
)}
</div>
);
};
+7
View File
@@ -13,6 +13,7 @@ import {
FilterFn,
SortingState,
OnChangeFn,
Row,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react';
@@ -50,6 +51,7 @@ export interface TableProps<TData extends object> {
manualSorting?: boolean;
rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -90,6 +92,7 @@ const Table = <TData extends object>({
manualSorting = false,
rowSelection,
setRowSelection,
enableRowSelection,
}: TableProps<TData>) => {
const isServerSideTable =
totalItems !== undefined &&
@@ -150,6 +153,10 @@ const Table = <TData extends object>({
tableOptions.getRowId = (row) => (row as { id: string }).id;
}
if (enableRowSelection !== undefined) {
tableOptions.enableRowSelection = enableRowSelection;
}
const table = useReactTable(tableOptions);
const { setPageSize } = table;
+5 -1
View File
@@ -9,7 +9,7 @@ import Button from '@/components/Button';
import { cn } from '@/lib/helper';
import { Color } from '@/types/theme';
interface ConfirmationModalProps {
export interface ConfirmationModalProps {
ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error';
text?: string;
@@ -30,6 +30,7 @@ interface ConfirmationModalProps {
modal?: string;
modalBox?: string;
};
children?: React.ReactNode;
}
const ConfirmationModal = ({
@@ -40,6 +41,7 @@ const ConfirmationModal = ({
primaryButton,
secondaryButton,
className,
children,
}: ConfirmationModalProps) => {
const closeModalHandler = () => {
ref.current?.close();
@@ -90,6 +92,8 @@ const ConfirmationModal = ({
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
</p>
{children && <div className='w-full'>{children}</div>}
<div className='w-full flex flex-row gap-2'>
{secondaryButton && secondaryButton.text && (
<Button
@@ -0,0 +1,69 @@
'use client';
import { ChangeEventHandler, useId, useState } from 'react';
import ConfirmationModal, {
ConfirmationModalProps,
} from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea';
import { Color } from '@/types/theme';
interface ConfirmationModalWithNotesProps
extends Omit<ConfirmationModalProps, 'children' | 'primaryButton'> {
rows?: number;
placeholder?: string;
primaryButton?: {
text?: string;
color?: Color;
isLoading?: boolean;
onClick?: (notes: string) => void;
};
}
const ConfirmationModalWithNotes: React.FC<ConfirmationModalWithNotesProps> = ({
ref,
type = 'info',
text,
closeOnBackdrop,
primaryButton,
secondaryButton,
className,
rows = 3,
placeholder = 'Catatan...',
}) => {
const randomId = useId();
const [notes, setNotes] = useState('');
const notesChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setNotes(e.target.value);
};
return (
<ConfirmationModal
ref={ref}
type={type}
text={text}
closeOnBackdrop={closeOnBackdrop}
primaryButton={{
...primaryButton,
onClick: () => {
primaryButton?.onClick?.(notes);
},
}}
secondaryButton={secondaryButton}
className={className}
>
<TextArea
name={randomId}
placeholder={placeholder}
value={notes}
onChange={notesChangeHandler}
rows={rows}
/>
</ConfirmationModal>
);
};
export default ConfirmationModalWithNotes;
+8 -1
View File
@@ -130,10 +130,17 @@ export const formatGroupedApprovalsToApprovalSteps = (
if (!approvalGroup) {
const isWaiting = currentStepNumber === latestApproval.step_number + 1;
const isPreviousApprovalRejected =
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
'REJECTED';
return {
name: approvalLineItem.step_name,
status: isWaiting ? 'WAITING' : 'IDLE',
status: isPreviousApprovalRejected
? 'IDLE'
: isWaiting
? 'WAITING'
: 'IDLE',
};
}
@@ -2,7 +2,12 @@
import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr';
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
import {
CellContext,
ColumnDef,
Row,
SortingState,
} from '@tanstack/react-table';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
@@ -20,6 +25,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
@@ -29,6 +35,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ROWS_OPTIONS } from '@/config/constant';
import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data';
import PillBadge from '@/components/PillBadge';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -43,6 +50,16 @@ const RowOptionsMenu = ({
rejectClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
const showEditButton =
props.row.original.approval.action !== 'APPROVED' &&
props.row.original.approval.action !== 'REJECTED';
const showDeleteButton = showEditButton;
// TODO: apply RBAC
const showApproveButton = showEditButton;
const showRejectButton = showEditButton;
return (
<RowOptionsMenuWrapper type={type}>
<Button
@@ -55,50 +72,57 @@ const RowOptionsMenu = ({
Detail
</Button>
<Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
variant='ghost'
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='ghost'
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
{showEditButton && (
<Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
)}
{/* TODO: apply RBAC */}
{showApproveButton && (
<Button
variant='ghost'
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
)}
{showRejectButton && (
<Button
variant='ghost'
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
)}
{showDeleteButton && (
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
)}
</RowOptionsMenuWrapper>
);
};
@@ -187,17 +211,24 @@ const TransferToLayingsTable = () => {
/>
</div>
),
cell: ({ row }) => (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
),
cell: ({ row }) => {
const isCheckboxDisabled =
!row.getCanSelect() ||
row.original.approval.action === 'APPROVED' ||
row.original.approval.action === 'REJECTED';
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={isCheckboxDisabled}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
},
},
{
header: '#',
@@ -214,21 +245,55 @@ const TransferToLayingsTable = () => {
{
accessorKey: 'flock_source',
header: 'Flock Asal',
cell: (props) => props.row.original.flock_source.name,
cell: (props) => props.row.original.from_project_flock.flock_name,
},
{
accessorKey: 'flock_destination',
header: 'Flock Tujuan',
cell: (props) => props.row.original.flock_destination.name,
cell: (props) => props.row.original.to_project_flock.flock_name,
},
{
accessorKey: 'quantity',
accessorKey: 'usage_qty',
header: 'Kuantitas',
cell: (props) => props.getValue() ?? props.row.original.pending_usage_qty,
},
{
accessorKey: 'reason',
accessorKey: 'notes',
header: 'Alasan Transfer',
},
{
header: 'Status',
cell: (props) => {
const isLatestApprovalRejected =
props.row.original.approval.action === 'REJECTED';
let latestApprovalStepName = props.row.original.approval.step_name;
let pillBadgeColor: 'yellow' | 'green' | 'gray' | 'red' = 'gray';
switch (latestApprovalStepName.toLowerCase()) {
case 'pengajuan':
pillBadgeColor = 'yellow';
break;
case 'disetujui':
pillBadgeColor = 'green';
break;
}
if (isLatestApprovalRejected) {
pillBadgeColor = 'red';
latestApprovalStepName = 'Ditolak';
}
return (
<PillBadge
content={latestApprovalStepName}
color={pillBadgeColor}
className='text-sm'
/>
);
},
},
{
header: 'Aksi',
cell: (props) => {
@@ -237,7 +302,7 @@ const TransferToLayingsTable = () => {
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
const approveClickHandler = () => {
setSelectedTransferToLaying(props.row.original);
@@ -268,7 +333,7 @@ const TransferToLayingsTable = () => {
return (
<>
{currentPageSize > 2 && (
{currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
@@ -280,7 +345,7 @@ const TransferToLayingsTable = () => {
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
{currentPageSize <= 3 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
@@ -297,6 +362,15 @@ const TransferToLayingsTable = () => {
},
];
const tableEnableRowSelectionHandler: (
row: Row<TransferToLaying>
) => boolean = (row) => {
return (
row.original.approval.action !== 'APPROVED' &&
row.original.approval.action !== 'REJECTED'
);
};
const bulkApproveClickHandler = () => {
approveModal.openModal();
};
@@ -309,27 +383,31 @@ const TransferToLayingsTable = () => {
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
refreshTransferToLayings();
try {
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
setIsDeleteLoading(false);
toast.success('Berhasil menghapus data transfer ke laying!');
refreshTransferToLayings();
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false);
}
};
const confirmationModalApproveClickHandler = async () => {
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true);
const bulkApproveResponse =
await TransferToLayingApi.bulkApprove(selectedRowIds);
const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings();
approveModal.closeModal();
// TODO: remove console.log
console.log('Approved data:', selectedRowIds);
toast.success(
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
);
@@ -346,19 +424,18 @@ const TransferToLayingsTable = () => {
setIsApproveLoading(false);
};
const confirmationModalRejectClickHandler = async () => {
const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true);
const bulkRejectResponse =
await TransferToLayingApi.bulkReject(selectedRowIds);
const bulkRejectResponse = await TransferToLayingApi.bulkReject(
selectedRowIds,
notes
);
if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings();
rejectModal.closeModal();
// TODO: remove console.log
console.log('Rejected data:', selectedRowIds);
toast.success(
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
);
@@ -559,6 +636,7 @@ const TransferToLayingsTable = () => {
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
className={{
containerClassName: cn({
'mb-20':
@@ -592,7 +670,7 @@ const TransferToLayingsTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
text={`Apakah anda yakin ingin approve data transfer ke laying ini (${selectedRowIds.length} data)?`}
@@ -607,7 +685,7 @@ const TransferToLayingsTable = () => {
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
@@ -1,4 +1,7 @@
import * as Yup from 'yup';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { formatDate } from '@/lib/helper';
type TransferToLayingFormSchemaType = {
transfer_date?: string;
@@ -14,7 +17,7 @@ type TransferToLayingFormSchemaType = {
totalQuantity?: number;
maxTotalQuantity?: number; // original cap (hidden), helper
kandangs: {
flockSourceKandangs: {
kandang: {
value: number;
label: string;
@@ -22,6 +25,16 @@ type TransferToLayingFormSchemaType = {
quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper
}[];
flockDestinationKandangs: {
kandang: {
value: number;
label: string;
};
quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper
}[];
reason?: string;
};
@@ -51,7 +64,29 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
.min(1, 'Jumlah transfer minimal 1')
.required('Jumlah transfer wajib diisi!'),
kandangs: Yup.array()
flockSourceKandangs: Yup.array()
.of(
Yup.object({
kandang: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).required('Kandang wajib diisi!'),
quantity: Yup.number()
.min(0, 'Kuantitas minimal 0!')
.max(
Yup.ref('maxQuantity'),
({ max }) => `Kuantitas maksimal ${max}!`
)
.required('Kuantitas wajib diisi!'),
maxQuantity: Yup.number().min(1).required(), // internal helper field
})
)
.min(1, 'Minimal 1 kandang terisi!')
.required('Kandang wajib diisi!'),
flockDestinationKandangs: Yup.array()
.of(
Yup.object({
kandang: Yup.object({
@@ -81,3 +116,122 @@ export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
export type TransferToLayingFormValues = Yup.InferType<
typeof TransferToLayingFormSchema
>;
export const getTransferToLayingFormInitialValues = (
initialValues?: TransferToLaying
): TransferToLayingFormValues => {
return {
transfer_date: initialValues?.transfer_date
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
: '',
flockSource: initialValues?.from_project_flock
? {
value: initialValues?.from_project_flock.id,
label: initialValues?.from_project_flock.flock_name,
}
: undefined,
flockDestination: initialValues?.to_project_flock
? {
value: initialValues?.to_project_flock.id,
label: initialValues?.to_project_flock.flock_name,
}
: undefined,
totalQuantity:
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? undefined,
flockSourceKandangs: initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({
kandang: {
value: sourceKandang.source_project_flock_kandang.kandang.id,
label: sourceKandang.source_project_flock_kandang.kandang.name,
},
quantity: sourceKandang.qty,
}))
: [],
flockDestinationKandangs: initialValues?.targets
? initialValues.targets.map((targetKandang) => ({
kandang: {
value: targetKandang.target_project_flock_kandang.kandang.id,
label: targetKandang.target_project_flock_kandang.kandang.name,
},
quantity: targetKandang.qty,
}))
: [],
reason: initialValues?.notes ?? undefined,
};
};
export const getFilledTransferToLayingFormInitialValues = async (
initialValues?: TransferToLaying
): Promise<TransferToLayingFormValues> => {
const mappedFlockSourceKandangsAvailableQty =
await TransferToLayingApi.getMappedFlockKandangsAvailability(
initialValues?.from_project_flock.id as number
);
const formattedFlockSourceKandangs = initialValues?.sources
? initialValues.sources.map((sourceKandang) => ({
kandang: {
value: sourceKandang.source_project_flock_kandang.kandang.id,
label: sourceKandang.source_project_flock_kandang.kandang.name,
},
quantity: sourceKandang.qty,
maxQuantity:
(mappedFlockSourceKandangsAvailableQty &&
mappedFlockSourceKandangsAvailableQty[
sourceKandang.source_project_flock_kandang.id
].available_qty) ??
0,
}))
: [];
let maxTotalQuantity = 0;
formattedFlockSourceKandangs.forEach((item) => {
maxTotalQuantity += item.maxQuantity;
});
return {
transfer_date: initialValues?.transfer_date
? formatDate(initialValues.transfer_date, 'YYYY-MM-DD')
: '',
flockSource: initialValues?.from_project_flock
? {
value: initialValues?.from_project_flock.id,
label: initialValues?.from_project_flock.flock_name,
}
: undefined,
flockDestination: initialValues?.to_project_flock
? {
value: initialValues?.to_project_flock.id,
label: initialValues?.to_project_flock.flock_name,
}
: undefined,
totalQuantity:
initialValues?.usage_qty ?? initialValues?.pending_usage_qty ?? undefined,
maxTotalQuantity: maxTotalQuantity,
flockSourceKandangs: formattedFlockSourceKandangs,
flockDestinationKandangs: initialValues?.targets
? initialValues.targets.map((targetKandang) => ({
kandang: {
value: targetKandang.target_project_flock_kandang.kandang.id,
label: targetKandang.target_project_flock_kandang.kandang.name,
},
quantity: targetKandang.qty,
// maxQuantity:
// targetKandang.target_project_flock_kandang.kandang.capacity,
// TODO: integrate this to real API kandang capacity
maxQuantity:
targetKandang.target_project_flock_kandang.kandang.capacity ??
Infinity,
}))
: [],
reason: initialValues?.notes ?? undefined,
};
};
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useFormik } from 'formik';
import { toast } from 'react-hot-toast';
@@ -8,16 +8,23 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import SelectInput, {
OptionType,
// useSelect,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import ApprovalSteps, {
formatGroupedApprovalsToApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import {
getFilledTransferToLayingFormInitialValues,
getTransferToLayingFormInitialValues,
TransferToLayingFormSchema,
TransferToLayingFormValues,
UpdateTransferToLayingFormSchema,
@@ -31,6 +38,8 @@ import {
import { cn } from '@/lib/helper';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { TRANSFER_TO_LAYING_APPROVAL_LINE } from '@/config/approval-line';
interface TransferToLayingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -55,11 +64,23 @@ const TransferToLayingForm = ({
const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = useState(false);
const { data: approvalHistory, isLoading: isLoadingApprovalHistory } = useSWR(
type === 'detail' && initialValues ? [String(initialValues.id)] : null,
([id]: string[]) => TransferToLayingApi.getApprovalHistory(Number(id))
);
const createTransferToLayingHandler = useCallback(
async (payload: CreateTransferToLayingPayload) => {
console.log('Create transfer to laying:', { payload });
const createTransferToLayingRes =
await TransferToLayingApi.create(payload);
toast.success('Berhasil menambahkan data transfer ke laying!');
if (isResponseError(createTransferToLayingRes)) {
setFormErrorMessage(createTransferToLayingRes.message);
return;
}
toast.success(createTransferToLayingRes?.message as string);
router.push('/production/transfer-to-laying');
},
[router]
);
@@ -69,46 +90,30 @@ const TransferToLayingForm = ({
transferToLayingId: number,
payload: UpdateTransferToLayingPayload
) => {
console.log(
`Update transfer to laying with ID of ${transferToLayingId}:`,
{ payload }
const updateKandangRes = await TransferToLayingApi.update(
transferToLayingId,
payload
);
toast.success('Berhasil mengubah data transfer ke laying!');
if (updateKandangRes?.status === 'error') {
setFormErrorMessage(updateKandangRes.message);
return;
}
toast.success(updateKandangRes?.message as string);
router.refresh();
router.push('/production/transfer-to-laying');
},
[router]
);
const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
return {
transfer_date: initialValues?.transfer_date ?? '',
flockSource: initialValues?.flock_source
? {
value: initialValues?.flock_source.id,
label: initialValues?.flock_source.name,
}
: undefined,
flockDestination: initialValues?.flock_destination
? {
value: initialValues?.flock_destination.id,
label: initialValues?.flock_destination.name,
}
: undefined,
totalQuantity: initialValues?.quantity ?? undefined,
// const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
// return getTransferToLayingFormInitialValues(initialValues);
// }, [initialValues]);
kandangs: initialValues?.kandangs
? initialValues.kandangs.map((kandang) => ({
kandang: {
value: kandang.kandang.id,
label: kandang.kandang.name,
},
quantity: kandang.quantity,
}))
: [],
reason: initialValues?.reason ?? undefined,
};
}, [initialValues]);
const [formikInitialValues, setFormikInitialValues] = useState(
getTransferToLayingFormInitialValues()
);
const formik = useFormik<TransferToLayingFormValues>({
initialValues: formikInitialValues,
@@ -117,23 +122,23 @@ const TransferToLayingForm = ({
? UpdateTransferToLayingFormSchema
: TransferToLayingFormSchema,
onSubmit: async (values) => {
console.log({ values });
setFormErrorMessage('');
const transferToLayingPayload: CreateTransferToLayingPayload = {
transfer_date: values.transfer_date as string,
flock_source_id: values.flockSource?.value as number,
flock_destination_id: values.flockDestination?.value as number,
source_project_flock_id: values.flockSource?.value as number,
target_project_flock_id: values.flockDestination?.value as number,
totalQuantity: values.totalQuantity as number,
kandangs: values.kandangs?.map((kandang) => ({
kandang_id: kandang.kandang.value,
quantity: kandang.quantity,
})) as {
kandang_id: number;
quantity: number;
}[],
source_kandangs: values.flockSourceKandangs?.map((kandang) => ({
project_flock_kandang_id: kandang.kandang.value,
quantity: parseFloat(kandang.quantity as string),
})) as CreateTransferToLayingPayload['source_kandangs'],
target_kandangs: values.flockDestinationKandangs?.map((kandang) => ({
project_flock_kandang_id: kandang.kandang.value,
quantity: parseFloat(kandang.quantity as string),
})) as CreateTransferToLayingPayload['target_kandangs'],
reason: values.reason as string,
};
@@ -154,7 +159,11 @@ const TransferToLayingForm = ({
});
const { setValues: formikSetValues, values: formikValues } = formik;
const { kandangs: kandangsValue } = formikValues;
const {
flockSourceKandangs: flockSourceKandangsValue,
flockDestinationKandangs: flockDestinationKandangsValue,
totalQuantity,
} = formikValues;
const deleteTransferToLayingClickHandler = () => {
deleteModal.openModal();
@@ -172,24 +181,32 @@ const TransferToLayingForm = ({
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
// TODO: delete data and integrate to real API
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!');
try {
await TransferToLayingApi.delete(initialValues?.id as number);
setIsDeleteLoading(false);
toast.success('Berhasil menghapus data transfer ke laying!');
router.push('/production/transfer-to-laying');
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false);
}
};
const confirmationModalApproveClickHandler = async () => {
const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true);
const approveResponse = await TransferToLayingApi.approve(
initialValues?.id as number
initialValues?.id as number,
notes
);
if (isResponseSuccess(approveResponse)) {
approveModal.closeModal();
toast.success('Berhasil approve data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else {
approveModal.closeModal();
@@ -199,17 +216,19 @@ const TransferToLayingForm = ({
setIsApproveLoading(false);
};
const confirmationModalRejectClickHandler = async () => {
const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true);
const rejectResponse = await TransferToLayingApi.reject(
initialValues?.id as number
initialValues?.id as number,
notes
);
if (isResponseSuccess(rejectResponse)) {
rejectModal.closeModal();
toast.success('Berhasil reject data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else {
rejectModal.closeModal();
@@ -219,49 +238,47 @@ const TransferToLayingForm = ({
setIsRejectLoading(false);
};
const isRepeaterInputError = (
column: keyof TransferToLayingFormValues['kandangs'][0],
// flock source
const isFlockSourceKandangsRepeaterInputError = (
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number
) => {
return (
formik.touched.kandangs?.[idx]?.[column] &&
formik.touched.flockSourceKandangs?.[idx]?.[column] &&
Boolean(
formik.errors.kandangs?.[idx] instanceof Object &&
formik.errors.kandangs?.[idx]?.[column]
formik.errors.flockSourceKandangs?.[idx] instanceof Object &&
formik.errors.flockSourceKandangs?.[idx]?.[column]
)
);
};
const repeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['kandangs'][0],
const flockSourceKandangsRepeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number
) => {
return (formik.errors.kandangs?.[idx] as Record<string, string>)?.[column];
return (
formik.errors.flockSourceKandangs?.[idx] as Record<string, string>
)?.[column];
};
// TODO: remove dummy data and use real data
// Flock Source
// const {
// inputValue: flockSourceInputValue,
// setInputValue: setFlockSourceInputValue,
// options: flockSourceOptions,
// isLoadingOptions: isLoadingFlockSourceOptions,
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-source', 'id', 'name');
// TODO: remove this dummy data
const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR(
'test',
() => TransferToLayingApi.getFlockSource()
const {
setInputValue: setFlockSourceInputValue,
options: flockSourceOptions,
isLoadingOptions: isLoadingFlockSourceOptions,
rawData: flockSources,
} = useSelect<ProjectFlock>(
'/production/project-flocks',
'id',
'flock_name',
'search',
{
category: 'GROWING',
}
);
const flockSourceOptions = isResponseSuccess(flockSources)
? flockSources?.data.map((flockSource) => ({
value: flockSource.id,
label: flockSource.name,
}))
: [];
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
const flockSourceChangeHandler = async (
val: OptionType | OptionType[] | null
) => {
// Get flock source data for total quantity and kandang
const flockSource =
isResponseSuccess(flockSources) && val !== null
@@ -272,21 +289,38 @@ const TransferToLayingForm = ({
// Set total quantity and kandangs
if (flockSource) {
const mappedFlockKandangsAvailableQty =
await TransferToLayingApi.getMappedFlockKandangsAvailability(
flockSource.id
);
const formattedKandangs = flockSource.kandangs.map((item) => ({
kandang: {
value: item.kandang.id,
label: item.kandang.name,
value: item.project_flock_kandang_id,
label: item.name,
},
quantity: '',
maxQuantity: item.quantity,
maxQuantity:
(mappedFlockKandangsAvailableQty &&
mappedFlockKandangsAvailableQty[item.project_flock_kandang_id]
.available_qty) ??
0,
}));
formik.setFieldValue('totalQuantity', flockSource.totalQuantity);
formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity);
formik.setFieldValue('kandangs', formattedKandangs);
let maxTotalQuantity = 0;
// flockSource.kandangs.forEach((item) => {
// maxTotalQuantity += item.capacity;
// });
formattedKandangs.forEach((item) => {
maxTotalQuantity += item.maxQuantity;
});
formik.setFieldValue('totalQuantity', '');
formik.setFieldValue('maxTotalQuantity', maxTotalQuantity);
formik.setFieldValue('flockSourceKandangs', formattedKandangs);
} else {
formik.setFieldValue('totalQuantity', undefined);
formik.setFieldValue('kandangs', undefined);
formik.setFieldValue('flockSourceKandangs', undefined);
formik.setFieldValue('reason', '');
}
@@ -294,52 +328,137 @@ const TransferToLayingForm = ({
formik.setFieldValue('flockSource', val);
};
// TODO: remove dummy data and use real data
// Flock Destination
// const {
// inputValue: flockDestinationInputValue,
// setInputValue: setFlockDestinationInputValue,
// options: flockDestinationOptions,
// isLoadingOptions: isLoadingFlockDestinationOptions,
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-destination', 'id', 'name');
// flock destination
const isFlockDestinationKandangsRepeaterInputError = (
column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
idx: number
) => {
return (
formik.touched.flockDestinationKandangs?.[idx]?.[column] &&
Boolean(
formik.errors.flockDestinationKandangs?.[idx] instanceof Object &&
formik.errors.flockDestinationKandangs?.[idx]?.[column]
)
);
};
const flockDestinationKandangsRepeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
idx: number
) => {
return (
formik.errors.flockDestinationKandangs?.[idx] as Record<string, string>
)?.[column];
};
// TODO: remove this dummy data
const {
data: flockDestinations,
isLoading: isLoadingFlockDestinationOptions,
} = useSWR('test', () => TransferToLayingApi.getFlockSource());
const flockDestinationOptions = isResponseSuccess(flockDestinations)
? flockDestinations?.data.map((flockDestination) => ({
value: flockDestination.id,
label: flockDestination.name,
}))
: [];
setInputValue: setFlockDestinationInputValue,
options: flockDestinationOptions,
isLoadingOptions: isLoadingFlockDestinationOptions,
rawData: flockDestinations,
} = useSelect<ProjectFlock>(
'/production/project-flocks',
'id',
'flock_name',
'search',
{
category: 'LAYING',
}
);
const flockDestinationChangeHandler = (
val: OptionType | OptionType[] | null
) => {
// Get flock destination data for total quantity and kandang
const flockDestination =
isResponseSuccess(flockDestinations) && val !== null
? flockDestinations.data.find(
(item) => item.id === (val as OptionType).value
)
: undefined;
// Set total quantity and kandangs
if (flockDestination) {
const formattedKandangs = flockDestination.kandangs.map((item) => ({
kandang: {
value: item.project_flock_kandang_id,
label: item.name,
},
quantity: '',
// TODO: integrate this later to real kandang capacity API
// maxQuantity: item.capacity ?? 0,
maxQuantity: item.capacity ?? Infinity,
}));
formik.setFieldValue('flockDestinationKandangs', formattedKandangs);
}
formik.setFieldTouched('flockDestination', true);
formik.setFieldValue('flockDestination', val);
};
const isShowApproveRejectButton =
initialValues &&
initialValues?.approval?.step_number === 1 &&
initialValues?.approval.action !== 'REJECTED';
const isShowDeleteButton =
initialValues &&
initialValues?.approval.action !== 'REJECTED' &&
initialValues?.approval.action !== 'APPROVED';
const isShowEditButton = isShowDeleteButton;
useEffect(() => {
const getFilledInitialValues = async () => {
if (initialValues) {
const filledInitialValues =
await getFilledTransferToLayingFormInitialValues(initialValues);
setFormikInitialValues(filledInitialValues);
}
};
getFilledInitialValues();
}, [initialValues, setFormikInitialValues]);
useEffect(() => {
formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]);
useEffect(() => {
// calculate total quantity if kandangs quantity change
if (kandangsValue && kandangsValue.length > 0) {
if (flockSourceKandangsValue && flockSourceKandangsValue.length > 0) {
let newTotalQuantity = 0;
kandangsValue.forEach((item) => {
newTotalQuantity += item.quantity as number;
flockSourceKandangsValue.forEach((item) => {
newTotalQuantity += parseFloat(item.quantity as string);
});
formik.setFieldValue('totalQuantity', newTotalQuantity);
formik.validateField('totalQuantity');
}
}, [formikSetValues, kandangsValue]);
}, [formikSetValues, flockSourceKandangsValue]);
useEffect(() => {
// calculate total quantity if kandangs quantity change
if (
flockDestinationKandangsValue &&
flockDestinationKandangsValue.length > 0
) {
let destinationKandangsTotalQuantity = 0;
flockDestinationKandangsValue.forEach((item) => {
destinationKandangsTotalQuantity += parseFloat(item.quantity as string);
});
if (
destinationKandangsTotalQuantity > parseFloat(String(totalQuantity))
) {
}
}
}, [formikSetValues, flockDestinationKandangsValue]);
return (
<>
@@ -361,30 +480,56 @@ const TransferToLayingForm = ({
</h1>
</header>
<div className='w-full my-4 flex flex-row justify-end gap-2'>
{type === 'detail' &&
initialValues &&
!isLoadingApprovalHistory &&
isResponseSuccess(approvalHistory) && (
<div className='w-full my-4'>
<ApprovalSteps
approvals={formatGroupedApprovalsToApprovalSteps(
TRANSFER_TO_LAYING_APPROVAL_LINE,
approvalHistory.data,
initialValues.approval
)}
/>
</div>
)}
<div className='w-full my-4 flex flex-row justify-between gap-2'>
{type === 'detail' && (
<>
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
{isShowApproveRejectButton && (
<div className='w-full flex flex-row justify-end gap-2'>
{/* TODO: apply RBAC */}
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</div>
)}
</>
)}
</div>
@@ -395,13 +540,12 @@ const TransferToLayingForm = ({
className='w-full flex flex-col gap-6'
>
<div className='flex flex-col gap-4'>
<TextInput
<DateInput
required
type='date'
label='Tanggal Transfer'
name='transfer_date'
placeholder='Masukkan tanggal transfer'
value={formik.values.transfer_date}
value={formik.values.transfer_date ?? ''}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
@@ -421,7 +565,7 @@ const TransferToLayingForm = ({
options={flockSourceOptions}
onChange={flockSourceChangeHandler}
isLoading={isLoadingFlockSourceOptions}
// onInputChange={setFlockSourceInputValue}
onInputChange={setFlockSourceInputValue}
isError={
formik.touched.flockSource &&
Boolean(typeof formik.errors.flockSource === 'string')
@@ -439,7 +583,7 @@ const TransferToLayingForm = ({
options={flockDestinationOptions}
onChange={flockDestinationChangeHandler}
isLoading={isLoadingFlockDestinationOptions}
// onInputChange={setFlockDestinationInputValue}
onInputChange={setFlockDestinationInputValue}
isError={
formik.touched.flockDestination &&
Boolean(typeof formik.errors.flockDestination === 'string')
@@ -450,9 +594,8 @@ const TransferToLayingForm = ({
/>
</div>
<TextInput
<NumberInput
required
type='number'
name='totalQuantity'
label='Jumlah Transfer'
bottomLabel={
@@ -461,7 +604,9 @@ const TransferToLayingForm = ({
: undefined
}
placeholder='Masukkan jumlah transfer'
value={formik.values.totalQuantity ?? ''}
value={
formik.values.totalQuantity ? formik.values.totalQuantity : ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
@@ -469,24 +614,22 @@ const TransferToLayingForm = ({
Boolean(formik.errors.totalQuantity)
}
errorMessage={formik.errors.totalQuantity}
// readOnly={type === 'detail'}
// disabled={Boolean(formik.errors.flockSource)}
disabled
/>
<div>
<div className='flex flex-col gap-4'>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Kandang</th>
<th>Kandang Flock Asal</th>
<th>Kuantitas</th>
</tr>
</thead>
<tbody>
{(!formik.values.kandangs ||
formik.values.kandangs.length === 0) && (
{(!formik.values.flockSourceKandangs ||
formik.values.flockSourceKandangs.length === 0) && (
<tr>
<td colSpan={2}>
<p className='w-full text-center text-gray-400'>
@@ -496,8 +639,8 @@ const TransferToLayingForm = ({
</tr>
)}
{formik.values.kandangs &&
formik.values.kandangs.map((kandang, idx) => (
{formik.values.flockSourceKandangs &&
formik.values.flockSourceKandangs.map((kandang, idx) => (
<tr key={idx}>
<td>
<SelectInput
@@ -511,10 +654,9 @@ const TransferToLayingForm = ({
</td>
<td>
<TextInput
<NumberInput
required
type='number'
name={`kandangs[${idx}].quantity`}
name={`flockSourceKandangs[${idx}].quantity`}
bottomLabel={
kandang.maxQuantity
? `Max: ${kandang.maxQuantity}`
@@ -524,8 +666,11 @@ const TransferToLayingForm = ({
value={kandang.quantity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isRepeaterInputError('quantity', idx)}
errorMessage={repeaterInputErrorMessage(
isError={isFlockSourceKandangsRepeaterInputError(
'quantity',
idx
)}
errorMessage={flockSourceKandangsRepeaterInputErrorMessage(
'quantity',
idx
)}
@@ -540,6 +685,76 @@ const TransferToLayingForm = ({
</tbody>
</table>
</div>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Kandang Flock Tujuan</th>
<th>Kuantitas</th>
</tr>
</thead>
<tbody>
{(!formik.values.flockDestinationKandangs ||
formik.values.flockDestinationKandangs.length === 0) && (
<tr>
<td colSpan={2}>
<p className='w-full text-center text-gray-400'>
Pilih flock tujuan terlebih dahulu!
</p>
</td>
</tr>
)}
{formik.values.flockDestinationKandangs &&
formik.values.flockDestinationKandangs.map(
(kandang, idx) => (
<tr key={idx}>
<td>
<SelectInput
value={kandang.kandang}
options={[]}
isDisabled
className={{
wrapper: 'min-w-52',
}}
/>
</td>
<td>
<NumberInput
required
name={`flockDestinationKandangs[${idx}].quantity`}
bottomLabel={
kandang.maxQuantity
? `Max: ${kandang.maxQuantity}`
: undefined
}
placeholder='Masukkan kuantitas'
value={kandang.quantity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isFlockDestinationKandangsRepeaterInputError(
'quantity',
idx
)}
errorMessage={flockDestinationKandangsRepeaterInputErrorMessage(
'quantity',
idx
)}
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-52',
}}
/>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
<TextArea
@@ -558,25 +773,38 @@ const TransferToLayingForm = ({
/>
</div>
{formErrorMessage && (
<div role='alert' className='alert alert-error w-full'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteTransferToLayingClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{isShowDeleteButton && (
<Button
type='button'
color='error'
onClick={deleteTransferToLayingClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
)}
{type !== 'edit' && (
{type !== 'edit' && isShowEditButton && (
<Button
type='button'
color='warning'
@@ -617,17 +845,6 @@ const TransferToLayingForm = ({
</div>
)}
</div>
{formErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{formErrorMessage}</span>
</div>
)}
</form>
</section>
@@ -650,7 +867,7 @@ const TransferToLayingForm = ({
{type === 'detail' && (
<>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={approveModal.ref}
type='success'
text='Apakah anda yakin ingin approve data transfer ke laying ini?'
@@ -665,7 +882,7 @@ const TransferToLayingForm = ({
}}
/>
<ConfirmationModal
<ConfirmationModalWithNotes
ref={rejectModal.ref}
type='error'
text='Apakah anda yakin ingin reject data transfer ke laying ini?'
+11
View File
@@ -11,6 +11,17 @@ export const PROJECT_FLOCK_APPROVAL_LINE: ApprovalLine = [
},
] as const;
export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
step_name: 'Pengajuan',
},
{
step_number: 2,
step_name: 'Disetujui',
},
] as const;
export const PURCHASE_ORDER_APPROVAL_LINE: ApprovalLine = [
{
step_number: 1,
+1 -1
View File
@@ -19,7 +19,7 @@ export const ProjectFlockApi = new BaseApiService<
ProjectFlock,
CreateProjectFlockPayload,
UpdateProjectFlockPayload
>('/production/project_flocks');
>('/production/project-flocks');
export const RecordingApi = new BaseApiService<
Recording,
CreateRecordingPayload,
File diff suppressed because it is too large Load Diff
-12
View File
@@ -12,15 +12,3 @@ export type CreateFlockPayload = {
};
export type UpdateFlockPayload = CreateFlockPayload;
// ---------------------------------------
// TODO: adjust this later after Transfer to Laying API done
import { BaseKandang } from '@/types/api/master-data/kandang';
export type FlockWithKandangs = BaseFlock & {
totalQuantity: number;
kandangs: {
kandang: BaseKandang;
quantity: number;
}[];
};
+1
View File
@@ -8,6 +8,7 @@ export type BaseKandang = {
status: string;
location: BaseLocation;
pic: BaseUser;
capacity: number;
};
export type Kandang = BaseMetadata & BaseKandang;
+14 -1
View File
@@ -8,6 +8,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = {
id: number;
name: string;
flock_name: string;
status: string;
flock: Flock;
flock_id: number;
@@ -20,7 +21,9 @@ export type BaseProjectFlock = {
location_id: number;
period: number;
kandang_ids: number[];
kandangs: Kandang[];
kandangs: (Kandang & {
project_flock_kandang_id: number;
})[];
approval: BaseApproval;
};
@@ -47,3 +50,13 @@ export type ProjectFlockApprovalPayload = {
action: 'APPROVED' | 'REJECTED';
approvable_ids: number[];
};
export type ProjectFlockAvailableQuantity = {
project_flock_id: number;
flock_name: string;
category: 'LAYING' | 'GROWING';
kandangs: {
project_flock_kandang_id: number;
available_qty: number;
}[];
};
+69 -15
View File
@@ -1,34 +1,88 @@
import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general';
import { Kandang } from '@/types/api/master-data/kandang';
import {
BaseApiResponse,
BaseMetadata,
CreatedUser,
flags,
} from '@/types/api/api-general';
import { BaseKandang, Kandang } from '@/types/api/master-data/kandang';
import { WarehouseType } from '@/types/api/master-data/warehouse';
export type BaseTransferToLaying = {
id: number;
transfer_number: string;
transfer_date: string;
flock_source: {
notes: string;
from_project_flock: {
id: number;
name: string;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
flock_destination: {
to_project_flock: {
id: number;
name: string;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
quantity: number;
kandangs: {
kandang: Kandang;
quantity: number;
pending_usage_qty: number | null;
usage_qty: number | null;
sources: {
source_project_flock_kandang: {
id: number;
kandang: Omit<BaseKandang, 'status' | 'location' | 'pic'>;
};
qty: number;
product_warehouse: {
product: {
id: number;
name: string;
};
warehouse: {
id: number;
name: string;
type: WarehouseType;
};
};
}[];
reason: string;
targets: {
target_project_flock_kandang: {
id: number;
kandang: Omit<BaseKandang, 'status' | 'location' | 'pic'>;
};
qty: number;
product_warehouse: {
product: {
id: number;
name: string;
};
warehouse: {
id: number;
name: string;
type: WarehouseType;
};
};
}[];
created_by: number;
created_user: CreatedUser;
created_at: string;
approval: BaseApproval;
};
export type TransferToLaying = BaseMetadata & BaseTransferToLaying;
export type CreateTransferToLayingPayload = {
transfer_date: string;
flock_source_id: number;
flock_destination_id: number;
source_project_flock_id: number;
target_project_flock_id: number;
totalQuantity: number;
kandangs: {
kandang_id: number;
source_kandangs: {
project_flock_kandang_id: number;
quantity: number;
}[];
target_kandangs: {
project_flock_kandang_id: number;
quantity: number;
}[];
reason: string;