mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
refactor(FE): Add transition restrictions for recording operations
This commit is contained in:
@@ -21,6 +21,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { useFormik } from 'formik';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { type Recording } from '@/types/api/production/recording';
|
||||
import { getRecordingRestriction } from './recording-utils';
|
||||
import { RecordingApi } from '@/services/api/production';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -105,30 +107,57 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const isRecordingEditable = (recording: Recording) => {
|
||||
if (
|
||||
recording.executed_at &&
|
||||
recording.project_flock?.project_flock_category === 'GROWING'
|
||||
) {
|
||||
const category = recording.project_flock?.project_flock_category;
|
||||
const isTransition = recording.is_transition;
|
||||
|
||||
const restriction = getRecordingRestriction(
|
||||
category || 'GROWING',
|
||||
isTransition
|
||||
);
|
||||
|
||||
if (restriction.isLocked) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||
const category = recording.project_flock?.project_flock_category;
|
||||
const isTransition = recording.is_transition;
|
||||
|
||||
return getRecordingRestriction(category || 'GROWING', isTransition);
|
||||
};
|
||||
|
||||
const isApproved = isRecordingApproved(props.row.original);
|
||||
const isRejected = isRecordingRejected(props.row.original);
|
||||
const isEditable = isRecordingEditable(props.row.original);
|
||||
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
<Tooltip
|
||||
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||
position='top'
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
className={restrictionInfo.isLocked ? 'text-error' : ''}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
restrictionInfo.isLocked
|
||||
? 'material-symbols:lock-outline'
|
||||
: 'material-symbols:more-vert'
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</PopoverButton>
|
||||
</Tooltip>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
@@ -763,9 +792,19 @@ const RecordingTable = () => {
|
||||
cell: (props) => {
|
||||
const category =
|
||||
props.row.original.project_flock?.project_flock_category;
|
||||
const isTransition = props.row.original.is_transition;
|
||||
if (!category) return '-';
|
||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
||||
return (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<StatusBadge color={color} text={formatTitleCase(category)} />
|
||||
{isTransition && (
|
||||
<span className='text-xs text-warning font-medium'>
|
||||
(Transisi)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
||||
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import { formatDate, formatNumber, cn } from '@/lib/helper';
|
||||
import toast from 'react-hot-toast';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
LAYING_RECORDING_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import { getRecordingRestriction } from '../recording-utils';
|
||||
|
||||
interface RecordingFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -272,16 +273,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return recording?.approval?.action === 'REJECTED';
|
||||
}, []);
|
||||
|
||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||
if (
|
||||
recording?.executed_at &&
|
||||
recording?.project_flock?.project_flock_category === 'GROWING'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// ===== PAYLOAD CREATION HELPERS =====
|
||||
const createGrowingPayload = useCallback(
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
@@ -476,6 +467,60 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
? projectFlockKandangDetailData.data
|
||||
: undefined;
|
||||
|
||||
// ===== TRANSITION RESTRICTION LOGIC =====
|
||||
const isTransitionPeriod = useMemo(() => {
|
||||
return initialValues?.is_transition ?? false;
|
||||
}, [initialValues]);
|
||||
|
||||
const recordingRestriction = useMemo(() => {
|
||||
const category =
|
||||
initialValues?.project_flock?.project_flock_category ||
|
||||
projectFlockKandangLookup?.project_flock?.category ||
|
||||
projectFlockKandangDetail?.project_flock?.category ||
|
||||
'GROWING';
|
||||
|
||||
const isTransition = initialValues?.is_transition ?? false;
|
||||
|
||||
const currentFlockCategory = projectFlockKandangDetail?.project_flock
|
||||
?.category as 'GROWING' | 'LAYING' | undefined;
|
||||
|
||||
return getRecordingRestriction(
|
||||
category as 'GROWING' | 'LAYING',
|
||||
isTransition,
|
||||
type === 'edit' ? currentFlockCategory : undefined
|
||||
);
|
||||
}, [
|
||||
initialValues,
|
||||
projectFlockKandangLookup,
|
||||
projectFlockKandangDetail,
|
||||
type,
|
||||
]);
|
||||
|
||||
const isRecordingEditable = useCallback(
|
||||
(recording?: Recording) => {
|
||||
if (!recording) return true;
|
||||
|
||||
const category = recording.project_flock?.project_flock_category;
|
||||
const isTransition = recording.is_transition;
|
||||
|
||||
const currentFlockCategory = projectFlockKandangDetail?.project_flock
|
||||
?.category as 'GROWING' | 'LAYING' | undefined;
|
||||
|
||||
const restriction = getRecordingRestriction(
|
||||
category || 'GROWING',
|
||||
isTransition,
|
||||
currentFlockCategory
|
||||
);
|
||||
|
||||
if (restriction.isLocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[projectFlockKandangDetail]
|
||||
);
|
||||
|
||||
const {
|
||||
options: stockProductOptions,
|
||||
rawData: stockProducts,
|
||||
@@ -2324,6 +2369,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setSelectedStocks([]);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2373,6 +2419,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2425,7 +2472,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
isSearchable
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
!formik.values.project_flock_kandang_id
|
||||
!formik.values.project_flock_kandang_id ||
|
||||
!recordingRestriction.canEditStock
|
||||
}
|
||||
isClearable={type !== 'detail'}
|
||||
inputPrefix={
|
||||
@@ -2472,7 +2520,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
disabled={
|
||||
type === 'detail' ||
|
||||
!recordingRestriction.canEditStock
|
||||
}
|
||||
/>
|
||||
{getStockUsageAdornment(idx)}
|
||||
</div>
|
||||
@@ -2484,6 +2535,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeStock(idx)}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
@@ -2501,38 +2553,81 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedStocks.length > 0 && (
|
||||
{selectedStocks.length > 0 &&
|
||||
recordingRestriction.canEditStock && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedStocks}
|
||||
disabled={selectedStocks.length === 0}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedStocks.length})
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
!recordingRestriction.canEditStock
|
||||
? 'Stock tidak dapat ditambahkan pada masa transisi Laying'
|
||||
: ''
|
||||
}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedStocks}
|
||||
disabled={selectedStocks.length === 0}
|
||||
color='success'
|
||||
onClick={addStock}
|
||||
className='w-fit'
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedStocks.length})
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Stok
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addStock}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Stok
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Transition Warning Banner -- MOVED UP -- */}
|
||||
{isTransitionPeriod && (
|
||||
<div className='alert alert-warning mb-4'>
|
||||
<Icon
|
||||
icon='material-symbols:warning-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>
|
||||
{isLayingCategory
|
||||
? 'Masa Transisi Laying: Hanya Deplesi yang dapat diisi. Stock (Pakan/OVK) tidak dapat diinput.'
|
||||
: 'Masa Transisi Growing: Hanya Stock (Pakan/OVK) yang dapat diisi. Deplesi tidak dapat diinput.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Locked Recording Warning */}
|
||||
{recordingRestriction.isLocked && (
|
||||
<div className='alert alert-error mb-4'>
|
||||
<Icon
|
||||
icon='material-symbols:lock-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{recordingRestriction.lockReason}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Depletions Table */}
|
||||
{((type as 'add' | 'edit' | 'detail') !== 'detail' ||
|
||||
(formik.values.depletions?.length ?? 0) > 0) && (
|
||||
<Card
|
||||
title='Deplesi'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
wrapper: cn('w-full mb-4 shadow', {
|
||||
'opacity-60':
|
||||
!recordingRestriction.canEditDepletion &&
|
||||
(type as 'add' | 'edit' | 'detail') !== 'detail',
|
||||
}),
|
||||
title: 'mb-4',
|
||||
}}
|
||||
>
|
||||
@@ -2562,6 +2657,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setSelectedDepletions([]);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditDepletion}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2598,6 +2694,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditDepletion}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2640,7 +2737,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
idx
|
||||
).errorMessage
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-48',
|
||||
}}
|
||||
@@ -2679,7 +2779,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
disabled={
|
||||
type === 'detail' ||
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
@@ -2689,6 +2792,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeDepletion(idx)}
|
||||
disabled={
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
@@ -2706,27 +2812,38 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedDepletions.length > 0 && (
|
||||
{selectedDepletions.length > 0 &&
|
||||
recordingRestriction.canEditDepletion && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedDepletions}
|
||||
disabled={selectedDepletions.length === 0}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedDepletions.length})
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
!recordingRestriction.canEditDepletion
|
||||
? 'Deplesi tidak dapat ditambahkan pada masa transisi Growing'
|
||||
: ''
|
||||
}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedDepletions}
|
||||
disabled={selectedDepletions.length === 0}
|
||||
color='success'
|
||||
onClick={addDepletion}
|
||||
className='w-fit'
|
||||
disabled={!recordingRestriction.canEditDepletion}
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedDepletions.length})
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Depletion
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addDepletion}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Depletion
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
export type RecordingRestriction = {
|
||||
canEditStock: boolean;
|
||||
canEditDepletion: boolean;
|
||||
canEditEgg: boolean;
|
||||
isLocked: boolean;
|
||||
lockReason?: string;
|
||||
};
|
||||
|
||||
export const getRecordingRestriction = (
|
||||
category: 'GROWING' | 'LAYING',
|
||||
isTransition: boolean,
|
||||
currentCategory?: 'GROWING' | 'LAYING'
|
||||
): RecordingRestriction => {
|
||||
if (currentCategory === 'LAYING' && category === 'GROWING') {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason:
|
||||
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||
};
|
||||
}
|
||||
|
||||
if (category === 'GROWING') {
|
||||
if (isTransition) {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (isTransition) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: true,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
};
|
||||
+1
@@ -74,6 +74,7 @@ export type ProjectFlockKandangLookup = {
|
||||
available_quantity?: number;
|
||||
population: number;
|
||||
chick_in_date: string;
|
||||
is_transition: boolean;
|
||||
};
|
||||
|
||||
export type ProjectFlockAvailableQuantity = {
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export type BaseRecording = {
|
||||
project_flock: ProjectFlock;
|
||||
record_datetime: string;
|
||||
day: number;
|
||||
executed_at: string;
|
||||
is_transition: boolean;
|
||||
} & ProductionMetrics;
|
||||
|
||||
export type RecordingDepletion = {
|
||||
|
||||
Reference in New Issue
Block a user