refactor(FE): Restrict stock and depletion edits based on permissions

This commit is contained in:
rstubryan
2026-03-11 10:18:19 +07:00
parent 5494cb0ff2
commit 811850857d
@@ -1043,6 +1043,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
}; };
} }
if (!recordingRestriction.canEditStock) {
baseValues.stocks = [];
}
if (!recordingRestriction.canEditDepletion) {
baseValues.depletions = [];
}
return baseValues; return baseValues;
}, [ }, [
initialValues, initialValues,
@@ -1053,6 +1061,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
selectedLocation, selectedLocation,
selectedProjectFlock, selectedProjectFlock,
selectedKandang, selectedKandang,
recordingRestriction.canEditStock,
recordingRestriction.canEditDepletion,
]); ]);
const formik = useFormik< const formik = useFormik<
@@ -2443,86 +2453,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div> </div>
)} )}
{/* Stocks Table */} {/* Stocks Table - Only show if can edit stock or has data */}
<Card {(recordingRestriction.canEditStock ||
title='Stok Persediaan' (type === 'detail' && formik.values.stocks?.length > 0)) && (
className={{ <Card
wrapper: 'w-full mb-4 shadow', title='Stok Persediaan'
title: 'mb-4', className={{
}} wrapper: 'w-full mb-4 shadow',
> title: 'mb-4',
<div className='overflow-x-auto'> }}
<table className='table'> >
<thead> <div className='overflow-x-auto'>
<tr> <table className='table'>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( <thead>
<th> <tr>
<CheckboxInput
name='select-all-stocks'
checked={
formik.values.stocks?.length ===
selectedStocks.length &&
formik.values.stocks?.length > 0
}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedStocks(
formik.values.stocks?.map((_, idx) => idx) ?? []
);
} else {
setSelectedStocks([]);
}
}}
disabled={!recordingRestriction.canEditStock}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</th>
)}
<th>
Persediaan
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Jumlah Pakai
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th>
)}
</tr>
</thead>
<tbody>
{formik.values.stocks?.map((stock, idx) => (
<tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='align-middle!'> <th>
<CheckboxInput <CheckboxInput
name={`stock-${idx}`} name='select-all-stocks'
checked={selectedStocks.includes(idx)} checked={
formik.values.stocks?.length ===
selectedStocks.length &&
formik.values.stocks?.length > 0
}
onChange={( onChange={(
e: React.ChangeEvent<HTMLInputElement> e: React.ChangeEvent<HTMLInputElement>
) => { ) => {
if (e.target.checked) { if (e.target.checked) {
setSelectedStocks([...selectedStocks, idx]);
} else {
setSelectedStocks( setSelectedStocks(
selectedStocks.filter((i) => i !== idx) formik.values.stocks?.map((_, idx) => idx) ??
[]
); );
} else {
setSelectedStocks([]);
} }
}} }}
disabled={!recordingRestriction.canEditStock} disabled={!recordingRestriction.canEditStock}
@@ -2531,169 +2494,221 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
}} }}
/> />
</td> </th>
)} )}
<td> <th>
<SelectInput Persediaan
required <span
key={`stock-product-${idx}-${stock.product_warehouse_id}`} className='tooltip tooltip-error tooltip-bottom '
value={ data-tip='required'
unifiedStockProducts.find( >
(product) => <span className='text-error'>*</span>
product.value === stock.product_warehouse_id </span>
) || null </th>
} <th>
onChange={(selectedOption) => { Jumlah Pakai
const option = selectedOption as OptionType | null; <span
formik.setFieldValue( className='tooltip tooltip-error tooltip-bottom '
`stocks.${idx}.product_warehouse_id`, data-tip='required'
option?.value || 0 >
); <span className='text-error'>*</span>
}} </span>
options={getAvailableStockProductOptions(idx)} </th>
placeholder={
!formik.values.project_flock_kandang_id
? 'Pilih kandang terlebih dahulu'
: 'Pilih Produk'
}
isLoading={isLoadingStockProducts}
onMenuScrollToBottom={loadMoreStockProducts}
isError={
isRepeaterInputError(
'stocks',
'product_warehouse_id',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'stocks',
'product_warehouse_id',
idx
).errorMessage
}
className={{
wrapper: 'w-full min-w-48',
}}
isSearchable
isDisabled={
type === 'detail' ||
!formik.values.project_flock_kandang_id ||
!recordingRestriction.canEditStock
}
isClearable={type !== 'detail'}
inputPrefix={
stock.product_warehouse_id
? getProductFlagBadgeAdornment(
stock.product_warehouse_id
)
: undefined
}
/>
</td>
<td>
<div className='flex flex-col gap-1'>
<NumberInput
required
name={`stocks.${idx}.qty`}
value={stock.qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'qty', idx)
.isError || Boolean(getStockUsageError(idx))
}
errorMessage={
isRepeaterInputError('stocks', 'qty', idx)
.errorMessage ||
getStockUsageError(idx) ||
undefined
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan jumlah pakai'
inputSuffix={
stock.product_warehouse_id
? getProductUomSuffix(
stock.product_warehouse_id,
'stock'
)
: null
}
disabled={
type === 'detail' ||
!recordingRestriction.canEditStock
}
/>
{getStockUsageAdornment(idx)}
</div>
</td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td> <th>Action</th>
<div className='flex items-center'>
<Button
type='button'
color='error'
onClick={() => removeStock(idx)}
disabled={!recordingRestriction.canEditStock}
>
<Icon
icon='mdi:trash-can'
width={24}
height={24}
/>
</Button>
</div>
</td>
)} )}
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table> {formik.values.stocks?.map((stock, idx) => (
</div> <tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && ( {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<div className='flex justify-center items-center mt-4 gap-4'> <td className='align-middle!'>
{selectedStocks.length > 0 && <CheckboxInput
recordingRestriction.canEditStock && ( name={`stock-${idx}`}
checked={selectedStocks.includes(idx)}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedStocks([...selectedStocks, idx]);
} else {
setSelectedStocks(
selectedStocks.filter((i) => i !== idx)
);
}
}}
disabled={!recordingRestriction.canEditStock}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</td>
)}
<td>
<SelectInput
required
key={`stock-product-${idx}-${stock.product_warehouse_id}`}
value={
unifiedStockProducts.find(
(product) =>
product.value === stock.product_warehouse_id
) || null
}
onChange={(selectedOption) => {
const option =
selectedOption as OptionType | null;
formik.setFieldValue(
`stocks.${idx}.product_warehouse_id`,
option?.value || 0
);
}}
options={getAvailableStockProductOptions(idx)}
placeholder={
!formik.values.project_flock_kandang_id
? 'Pilih kandang terlebih dahulu'
: 'Pilih Produk'
}
isLoading={isLoadingStockProducts}
onMenuScrollToBottom={loadMoreStockProducts}
isError={
isRepeaterInputError(
'stocks',
'product_warehouse_id',
idx
).isError
}
errorMessage={
isRepeaterInputError(
'stocks',
'product_warehouse_id',
idx
).errorMessage
}
className={{
wrapper: 'w-full min-w-48',
}}
isSearchable
isDisabled={
type === 'detail' ||
!formik.values.project_flock_kandang_id ||
!recordingRestriction.canEditStock
}
isClearable={type !== 'detail'}
inputPrefix={
stock.product_warehouse_id
? getProductFlagBadgeAdornment(
stock.product_warehouse_id
)
: undefined
}
/>
</td>
<td>
<div className='flex flex-col gap-1'>
<NumberInput
required
name={`stocks.${idx}.qty`}
value={stock.qty ?? ''}
onChange={handleStockUsageQtyChangeWrapper(idx)}
onBlur={formik.handleBlur}
decimalScale={0}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
isError={
isRepeaterInputError('stocks', 'qty', idx)
.isError || Boolean(getStockUsageError(idx))
}
errorMessage={
isRepeaterInputError('stocks', 'qty', idx)
.errorMessage ||
getStockUsageError(idx) ||
undefined
}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-24',
}}
placeholder='Masukkan jumlah pakai'
inputSuffix={
stock.product_warehouse_id
? getProductUomSuffix(
stock.product_warehouse_id,
'stock'
)
: null
}
disabled={
type === 'detail' ||
!recordingRestriction.canEditStock
}
/>
{getStockUsageAdornment(idx)}
</div>
</td>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td>
<div className='flex items-center'>
<Button
type='button'
color='error'
onClick={() => removeStock(idx)}
disabled={!recordingRestriction.canEditStock}
>
<Icon
icon='mdi:trash-can'
width={24}
height={24}
/>
</Button>
</div>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<div className='flex justify-center items-center mt-4 gap-4'>
{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 <Button
type='button' type='button'
color='error' color='success'
onClick={removeSelectedStocks} onClick={addStock}
disabled={selectedStocks.length === 0}
className='w-fit' className='w-fit'
disabled={!recordingRestriction.canEditStock}
> >
<Icon icon='mdi:trash-can' width={24} height={24} /> <Icon icon='ic:round-plus' width={24} height={24} />
Hapus Terpilih ({selectedStocks.length}) Tambah Stok
</Button> </Button>
)} </Tooltip>
<Tooltip </div>
content={ )}
!recordingRestriction.canEditStock </Card>
? 'Stock tidak dapat ditambahkan pada masa transisi Laying' )}
: ''
}
position='top'
>
<Button
type='button'
color='success'
onClick={addStock}
className='w-fit'
disabled={!recordingRestriction.canEditStock}
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah Stok
</Button>
</Tooltip>
</div>
)}
</Card>
{/* Transition Warning Banner -- MOVED UP -- */} {/* Transition Warning Banner -- MOVED UP -- */}
{isTransitionPeriod && ( {isTransitionPeriod && (