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;
}, [
initialValues,
@@ -1053,6 +1061,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
selectedLocation,
selectedProjectFlock,
selectedKandang,
recordingRestriction.canEditStock,
recordingRestriction.canEditDepletion,
]);
const formik = useFormik<
@@ -2443,86 +2453,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</div>
)}
{/* Stocks Table */}
<Card
title='Stok Persediaan'
className={{
wrapper: 'w-full mb-4 shadow',
title: 'mb-4',
}}
>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>
<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}`}>
{/* Stocks Table - Only show if can edit stock or has data */}
{(recordingRestriction.canEditStock ||
(type === 'detail' && formik.values.stocks?.length > 0)) && (
<Card
title='Stok Persediaan'
className={{
wrapper: 'w-full mb-4 shadow',
title: 'mb-4',
}}
>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='align-middle!'>
<th>
<CheckboxInput
name={`stock-${idx}`}
checked={selectedStocks.includes(idx)}
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([...selectedStocks, idx]);
} else {
setSelectedStocks(
selectedStocks.filter((i) => i !== idx)
formik.values.stocks?.map((_, idx) => idx) ??
[]
);
} else {
setSelectedStocks([]);
}
}}
disabled={!recordingRestriction.canEditStock}
@@ -2531,169 +2494,221 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
checkbox: 'checkbox checkbox-sm',
}}
/>
</td>
</th>
)}
<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>
<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' && (
<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>
<th>Action</th>
)}
</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 && (
</thead>
<tbody>
{formik.values.stocks?.map((stock, idx) => (
<tr key={`stock-${idx}`}>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<td className='align-middle!'>
<CheckboxInput
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
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>
)}
<Tooltip
content={
!recordingRestriction.canEditStock
? '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>
</Tooltip>
</div>
)}
</Card>
)}
{/* Transition Warning Banner -- MOVED UP -- */}
{isTransitionPeriod && (