chore: prettier format

This commit is contained in:
rstubryan
2025-11-13 14:28:46 +07:00
21 changed files with 1418 additions and 1410 deletions
+1
View File
@@ -1,2 +1,3 @@
npm run format
npm run lint npm run lint
npm run build npm run build
+44
View File
@@ -16,6 +16,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
@@ -194,6 +195,12 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
@@ -2868,6 +2875,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -5741,6 +5764,27 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-day-picker": {
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+1
View File
@@ -19,6 +19,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.5.3", "next": "15.5.3",
"react": "19.1.0", "react": "19.1.0",
"react-day-picker": "^9.11.1",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4", "react-number-format": "^5.4.4",
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; 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 TransferToLayingEdit = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -114,33 +29,33 @@ const TransferToLayingEdit = () => {
); );
} }
// TODO: remove dummy data and integrate with real API
if ( if (
!isLoadingTransferToLaying && !isLoadingTransferToLaying &&
(!transferToLaying || (!transferToLaying || isResponseError(transferToLaying))
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
) { ) {
router.replace('/404'); router.replace('/404');
return; return;
} }
if (
isResponseSuccess(transferToLaying) &&
transferToLaying.data.approval.step_number === 2
) {
router.replace('/production/transfer-to-laying');
return;
}
return ( return (
<div className='w-full p-4 flex flex-row justify-center'> <div className='w-full p-4 flex flex-row justify-center'>
{isLoadingTransferToLaying && ( {isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' /> <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 <TransferToLayingForm
type='edit' type='edit'
initialValues={DUMMY_TRANSFER_TO_LAYING_EDIT} initialValues={transferToLaying.data}
/> />
)}
</div> </div>
); );
}; };
@@ -8,91 +8,6 @@ import TransferToLayingForm from '@/components/pages/production/transfer-to-layi
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; 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 TransferToLayingDetail = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -114,11 +29,9 @@ const TransferToLayingDetail = () => {
); );
} }
// TODO: remove dummy data and integrate with real API
if ( if (
!isLoadingTransferToLaying && !isLoadingTransferToLaying &&
(!transferToLaying || (!transferToLaying || isResponseError(transferToLaying))
(isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
) { ) {
router.replace('/404'); router.replace('/404');
return; return;
@@ -129,18 +42,13 @@ const TransferToLayingDetail = () => {
{isLoadingTransferToLaying && ( {isLoadingTransferToLaying && (
<span className='loading loading-spinner loading-xl' /> <span className='loading loading-spinner loading-xl' />
)} )}
{/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
{!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
<TransferToLayingForm <TransferToLayingForm
type='detail' type='detail'
initialValues={transferToLaying.data} initialValues={transferToLaying.data}
/> />
)} */} )}
{/* TODO: remove this dummy data and integrate to real API */}
<TransferToLayingForm
type='detail'
initialValues={DUMMY_TRANSFER_TO_LAYING_DETAIL}
/>
</div> </div>
); );
}; };
+37 -23
View File
@@ -1,6 +1,13 @@
'use client'; 'use client';
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react'; import {
ReactNode,
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
export const useModal = () => { export const useModal = () => {
@@ -8,31 +15,34 @@ export const useModal = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const openModal = useCallback(() => { const openModal = useCallback(() => {
if (!ref.current) return;
ref.current.show();
setOpen(true); setOpen(true);
ref.current?.showModal();
}, []); }, []);
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
if (!ref.current) return;
ref.current.close();
setOpen(false); setOpen(false);
ref.current?.close();
}, []); }, []);
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (open) { open ? closeModal() : openModal();
closeModal();
} else {
openModal();
}
}, [open, closeModal, openModal]); }, [open, closeModal, openModal]);
if (ref.current) { useEffect(() => {
ref.current.addEventListener('close', () => { const dialog = ref.current;
closeModal(); if (!dialog) return;
});
}
return { ref, open, setOpen, openModal, closeModal, toggle } as const; const handleClose = () => setOpen(false);
dialog.addEventListener('close', handleClose);
return () => {
dialog.removeEventListener('close', handleClose);
};
}, []);
return { ref, open, openModal, closeModal, toggle } as const;
}; };
interface ModalProps { interface ModalProps {
@@ -46,15 +56,19 @@ interface ModalProps {
} }
const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => { const Modal = ({ ref, children, closeOnBackdrop, className }: ModalProps) => {
return ( const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
<dialog ref={ref} className={cn('modal', className?.modal)}> if (closeOnBackdrop && e.target === ref.current) {
<div className={cn('modal-box', className?.modalBox)}>{children}</div> ref.current?.close();
}
};
{closeOnBackdrop && ( return (
<form method='dialog' className='modal-backdrop'> <dialog
<button>close</button> ref={ref}
</form> className={cn('modal', className?.modal)}
)} onClick={handleBackdropClick}
>
<div className={cn('modal-box', className?.modalBox)}>{children}</div>
</dialog> </dialog>
); );
}; };
+7
View File
@@ -13,6 +13,7 @@ import {
FilterFn, FilterFn,
SortingState, SortingState,
OnChangeFn, OnChangeFn,
Row,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils'; import { rankItem } from '@tanstack/match-sorter-utils';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -50,6 +51,7 @@ export interface TableProps<TData extends object> {
manualSorting?: boolean; manualSorting?: boolean;
rowSelection?: Record<string, boolean>; rowSelection?: Record<string, boolean>;
setRowSelection?: OnChangeFn<Record<string, boolean>>; setRowSelection?: OnChangeFn<Record<string, boolean>>;
enableRowSelection?: boolean | ((row: Row<TData>) => boolean);
} }
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -90,6 +92,7 @@ const Table = <TData extends object>({
manualSorting = false, manualSorting = false,
rowSelection, rowSelection,
setRowSelection, setRowSelection,
enableRowSelection,
}: TableProps<TData>) => { }: TableProps<TData>) => {
const isServerSideTable = const isServerSideTable =
totalItems !== undefined && totalItems !== undefined &&
@@ -150,6 +153,10 @@ const Table = <TData extends object>({
tableOptions.getRowId = (row) => (row as { id: string }).id; tableOptions.getRowId = (row) => (row as { id: string }).id;
} }
if (enableRowSelection !== undefined) {
tableOptions.enableRowSelection = enableRowSelection;
}
const table = useReactTable(tableOptions); const table = useReactTable(tableOptions);
const { setPageSize } = table; const { setPageSize } = table;
+240 -40
View File
@@ -1,14 +1,23 @@
'use client'; 'use client';
import { ChangeEventHandler, FocusEventHandler, ReactNode } from 'react'; import {
ChangeEventHandler,
import { cn } from '@/lib/helper'; FocusEventHandler,
useEffect,
useState,
} from 'react';
import { cn, formatDate } from '@/lib/helper';
import Modal, { useModal } from '@/components/Modal';
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
export interface DateInputProps { export interface DateInputProps {
label?: string; label?: string;
bottomLabel?: string; bottomLabel?: string;
name: string; name: string;
value?: string; value?: string | { from?: string; to?: string };
placeholder?: string; placeholder?: string;
min?: string; min?: string;
max?: string; max?: string;
@@ -24,9 +33,8 @@ export interface DateInputProps {
readOnly?: boolean; readOnly?: boolean;
required?: boolean; required?: boolean;
isLoading?: boolean; isLoading?: boolean;
isRange?: boolean;
errorMessage?: string; errorMessage?: string;
startAdornment?: ReactNode;
endAdornment?: ReactNode;
onChange?: ChangeEventHandler<HTMLInputElement>; onChange?: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>; onBlur?: FocusEventHandler<HTMLInputElement>;
} }
@@ -36,22 +44,144 @@ const DateInput = ({
bottomLabel, bottomLabel,
name, name,
value, value,
placeholder, placeholder = 'dd/mm/yyyy',
min, min,
max, max,
className, className,
isError, isError: externalError,
isValid, isValid: externalValid,
errorMessage, errorMessage: externalErrorMessage,
startAdornment,
endAdornment,
disabled = false, disabled = false,
required = false, required = false,
onChange, onChange,
onBlur, onBlur,
readOnly = false, readOnly = false,
isLoading = false, isLoading = false,
isRange = false,
}: DateInputProps) => { }: DateInputProps) => {
const [internalError, setInternalError] = useState<string | null>(null);
const [selected, setSelected] = useState<Date | undefined>();
const [selectedRange, setSelectedRange] = useState<{
from?: Date;
to?: Date;
}>({});
const [displayValue, setDisplayValue] = useState<string>('');
const minDate = min
? new Date(min.split('/').reverse().join('-'))
: undefined;
const maxDate = max
? new Date(max.split('/').reverse().join('-'))
: undefined;
const calendarModal = useModal();
// --- Sync value props ---
useEffect(() => {
if (!value) return;
if (isRange && typeof value === 'object') {
const from = value.from ? new Date(value.from) : undefined;
const to = value.to ? new Date(value.to) : undefined;
setSelectedRange({ from, to });
setDisplayValue(
`${from ? formatDate(from, 'DD/MM/YYYY') : ''} ${
to ? '- ' + formatDate(to, 'DD/MM/YYYY') : ''
}`
);
} else if (typeof value === 'string') {
const iso = value.includes('/')
? value.split('/').reverse().join('-')
: value;
const date = new Date(iso);
setSelected(date);
setDisplayValue(formatDate(iso, 'DD/MM/YYYY'));
}
}, [value, isRange]);
const handleClick = (e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault();
if (!disabled && !readOnly) calendarModal.openModal();
};
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
onBlur?.(e);
};
const handleSelectSingle = (selectedDate?: Date) => {
if (!selectedDate) return;
if (minDate && selectedDate < minDate) {
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
return;
}
if (maxDate && selectedDate > maxDate) {
setInternalError(`Tanggal tidak boleh setelah ${max}`);
return;
}
setInternalError(null);
setSelected(selectedDate);
const formattedDisplay = formatDate(selectedDate, 'DD/MM/YYYY');
const formattedISO = formatDate(selectedDate, 'YYYY-MM-DD');
setDisplayValue(formattedDisplay);
const syntheticEvent = {
target: { name, value: formattedISO },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
calendarModal.closeModal();
};
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
if (!range) return;
setSelectedRange(range);
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
const toStr = range.to ? formatDate(range.to, 'DD/MM/YYYY') : '';
setDisplayValue(`${fromStr}${toStr ? ' - ' + toStr : ''}`);
// Jika kedua tanggal sudah terpilih
if (range.from && range.to) {
if (minDate && range.from < minDate) {
setInternalError(`Tanggal mulai tidak boleh sebelum ${min}`);
return;
}
if (maxDate && range.to > maxDate) {
setInternalError(`Tanggal akhir tidak boleh setelah ${max}`);
return;
}
setInternalError(null);
const syntheticEvent = {
target: {
name,
value: {
from: formatDate(range.from, 'YYYY-MM-DD'),
to: formatDate(range.to, 'YYYY-MM-DD'),
},
},
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
}
};
const handleResetDate = () => {
setSelected(undefined);
setSelectedRange({});
setDisplayValue('');
const syntheticEvent = {
target: { name, value: isRange ? { from: '', to: '' } : '' },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
calendarModal.closeModal();
};
const handleSaveDate = () => {
if (internalError) return;
calendarModal.closeModal();
};
const finalIsError = externalError || !!internalError;
const finalErrorMessage = internalError || externalErrorMessage;
return ( return (
<div <div
className={cn( className={cn(
@@ -64,65 +194,135 @@ const DateInput = ({
htmlFor={name} htmlFor={name}
className={cn( className={cn(
'w-full text-sm font-normal leading-5', 'w-full text-sm font-normal leading-5',
{ { 'text-error': finalIsError },
'text-error': isError,
},
className?.label className?.label
)} )}
> >
{label} {label}
{required && ( {required && (
<> <span className='text-error' title='required'>
{' '} *
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'>*</span>
</span> </span>
</>
)} )}
</label> </label>
)} )}
<div <div
className={cn( className={cn(
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded outline-none! transition-all duration-200 flex items-center', 'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded transition-all duration-200 flex items-center border',
{ {
'border-error': isError, 'border-error': finalIsError,
'border-success!': isValid, 'border-success': externalValid && !finalIsError,
}, },
className?.inputWrapper className?.inputWrapper
)} )}
> >
{startAdornment && startAdornment}
<input <input
type='date' type='text'
id={name} id={name}
name={name} name={name}
placeholder={placeholder} placeholder={isRange ? 'dd/mm/yyyy - dd/mm/yyyy' : placeholder}
value={value} value={displayValue}
onChange={onChange} onBlur={handleBlur}
onBlur={onBlur} onClick={handleClick}
min={min}
max={max}
disabled={disabled} disabled={disabled}
className={cn('grow bg-transparent cursor-pointer', className?.input)} readOnly // ✅ tidak bisa diketik manual
readOnly={readOnly} className={cn(
'grow bg-transparent cursor-pointer focus:outline-none',
className?.input
)}
/> />
{(isLoading || endAdornment) && ( {isLoading && (
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
{isLoading && <span className='loading loading-spinner' />} <span className='loading loading-spinner' />
{endAdornment && endAdornment}
</div> </div>
)} )}
<Icon
icon='uil:calendar'
width={24}
height={24}
className='cursor-pointer text-dark'
onClick={(e) =>
handleClick(e as unknown as React.MouseEvent<HTMLInputElement>)
}
/>
</div> </div>
{!isError && bottomLabel && ( {!finalIsError && bottomLabel && (
<p className='w-full text-sm opacity-60'>{bottomLabel}</p> <p className='w-full text-sm opacity-60'>{bottomLabel}</p>
)} )}
{isError && errorMessage && ( {finalIsError && finalErrorMessage && (
<p className='w-full text-sm text-error'>{errorMessage}</p> <p className='w-full text-sm text-error'>{finalErrorMessage}</p>
)} )}
<Modal
ref={calendarModal.ref}
className={{
modal: 'rounded',
modalBox: `w-fit min-h-${isRange ? '124' : '110'} flex flex-col`,
}}
closeOnBackdrop
>
{isRange ? (
<DayPicker
required={required}
mode='range'
captionLayout='dropdown-years'
navLayout='around'
reverseYears
defaultMonth={selectedRange.from ?? new Date()}
startMonth={minDate ?? new Date(1999, 1)}
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
selected={selectedRange as DateRange}
onSelect={handleSelectRange}
footer={<div className='text-center mt-3'>{displayValue}</div>}
disabled={
[
minDate ? { before: minDate } : undefined,
maxDate ? { after: maxDate } : undefined,
].filter(Boolean) as Matcher[]
}
/>
) : (
<DayPicker
required={required}
mode='single'
captionLayout='dropdown-years'
navLayout='around'
reverseYears
defaultMonth={selected ?? new Date()}
startMonth={minDate ?? new Date(1999, 1)}
endMonth={maxDate ?? new Date(new Date().getFullYear() + 5, 11)}
selected={selected}
onSelect={handleSelectSingle}
disabled={
[
minDate ? { before: minDate } : undefined,
maxDate ? { after: maxDate } : undefined,
].filter(Boolean) as Matcher[]
}
/>
)}
<div className='mt-auto flex flex-col gap-2'>
{isRange && (
<small className='text-secondary'>
Tekan dua kali untuk memilih tanggal awal
</small>
)}
<div className='flex h-full justify-end items-end gap-2'>
<Button type='button' color='warning' onClick={handleResetDate}>
Reset
</Button>
{isRange && (
<Button type='button' onClick={handleSaveDate}>
Simpan
</Button>
)}
</div>
</div>
</Modal>
</div> </div>
); );
}; };
+2 -6
View File
@@ -9,7 +9,7 @@ import Button from '@/components/Button';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { Color } from '@/types/theme'; import { Color } from '@/types/theme';
interface ConfirmationModalProps { export interface ConfirmationModalProps {
ref: RefObject<HTMLDialogElement | null>; ref: RefObject<HTMLDialogElement | null>;
type?: 'info' | 'success' | 'error'; type?: 'info' | 'success' | 'error';
text?: string; text?: string;
@@ -92,11 +92,7 @@ const ConfirmationModal = ({
{text ?? 'Apakah anda yakin ingin melakukan hal ini?'} {text ?? 'Apakah anda yakin ingin melakukan hal ini?'}
</p> </p>
{children && ( {children && <div className='w-full'>{children}</div>}
<div className='w-full'>
{children}
</div>
)}
<div className='w-full flex flex-row gap-2'> <div className='w-full flex flex-row gap-2'>
{secondaryButton && secondaryButton.text && ( {secondaryButton && secondaryButton.text && (
@@ -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) { if (!approvalGroup) {
const isWaiting = currentStepNumber === latestApproval.step_number + 1; const isWaiting = currentStepNumber === latestApproval.step_number + 1;
const isPreviousApprovalRejected =
groupedApprovals[groupedApprovals.length - 1].approvals[0].action ===
'REJECTED';
return { return {
name: approvalLineItem.step_name, name: approvalLineItem.step_name,
status: isWaiting ? 'WAITING' : 'IDLE', status: isPreviousApprovalRejected
? 'IDLE'
: isWaiting
? 'WAITING'
: 'IDLE',
}; };
} }
@@ -625,12 +625,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
let options: OptionType[] = []; let options: OptionType[] = [];
if (isResponseSuccess(locations)) { if (isResponseSuccess(locations)) {
options = options.concat( const locationOptionsList = locations?.data.map((location) => ({
locations?.data.map((location) => ({
value: location.id, value: location.id,
label: location.name, label: location.name || '',
})) || [] })) || [];
); options = options.concat(locationOptionsList);
} }
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
@@ -641,7 +640,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
) { ) {
options.push({ options.push({
value: currentLocation.id, value: currentLocation.id,
label: currentLocation.name, label: currentLocation.name || '',
}); });
} }
} }
@@ -653,12 +652,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
let options: OptionType[] = []; let options: OptionType[] = [];
if (isResponseSuccess(projectFlocks)) { if (isResponseSuccess(projectFlocks)) {
options = options.concat( const flockOptions = projectFlocks?.data.map((projectFlock) => ({
projectFlocks?.data.map((projectFlock) => ({
value: projectFlock.id, value: projectFlock.id,
label: projectFlock.flock_name, label: projectFlock.flock_name || '',
})) || [] })) || [];
); options = options.concat(flockOptions);
} }
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
@@ -669,7 +667,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
) { ) {
options.push({ options.push({
value: currentProjectFlock.id, value: currentProjectFlock.id,
label: currentProjectFlock.flock_name, label: currentProjectFlock.flock_name || '',
}); });
} }
} }
@@ -686,12 +684,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
); );
if (selectedProjectFlockData?.kandangs) { if (selectedProjectFlockData?.kandangs) {
options = options.concat( const kandangOptions = selectedProjectFlockData.kandangs.map((kandang: Kandang) => ({
selectedProjectFlockData.kandangs.map((kandang: Kandang) => ({
value: kandang.id, value: kandang.id,
label: kandang.name, label: kandang.name || '',
})) }));
); options = options.concat(kandangOptions);
} }
} }
@@ -703,7 +700,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
) { ) {
options.push({ options.push({
value: currentKandang.id, value: currentKandang.id,
label: currentKandang.name, label: currentKandang.name || '',
}); });
} }
} }
@@ -870,7 +867,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
baseValues.project_flock_kandang = { baseValues.project_flock_kandang = {
value: projectFlockKandangDetail.project_flock.id, value: projectFlockKandangDetail.project_flock.id,
label: projectFlockKandangDetail.project_flock.flock_name, label: projectFlockKandangDetail.project_flock.flock_name || '',
}; };
} }
@@ -1186,21 +1183,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
if (location) { if (location) {
const locationOption = { const locationOption = {
value: location.id, value: location.id,
label: location.name, label: location.name || '',
}; };
setSelectedLocation(locationOption); setSelectedLocation(locationOption);
if (projectFlock) { if (projectFlock) {
const projectFlockOption = { const projectFlockOption = {
value: projectFlock.id, value: projectFlock.id,
label: projectFlock.flock_name, label: projectFlock.flock_name || '',
}; };
setSelectedProjectFlock(projectFlockOption); setSelectedProjectFlock(projectFlockOption);
if (kandang) { if (kandang) {
const kandangOption = { const kandangOption = {
value: kandang.id, value: kandang.id,
label: kandang.name, label: kandang.name || '',
}; };
setSelectedKandang(kandangOption); setSelectedKandang(kandangOption);
@@ -2,7 +2,12 @@
import { ChangeEventHandler, useState } from 'react'; import { ChangeEventHandler, useState } from 'react';
import useSWR from 'swr'; 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 toast from 'react-hot-toast';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -20,6 +25,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import CheckboxInput from '@/components/input/CheckboxInput'; import CheckboxInput from '@/components/input/CheckboxInput';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
import { TransferToLayingApi } from '@/services/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 { ROWS_OPTIONS } from '@/config/constant';
import { Flock } from '@/types/api/master-data/flock'; import { Flock } from '@/types/api/master-data/flock';
import { FlockApi } from '@/services/api/master-data'; import { FlockApi } from '@/services/api/master-data';
import PillBadge from '@/components/PillBadge';
const RowOptionsMenu = ({ const RowOptionsMenu = ({
type = 'dropdown', type = 'dropdown',
@@ -43,6 +50,16 @@ const RowOptionsMenu = ({
rejectClickHandler: () => void; rejectClickHandler: () => void;
deleteClickHandler: () => 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 ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
<Button <Button
@@ -55,6 +72,7 @@ const RowOptionsMenu = ({
Detail Detail
</Button> </Button>
{showEditButton && (
<Button <Button
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`} href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
variant='ghost' variant='ghost'
@@ -64,7 +82,10 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:edit-outline' width={16} height={16} /> <Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit Edit
</Button> </Button>
)}
{/* TODO: apply RBAC */}
{showApproveButton && (
<Button <Button
variant='ghost' variant='ghost'
color='success' color='success'
@@ -74,7 +95,8 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:check' width={24} height={24} /> <Icon icon='material-symbols:check' width={24} height={24} />
Approve Approve
</Button> </Button>
)}
{showRejectButton && (
<Button <Button
variant='ghost' variant='ghost'
color='error' color='error'
@@ -84,7 +106,8 @@ const RowOptionsMenu = ({
<Icon icon='material-symbols:close' width={24} height={24} /> <Icon icon='material-symbols:close' width={24} height={24} />
Reject Reject
</Button> </Button>
)}
{showDeleteButton && (
<Button <Button
onClick={deleteClickHandler} onClick={deleteClickHandler}
variant='ghost' variant='ghost'
@@ -99,6 +122,7 @@ const RowOptionsMenu = ({
/> />
Delete Delete
</Button> </Button>
)}
</RowOptionsMenuWrapper> </RowOptionsMenuWrapper>
); );
}; };
@@ -187,17 +211,24 @@ const TransferToLayingsTable = () => {
/> />
</div> </div>
), ),
cell: ({ row }) => ( cell: ({ row }) => {
const isCheckboxDisabled =
!row.getCanSelect() ||
row.original.approval.action === 'APPROVED' ||
row.original.approval.action === 'REJECTED';
return (
<div> <div>
<CheckboxInput <CheckboxInput
name='row' name='row'
checked={row.getIsSelected()} checked={row.getIsSelected()}
disabled={!row.getCanSelect()} disabled={isCheckboxDisabled}
indeterminate={row.getIsSomeSelected()} indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()} onChange={row.getToggleSelectedHandler()}
/> />
</div> </div>
), );
},
}, },
{ {
header: '#', header: '#',
@@ -214,21 +245,55 @@ const TransferToLayingsTable = () => {
{ {
accessorKey: 'flock_source', accessorKey: 'flock_source',
header: 'Flock Asal', header: 'Flock Asal',
cell: (props) => props.row.original.flock_source.name, cell: (props) => props.row.original.from_project_flock.flock_name,
}, },
{ {
accessorKey: 'flock_destination', accessorKey: 'flock_destination',
header: 'Flock Tujuan', 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', header: 'Kuantitas',
cell: (props) => props.getValue() ?? props.row.original.pending_usage_qty,
}, },
{ {
accessorKey: 'reason', accessorKey: 'notes',
header: 'Alasan Transfer', 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', header: 'Aksi',
cell: (props) => { cell: (props) => {
@@ -237,7 +302,7 @@ const TransferToLayingsTable = () => {
const currentRowRelativeIndex = const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1; currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3;
const approveClickHandler = () => { const approveClickHandler = () => {
setSelectedTransferToLaying(props.row.original); setSelectedTransferToLaying(props.row.original);
@@ -268,7 +333,7 @@ const TransferToLayingsTable = () => {
return ( return (
<> <>
{currentPageSize > 2 && ( {currentPageSize > 3 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}> <RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu <RowOptionsMenu
type='dropdown' type='dropdown'
@@ -280,7 +345,7 @@ const TransferToLayingsTable = () => {
</RowDropdownOptions> </RowDropdownOptions>
)} )}
{currentPageSize <= 2 && ( {currentPageSize <= 3 && (
<RowCollapseOptions> <RowCollapseOptions>
<RowOptionsMenu <RowOptionsMenu
type='collapse' 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 = () => { const bulkApproveClickHandler = () => {
approveModal.openModal(); approveModal.openModal();
}; };
@@ -309,27 +383,31 @@ const TransferToLayingsTable = () => {
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
try {
await TransferToLayingApi.delete(selectedTransferToLaying?.id as number); await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
refreshTransferToLayings();
deleteModal.closeModal();
toast.success('Berhasil menghapus data transfer ke laying!'); toast.success('Berhasil menghapus data transfer ke laying!');
refreshTransferToLayings();
} catch (error) {
toast.success('Gagal menghapus data transfer ke laying!');
} finally {
deleteModal.closeModal();
setIsDeleteLoading(false); setIsDeleteLoading(false);
}
}; };
const confirmationModalApproveClickHandler = async () => { const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true); setIsApproveLoading(true);
const bulkApproveResponse = const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
await TransferToLayingApi.bulkApprove(selectedRowIds); selectedRowIds,
notes
);
if (isResponseSuccess(bulkApproveResponse)) { if (isResponseSuccess(bulkApproveResponse)) {
refreshTransferToLayings(); refreshTransferToLayings();
approveModal.closeModal(); approveModal.closeModal();
// TODO: remove console.log
console.log('Approved data:', selectedRowIds);
toast.success( toast.success(
`Berhasil approve ${selectedRowIds.length} data transfer ke laying!` `Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
); );
@@ -346,19 +424,18 @@ const TransferToLayingsTable = () => {
setIsApproveLoading(false); setIsApproveLoading(false);
}; };
const confirmationModalRejectClickHandler = async () => { const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true); setIsRejectLoading(true);
const bulkRejectResponse = const bulkRejectResponse = await TransferToLayingApi.bulkReject(
await TransferToLayingApi.bulkReject(selectedRowIds); selectedRowIds,
notes
);
if (isResponseSuccess(bulkRejectResponse)) { if (isResponseSuccess(bulkRejectResponse)) {
refreshTransferToLayings(); refreshTransferToLayings();
rejectModal.closeModal(); rejectModal.closeModal();
// TODO: remove console.log
console.log('Rejected data:', selectedRowIds);
toast.success( toast.success(
`Berhasil reject ${selectedRowIds.length} data transfer ke laying!` `Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
); );
@@ -559,6 +636,7 @@ const TransferToLayingsTable = () => {
setSorting={setSorting} setSorting={setSorting}
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
enableRowSelection={tableEnableRowSelectionHandler}
className={{ className={{
containerClassName: cn({ containerClassName: cn({
'mb-20': 'mb-20':
@@ -592,7 +670,7 @@ const TransferToLayingsTable = () => {
}} }}
/> />
<ConfirmationModal <ConfirmationModalWithNotes
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
text={`Apakah anda yakin ingin approve data transfer ke laying ini (${selectedRowIds.length} data)?`} 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} ref={rejectModal.ref}
type='error' type='error'
text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`} text={`Apakah anda yakin ingin reject data transfer ke laying ini (${selectedRowIds.length} data)?`}
@@ -1,4 +1,7 @@
import * as Yup from 'yup'; 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 = { type TransferToLayingFormSchemaType = {
transfer_date?: string; transfer_date?: string;
@@ -14,7 +17,7 @@ type TransferToLayingFormSchemaType = {
totalQuantity?: number; totalQuantity?: number;
maxTotalQuantity?: number; // original cap (hidden), helper maxTotalQuantity?: number; // original cap (hidden), helper
kandangs: { flockSourceKandangs: {
kandang: { kandang: {
value: number; value: number;
label: string; label: string;
@@ -22,6 +25,16 @@ type TransferToLayingFormSchemaType = {
quantity: number | string; // editable quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper maxQuantity?: number; // original cap (hidden), helper
}[]; }[];
flockDestinationKandangs: {
kandang: {
value: number;
label: string;
};
quantity: number | string; // editable
maxQuantity?: number; // original cap (hidden), helper
}[];
reason?: string; reason?: string;
}; };
@@ -51,7 +64,29 @@ export const TransferToLayingFormSchema: Yup.ObjectSchema<TransferToLayingFormSc
.min(1, 'Jumlah transfer minimal 1') .min(1, 'Jumlah transfer minimal 1')
.required('Jumlah transfer wajib diisi!'), .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( .of(
Yup.object({ Yup.object({
kandang: Yup.object({ kandang: Yup.object({
@@ -81,3 +116,122 @@ export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
export type TransferToLayingFormValues = Yup.InferType< export type TransferToLayingFormValues = Yup.InferType<
typeof TransferToLayingFormSchema 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'; 'use client';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@@ -8,16 +8,23 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
// useSelect, useSelect,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea'; import TextArea from '@/components/input/TextArea';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; 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 { import {
getFilledTransferToLayingFormInitialValues,
getTransferToLayingFormInitialValues,
TransferToLayingFormSchema, TransferToLayingFormSchema,
TransferToLayingFormValues, TransferToLayingFormValues,
UpdateTransferToLayingFormSchema, UpdateTransferToLayingFormSchema,
@@ -31,6 +38,8 @@ import {
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; 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 { interface TransferToLayingFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -55,11 +64,23 @@ const TransferToLayingForm = ({
const [isApproveLoading, setIsApproveLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false);
const [isRejectLoading, setIsRejectLoading] = 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( const createTransferToLayingHandler = useCallback(
async (payload: CreateTransferToLayingPayload) => { 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] [router]
); );
@@ -69,46 +90,30 @@ const TransferToLayingForm = ({
transferToLayingId: number, transferToLayingId: number,
payload: UpdateTransferToLayingPayload payload: UpdateTransferToLayingPayload
) => { ) => {
console.log( const updateKandangRes = await TransferToLayingApi.update(
`Update transfer to laying with ID of ${transferToLayingId}:`, transferToLayingId,
{ payload } 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] [router]
); );
const formikInitialValues = useMemo<TransferToLayingFormValues>(() => { // const formikInitialValues = useMemo<TransferToLayingFormValues>(() => {
return { // return getTransferToLayingFormInitialValues(initialValues);
transfer_date: initialValues?.transfer_date ?? '', // }, [initialValues]);
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,
kandangs: initialValues?.kandangs const [formikInitialValues, setFormikInitialValues] = useState(
? initialValues.kandangs.map((kandang) => ({ getTransferToLayingFormInitialValues()
kandang: { );
value: kandang.kandang.id,
label: kandang.kandang.name,
},
quantity: kandang.quantity,
}))
: [],
reason: initialValues?.reason ?? undefined,
};
}, [initialValues]);
const formik = useFormik<TransferToLayingFormValues>({ const formik = useFormik<TransferToLayingFormValues>({
initialValues: formikInitialValues, initialValues: formikInitialValues,
@@ -117,23 +122,23 @@ const TransferToLayingForm = ({
? UpdateTransferToLayingFormSchema ? UpdateTransferToLayingFormSchema
: TransferToLayingFormSchema, : TransferToLayingFormSchema,
onSubmit: async (values) => { onSubmit: async (values) => {
console.log({ values });
setFormErrorMessage(''); setFormErrorMessage('');
const transferToLayingPayload: CreateTransferToLayingPayload = { const transferToLayingPayload: CreateTransferToLayingPayload = {
transfer_date: values.transfer_date as string, transfer_date: values.transfer_date as string,
flock_source_id: values.flockSource?.value as number, source_project_flock_id: values.flockSource?.value as number,
flock_destination_id: values.flockDestination?.value as number, target_project_flock_id: values.flockDestination?.value as number,
totalQuantity: values.totalQuantity as number, totalQuantity: values.totalQuantity as number,
kandangs: values.kandangs?.map((kandang) => ({ source_kandangs: values.flockSourceKandangs?.map((kandang) => ({
kandang_id: kandang.kandang.value, project_flock_kandang_id: kandang.kandang.value,
quantity: kandang.quantity, quantity: parseFloat(kandang.quantity as string),
})) as { })) as CreateTransferToLayingPayload['source_kandangs'],
kandang_id: number;
quantity: number; 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, reason: values.reason as string,
}; };
@@ -154,7 +159,11 @@ const TransferToLayingForm = ({
}); });
const { setValues: formikSetValues, values: formikValues } = formik; const { setValues: formikSetValues, values: formikValues } = formik;
const { kandangs: kandangsValue } = formikValues; const {
flockSourceKandangs: flockSourceKandangsValue,
flockDestinationKandangs: flockDestinationKandangsValue,
totalQuantity,
} = formikValues;
const deleteTransferToLayingClickHandler = () => { const deleteTransferToLayingClickHandler = () => {
deleteModal.openModal(); deleteModal.openModal();
@@ -172,24 +181,32 @@ const TransferToLayingForm = ({
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
// TODO: delete data and integrate to real API try {
deleteModal.closeModal(); await TransferToLayingApi.delete(initialValues?.id as number);
toast.success('Berhasil menghapus data transfer ke laying!');
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); setIsDeleteLoading(false);
}
}; };
const confirmationModalApproveClickHandler = async () => { const confirmationModalApproveClickHandler = async (notes: string) => {
setIsApproveLoading(true); setIsApproveLoading(true);
const approveResponse = await TransferToLayingApi.approve( const approveResponse = await TransferToLayingApi.approve(
initialValues?.id as number initialValues?.id as number,
notes
); );
if (isResponseSuccess(approveResponse)) { if (isResponseSuccess(approveResponse)) {
approveModal.closeModal(); approveModal.closeModal();
toast.success('Berhasil approve data transfer ke laying!'); toast.success('Berhasil approve data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else { } else {
approveModal.closeModal(); approveModal.closeModal();
@@ -199,17 +216,19 @@ const TransferToLayingForm = ({
setIsApproveLoading(false); setIsApproveLoading(false);
}; };
const confirmationModalRejectClickHandler = async () => { const confirmationModalRejectClickHandler = async (notes: string) => {
setIsRejectLoading(true); setIsRejectLoading(true);
const rejectResponse = await TransferToLayingApi.reject( const rejectResponse = await TransferToLayingApi.reject(
initialValues?.id as number initialValues?.id as number,
notes
); );
if (isResponseSuccess(rejectResponse)) { if (isResponseSuccess(rejectResponse)) {
rejectModal.closeModal(); rejectModal.closeModal();
toast.success('Berhasil reject data transfer ke laying!'); toast.success('Berhasil reject data transfer ke laying!');
router.push('/production/transfer-to-laying');
} else { } else {
rejectModal.closeModal(); rejectModal.closeModal();
@@ -219,49 +238,47 @@ const TransferToLayingForm = ({
setIsRejectLoading(false); setIsRejectLoading(false);
}; };
const isRepeaterInputError = ( // flock source
column: keyof TransferToLayingFormValues['kandangs'][0], const isFlockSourceKandangsRepeaterInputError = (
column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number idx: number
) => { ) => {
return ( return (
formik.touched.kandangs?.[idx]?.[column] && formik.touched.flockSourceKandangs?.[idx]?.[column] &&
Boolean( Boolean(
formik.errors.kandangs?.[idx] instanceof Object && formik.errors.flockSourceKandangs?.[idx] instanceof Object &&
formik.errors.kandangs?.[idx]?.[column] formik.errors.flockSourceKandangs?.[idx]?.[column]
) )
); );
}; };
const repeaterInputErrorMessage = ( const flockSourceKandangsRepeaterInputErrorMessage = (
column: keyof TransferToLayingFormValues['kandangs'][0], column: keyof TransferToLayingFormValues['flockSourceKandangs'][0],
idx: number 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 const {
// Flock Source setInputValue: setFlockSourceInputValue,
// const { options: flockSourceOptions,
// inputValue: flockSourceInputValue, isLoadingOptions: isLoadingFlockSourceOptions,
// setInputValue: setFlockSourceInputValue, rawData: flockSources,
// options: flockSourceOptions, } = useSelect<ProjectFlock>(
// isLoadingOptions: isLoadingFlockSourceOptions, '/production/project-flocks',
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-source', 'id', 'name'); 'id',
'flock_name',
// TODO: remove this dummy data 'search',
const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR( {
'test', category: 'GROWING',
() => TransferToLayingApi.getFlockSource() }
); );
const flockSourceOptions = isResponseSuccess(flockSources) const flockSourceChangeHandler = async (
? flockSources?.data.map((flockSource) => ({ val: OptionType | OptionType[] | null
value: flockSource.id, ) => {
label: flockSource.name,
}))
: [];
const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
// Get flock source data for total quantity and kandang // Get flock source data for total quantity and kandang
const flockSource = const flockSource =
isResponseSuccess(flockSources) && val !== null isResponseSuccess(flockSources) && val !== null
@@ -272,21 +289,38 @@ const TransferToLayingForm = ({
// Set total quantity and kandangs // Set total quantity and kandangs
if (flockSource) { if (flockSource) {
const mappedFlockKandangsAvailableQty =
await TransferToLayingApi.getMappedFlockKandangsAvailability(
flockSource.id
);
const formattedKandangs = flockSource.kandangs.map((item) => ({ const formattedKandangs = flockSource.kandangs.map((item) => ({
kandang: { kandang: {
value: item.kandang.id, value: item.project_flock_kandang_id,
label: item.kandang.name, label: item.name,
}, },
quantity: '', quantity: '',
maxQuantity: item.quantity, maxQuantity:
(mappedFlockKandangsAvailableQty &&
mappedFlockKandangsAvailableQty[item.project_flock_kandang_id]
.available_qty) ??
0,
})); }));
formik.setFieldValue('totalQuantity', flockSource.totalQuantity); let maxTotalQuantity = 0;
formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity); // flockSource.kandangs.forEach((item) => {
formik.setFieldValue('kandangs', formattedKandangs); // maxTotalQuantity += item.capacity;
// });
formattedKandangs.forEach((item) => {
maxTotalQuantity += item.maxQuantity;
});
formik.setFieldValue('totalQuantity', '');
formik.setFieldValue('maxTotalQuantity', maxTotalQuantity);
formik.setFieldValue('flockSourceKandangs', formattedKandangs);
} else { } else {
formik.setFieldValue('totalQuantity', undefined); formik.setFieldValue('totalQuantity', undefined);
formik.setFieldValue('kandangs', undefined); formik.setFieldValue('flockSourceKandangs', undefined);
formik.setFieldValue('reason', ''); formik.setFieldValue('reason', '');
} }
@@ -294,52 +328,137 @@ const TransferToLayingForm = ({
formik.setFieldValue('flockSource', val); formik.setFieldValue('flockSource', val);
}; };
// TODO: remove dummy data and use real data // flock destination
// Flock Destination const isFlockDestinationKandangsRepeaterInputError = (
// const { column: keyof TransferToLayingFormValues['flockDestinationKandangs'][0],
// inputValue: flockDestinationInputValue, idx: number
// setInputValue: setFlockDestinationInputValue, ) => {
// options: flockDestinationOptions, return (
// isLoadingOptions: isLoadingFlockDestinationOptions, formik.touched.flockDestinationKandangs?.[idx]?.[column] &&
// } = useSelect<FlockWithKandangs>('/transfer-to-laying/production/get-flock-destination', 'id', 'name'); 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 { const {
data: flockDestinations, setInputValue: setFlockDestinationInputValue,
isLoading: isLoadingFlockDestinationOptions, options: flockDestinationOptions,
} = useSWR('test', () => TransferToLayingApi.getFlockSource()); isLoadingOptions: isLoadingFlockDestinationOptions,
rawData: flockDestinations,
const flockDestinationOptions = isResponseSuccess(flockDestinations) } = useSelect<ProjectFlock>(
? flockDestinations?.data.map((flockDestination) => ({ '/production/project-flocks',
value: flockDestination.id, 'id',
label: flockDestination.name, 'flock_name',
})) 'search',
: []; {
category: 'LAYING',
}
);
const flockDestinationChangeHandler = ( const flockDestinationChangeHandler = (
val: OptionType | OptionType[] | null 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.setFieldTouched('flockDestination', true);
formik.setFieldValue('flockDestination', val); 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(() => { useEffect(() => {
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]); }, [formikSetValues, formikInitialValues]);
useEffect(() => { useEffect(() => {
// calculate total quantity if kandangs quantity change // calculate total quantity if kandangs quantity change
if (kandangsValue && kandangsValue.length > 0) { if (flockSourceKandangsValue && flockSourceKandangsValue.length > 0) {
let newTotalQuantity = 0; let newTotalQuantity = 0;
kandangsValue.forEach((item) => { flockSourceKandangsValue.forEach((item) => {
newTotalQuantity += item.quantity as number; newTotalQuantity += parseFloat(item.quantity as string);
}); });
formik.setFieldValue('totalQuantity', newTotalQuantity); formik.setFieldValue('totalQuantity', newTotalQuantity);
formik.validateField('totalQuantity'); 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 ( return (
<> <>
@@ -361,17 +480,38 @@ const TransferToLayingForm = ({
</h1> </h1>
</header> </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' && ( {type === 'detail' && (
<> <>
{isShowApproveRejectButton && (
<div className='w-full flex flex-row justify-end gap-2'>
{/* TODO: apply RBAC */}
<Button <Button
variant='outline' variant='outline'
color='success' color='success'
onClick={approveClickHandler} onClick={approveClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit' className='w-full sm:w-fit'
> >
<Icon icon='material-symbols:check' width={24} height={24} /> <Icon
icon='material-symbols:check'
width={24}
height={24}
/>
Approve Approve
</Button> </Button>
@@ -379,12 +519,17 @@ const TransferToLayingForm = ({
variant='outline' variant='outline'
color='error' color='error'
onClick={rejectClickHandler} onClick={rejectClickHandler}
// disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit' className='w-full sm:w-fit'
> >
<Icon icon='material-symbols:close' width={24} height={24} /> <Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject Reject
</Button> </Button>
</div>
)}
</> </>
)} )}
</div> </div>
@@ -395,13 +540,12 @@ const TransferToLayingForm = ({
className='w-full flex flex-col gap-6' className='w-full flex flex-col gap-6'
> >
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<TextInput <DateInput
required required
type='date'
label='Tanggal Transfer' label='Tanggal Transfer'
name='transfer_date' name='transfer_date'
placeholder='Masukkan tanggal transfer' placeholder='Masukkan tanggal transfer'
value={formik.values.transfer_date} value={formik.values.transfer_date ?? ''}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
isError={ isError={
@@ -421,7 +565,7 @@ const TransferToLayingForm = ({
options={flockSourceOptions} options={flockSourceOptions}
onChange={flockSourceChangeHandler} onChange={flockSourceChangeHandler}
isLoading={isLoadingFlockSourceOptions} isLoading={isLoadingFlockSourceOptions}
// onInputChange={setFlockSourceInputValue} onInputChange={setFlockSourceInputValue}
isError={ isError={
formik.touched.flockSource && formik.touched.flockSource &&
Boolean(typeof formik.errors.flockSource === 'string') Boolean(typeof formik.errors.flockSource === 'string')
@@ -439,7 +583,7 @@ const TransferToLayingForm = ({
options={flockDestinationOptions} options={flockDestinationOptions}
onChange={flockDestinationChangeHandler} onChange={flockDestinationChangeHandler}
isLoading={isLoadingFlockDestinationOptions} isLoading={isLoadingFlockDestinationOptions}
// onInputChange={setFlockDestinationInputValue} onInputChange={setFlockDestinationInputValue}
isError={ isError={
formik.touched.flockDestination && formik.touched.flockDestination &&
Boolean(typeof formik.errors.flockDestination === 'string') Boolean(typeof formik.errors.flockDestination === 'string')
@@ -450,9 +594,8 @@ const TransferToLayingForm = ({
/> />
</div> </div>
<TextInput <NumberInput
required required
type='number'
name='totalQuantity' name='totalQuantity'
label='Jumlah Transfer' label='Jumlah Transfer'
bottomLabel={ bottomLabel={
@@ -461,7 +604,9 @@ const TransferToLayingForm = ({
: undefined : undefined
} }
placeholder='Masukkan jumlah transfer' placeholder='Masukkan jumlah transfer'
value={formik.values.totalQuantity ?? ''} value={
formik.values.totalQuantity ? formik.values.totalQuantity : ''
}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
isError={ isError={
@@ -469,24 +614,22 @@ const TransferToLayingForm = ({
Boolean(formik.errors.totalQuantity) Boolean(formik.errors.totalQuantity)
} }
errorMessage={formik.errors.totalQuantity} errorMessage={formik.errors.totalQuantity}
// readOnly={type === 'detail'}
// disabled={Boolean(formik.errors.flockSource)}
disabled disabled
/> />
<div> <div className='flex flex-col gap-4'>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<table className='table'> <table className='table'>
<thead> <thead>
<tr> <tr>
<th>Kandang</th> <th>Kandang Flock Asal</th>
<th>Kuantitas</th> <th>Kuantitas</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{(!formik.values.kandangs || {(!formik.values.flockSourceKandangs ||
formik.values.kandangs.length === 0) && ( formik.values.flockSourceKandangs.length === 0) && (
<tr> <tr>
<td colSpan={2}> <td colSpan={2}>
<p className='w-full text-center text-gray-400'> <p className='w-full text-center text-gray-400'>
@@ -496,8 +639,8 @@ const TransferToLayingForm = ({
</tr> </tr>
)} )}
{formik.values.kandangs && {formik.values.flockSourceKandangs &&
formik.values.kandangs.map((kandang, idx) => ( formik.values.flockSourceKandangs.map((kandang, idx) => (
<tr key={idx}> <tr key={idx}>
<td> <td>
<SelectInput <SelectInput
@@ -511,10 +654,9 @@ const TransferToLayingForm = ({
</td> </td>
<td> <td>
<TextInput <NumberInput
required required
type='number' name={`flockSourceKandangs[${idx}].quantity`}
name={`kandangs[${idx}].quantity`}
bottomLabel={ bottomLabel={
kandang.maxQuantity kandang.maxQuantity
? `Max: ${kandang.maxQuantity}` ? `Max: ${kandang.maxQuantity}`
@@ -524,8 +666,11 @@ const TransferToLayingForm = ({
value={kandang.quantity} value={kandang.quantity}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
isError={isRepeaterInputError('quantity', idx)} isError={isFlockSourceKandangsRepeaterInputError(
errorMessage={repeaterInputErrorMessage( 'quantity',
idx
)}
errorMessage={flockSourceKandangsRepeaterInputErrorMessage(
'quantity', 'quantity',
idx idx
)} )}
@@ -540,6 +685,76 @@ const TransferToLayingForm = ({
</tbody> </tbody>
</table> </table>
</div> </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> </div>
<TextArea <TextArea
@@ -558,9 +773,21 @@ const TransferToLayingForm = ({
/> />
</div> </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'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && ( {type !== 'add' && (
<div className='flex flex-row justify-start gap-2'> <div className='flex flex-row justify-start gap-2'>
{isShowDeleteButton && (
<Button <Button
type='button' type='button'
color='error' color='error'
@@ -575,8 +802,9 @@ const TransferToLayingForm = ({
/> />
Delete Delete
</Button> </Button>
)}
{type !== 'edit' && ( {type !== 'edit' && isShowEditButton && (
<Button <Button
type='button' type='button'
color='warning' color='warning'
@@ -617,17 +845,6 @@ const TransferToLayingForm = ({
</div> </div>
)} )}
</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> </form>
</section> </section>
@@ -650,7 +867,7 @@ const TransferToLayingForm = ({
{type === 'detail' && ( {type === 'detail' && (
<> <>
<ConfirmationModal <ConfirmationModalWithNotes
ref={approveModal.ref} ref={approveModal.ref}
type='success' type='success'
text='Apakah anda yakin ingin approve data transfer ke laying ini?' text='Apakah anda yakin ingin approve data transfer ke laying ini?'
@@ -665,7 +882,7 @@ const TransferToLayingForm = ({
}} }}
/> />
<ConfirmationModal <ConfirmationModalWithNotes
ref={rejectModal.ref} ref={rejectModal.ref}
type='error' type='error'
text='Apakah anda yakin ingin reject data transfer ke laying ini?' 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; ] 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 RECORDING_APPROVAL_LINE: ApprovalLine = [ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
{ {
step_number: 1, step_number: 1,
File diff suppressed because it is too large Load Diff
-12
View File
@@ -12,15 +12,3 @@ export type CreateFlockPayload = {
}; };
export type UpdateFlockPayload = 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; status: string;
location: BaseLocation; location: BaseLocation;
pic: BaseUser; pic: BaseUser;
capacity: number;
}; };
export type Kandang = BaseMetadata & BaseKandang; export type Kandang = BaseMetadata & BaseKandang;
+15 -2
View File
@@ -7,7 +7,8 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
export type BaseProjectFlock = { export type BaseProjectFlock = {
id: number; id: number;
flock_name: string; name?: string;
flock_name?: string;
status: string; status: string;
flock: Flock; flock: Flock;
flock_id: number; flock_id: number;
@@ -20,7 +21,9 @@ export type BaseProjectFlock = {
location_id: number; location_id: number;
period: number; period: number;
kandang_ids: number[]; kandang_ids: number[];
kandangs: Kandang[]; kandangs: (Kandang & {
project_flock_kandang_id: number;
})[];
approval: BaseApproval; approval: BaseApproval;
}; };
@@ -57,3 +60,13 @@ export type ProjectFlockKandangLookup = {
project_flock: ProjectFlock; project_flock: ProjectFlock;
quantity: number; quantity: number;
}; };
export type ProjectFlockAvailableQuantity = {
project_flock_id: number;
flock_name: string;
category: 'LAYING' | 'GROWING';
kandangs: {
project_flock_kandang_id: number;
available_qty: number;
}[];
};
+67 -13
View File
@@ -1,34 +1,88 @@
import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general'; import {
import { Kandang } from '@/types/api/master-data/kandang'; 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 = { export type BaseTransferToLaying = {
id: number; id: number;
transfer_number: string;
transfer_date: string; transfer_date: string;
flock_source: { notes: string;
from_project_flock: {
id: number;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
to_project_flock: {
id: number;
flock_name: string;
category: 'GROWING' | 'LAYING';
};
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; id: number;
name: string; name: string;
}; };
flock_destination: { warehouse: {
id: number; id: number;
name: string; name: string;
type: WarehouseType;
};
}; };
quantity: number;
kandangs: {
kandang: Kandang;
quantity: number;
}[]; }[];
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 TransferToLaying = BaseMetadata & BaseTransferToLaying;
export type CreateTransferToLayingPayload = { export type CreateTransferToLayingPayload = {
transfer_date: string; transfer_date: string;
flock_source_id: number; source_project_flock_id: number;
flock_destination_id: number; target_project_flock_id: number;
totalQuantity: number; totalQuantity: number;
kandangs: { source_kandangs: {
kandang_id: number; project_flock_kandang_id: number;
quantity: number;
}[];
target_kandangs: {
project_flock_kandang_id: number;
quantity: number; quantity: number;
}[]; }[];
reason: string; reason: string;