mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'hotfix/adjustment-recording-fifo-stock' into 'development'
[HOTFIX/FE] Adjustment FIFO Stock V2 (Stock Related Usage) See merge request mbugroup/lti-web-client!351
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -26,6 +26,10 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
|||||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||||
@@ -38,6 +42,62 @@ import {
|
|||||||
AdjustmentFilterType,
|
AdjustmentFilterType,
|
||||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
const RowOptionsMenu = ({
|
||||||
|
popoverPosition = 'bottom',
|
||||||
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
|
}: {
|
||||||
|
popoverPosition: 'bottom' | 'top';
|
||||||
|
props: CellContext<InventoryAdjustment, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
|
}) => {
|
||||||
|
const popoverId = `adjustment#${props.row.original.id}`;
|
||||||
|
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
|
||||||
|
|
||||||
|
const closePopover = () => {
|
||||||
|
document.getElementById(popoverId)?.hidePopover();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative'>
|
||||||
|
<PopoverButton
|
||||||
|
tabIndex={0}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
popoverTarget={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
id={popoverId}
|
||||||
|
anchorName={popoverAnchorName}
|
||||||
|
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||||
|
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||||
|
<RequirePermission permissions='lti.inventory.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const InventoryAdjustmentTable = () => {
|
const InventoryAdjustmentTable = () => {
|
||||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||||
@@ -182,12 +242,39 @@ const InventoryAdjustmentTable = () => {
|
|||||||
formik.validateForm();
|
formik.validateForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
const {
|
||||||
|
data: inventoryAdjustments,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshAdjustments,
|
||||||
|
} = useSWR(
|
||||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||||
InventoryAdjustmentApi.getAllFetcher
|
InventoryAdjustmentApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleDeleteHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
const response = await InventoryAdjustmentApi.delete(
|
||||||
|
selectedAdjustment?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
singleDeleteModal.closeModal();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Adjustment!');
|
||||||
|
refreshAdjustments();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Adjustment');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [selectedAdjustment, setSelectedAdjustment] = useState<
|
||||||
|
InventoryAdjustment | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const singleDeleteModal = useModal();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateFilter('search', searchValue);
|
updateFilter('search', searchValue);
|
||||||
@@ -302,8 +389,39 @@ const InventoryAdjustmentTable = () => {
|
|||||||
header: 'Oleh',
|
header: 'Oleh',
|
||||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Aksi',
|
||||||
|
cell: (props: CellContext<InventoryAdjustment, unknown>) => {
|
||||||
|
const currentPageSize =
|
||||||
|
props.table.getPaginationRowModel().rows.length;
|
||||||
|
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||||
|
const currentRowRelativeIndex =
|
||||||
|
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||||
|
|
||||||
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
const deleteClickHandler = () => {
|
||||||
|
setSelectedAdjustment(props.row.original);
|
||||||
|
singleDeleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowOptionsMenu
|
||||||
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[tableFilterState.pageSize, tableFilterState.page]
|
[
|
||||||
|
tableFilterState.pageSize,
|
||||||
|
tableFilterState.page,
|
||||||
|
singleDeleteModal,
|
||||||
|
setSelectedAdjustment,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateSortingFilter = useCallback(
|
const updateSortingFilter = useCallback(
|
||||||
@@ -532,6 +650,21 @@ const InventoryAdjustmentTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={singleDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: singleDeleteHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR, { mutate } from 'swr';
|
||||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||||
@@ -41,9 +43,11 @@ import {
|
|||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
|
deleteClickHandler,
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Movement, unknown>;
|
props: CellContext<Movement, unknown>;
|
||||||
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const popoverId = `movement#${props.row.original.id}`;
|
const popoverId = `movement#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||||
@@ -83,6 +87,20 @@ const RowOptionsMenu = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.inventory.transfer.delete'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deleteClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='error'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,12 +224,37 @@ const MovementTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [selectedMovement, setSelectedMovement] = useState<
|
||||||
|
Movement | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const singleDeleteModal = useModal();
|
||||||
|
|
||||||
const { data: movements, isLoading } = useSWR(
|
const {
|
||||||
|
data: movements,
|
||||||
|
isLoading,
|
||||||
|
mutate: refreshMovements,
|
||||||
|
} = useSWR(
|
||||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||||
MovementApi.getAllFetcher
|
MovementApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleDeleteHandler = async () => {
|
||||||
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
|
const response = await MovementApi.delete(selectedMovement?.id as number);
|
||||||
|
|
||||||
|
singleDeleteModal.closeModal();
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Movement!');
|
||||||
|
refreshMovements();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Movement');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateFilter('search', searchValue);
|
updateFilter('search', searchValue);
|
||||||
}, [searchValue, updateFilter]);
|
}, [searchValue, updateFilter]);
|
||||||
@@ -275,16 +318,27 @@ const MovementTable = () => {
|
|||||||
|
|
||||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||||
|
|
||||||
|
const deleteClickHandler = () => {
|
||||||
|
setSelectedMovement(props.row.original);
|
||||||
|
singleDeleteModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
props={props}
|
props={props}
|
||||||
|
deleteClickHandler={deleteClickHandler}
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[tableFilterState.pageSize, tableFilterState.page]
|
[
|
||||||
|
tableFilterState.pageSize,
|
||||||
|
tableFilterState.page,
|
||||||
|
singleDeleteModal,
|
||||||
|
setSelectedMovement,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -455,6 +509,21 @@ const MovementTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={singleDeleteModal.ref}
|
||||||
|
type='error'
|
||||||
|
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'error',
|
||||||
|
isLoading: isDeleteLoading,
|
||||||
|
onClick: singleDeleteHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: number;
|
warehouse_id: number;
|
||||||
warehouse_name: string;
|
warehouse_name: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
transfer_available_qty?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== USE SELECT HOOKS =====
|
// ===== USE SELECT HOOKS =====
|
||||||
@@ -379,6 +380,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: formik.values.source_warehouse_id
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
? formik.values.source_warehouse_id.toString()
|
? formik.values.source_warehouse_id.toString()
|
||||||
: '',
|
: '',
|
||||||
|
transfer_context: 'inventory_transfer',
|
||||||
|
stock_mode: 'exclude_chickin',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -391,6 +394,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
warehouse_id: pw.warehouse.id,
|
warehouse_id: pw.warehouse.id,
|
||||||
warehouse_name: pw.warehouse.name,
|
warehouse_name: pw.warehouse.name,
|
||||||
quantity: pw.quantity,
|
quantity: pw.quantity,
|
||||||
|
transfer_available_qty: pw.transfer_available_qty,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
}, [productWarehouses]);
|
}, [productWarehouses]);
|
||||||
@@ -834,6 +838,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}, [formik.values.products, formik.values.deliveries]);
|
}, [formik.values.products, formik.values.deliveries]);
|
||||||
|
|
||||||
const getAvailableStock = useCallback(
|
const getAvailableStock = useCallback(
|
||||||
|
(productId: number) => {
|
||||||
|
if (type === 'detail') return 0;
|
||||||
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
|
(pw) => pw.product_id === productId
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
productWarehouse?.transfer_available_qty ??
|
||||||
|
productWarehouse?.quantity ??
|
||||||
|
0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[productWarehouseOptions, type]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTotalStock = useCallback(
|
||||||
(productId: number) => {
|
(productId: number) => {
|
||||||
if (type === 'detail') return 0;
|
if (type === 'detail') return 0;
|
||||||
const productWarehouse = productWarehouseOptions.find(
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
@@ -844,6 +864,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
[productWarehouseOptions, type]
|
[productWarehouseOptions, type]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hasAvailableQty = useCallback(
|
||||||
|
(productId: number) => {
|
||||||
|
const productWarehouse = productWarehouseOptions.find(
|
||||||
|
(pw) => pw.product_id === productId
|
||||||
|
);
|
||||||
|
return productWarehouse?.transfer_available_qty !== undefined;
|
||||||
|
},
|
||||||
|
[productWarehouseOptions]
|
||||||
|
);
|
||||||
|
|
||||||
const getProductQtyBottomLabel = useCallback(
|
const getProductQtyBottomLabel = useCallback(
|
||||||
(productIdx: number) => {
|
(productIdx: number) => {
|
||||||
if (type === 'detail') return undefined;
|
if (type === 'detail') return undefined;
|
||||||
@@ -851,16 +881,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return undefined;
|
if (!product || !product.product_id) return undefined;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
|
const totalStock = getTotalStock(product.product_id);
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
const remainingStock = availableStock - requestedQty;
|
const remainingStock = availableStock - requestedQty;
|
||||||
|
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||||
|
|
||||||
if (requestedQty > 0) {
|
if (requestedQty > 0) {
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
|
||||||
|
}
|
||||||
return `Sisa: ${formatNumber(remainingStock)}`;
|
return `Sisa: ${formatNumber(remainingStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
|
||||||
|
}
|
||||||
|
|
||||||
return `Tersedia: ${formatNumber(availableStock)}`;
|
return `Tersedia: ${formatNumber(availableStock)}`;
|
||||||
},
|
},
|
||||||
[formik.values.products, getAvailableStock, type]
|
[
|
||||||
|
formik.values.products,
|
||||||
|
getAvailableStock,
|
||||||
|
getTotalStock,
|
||||||
|
hasAvailableQty,
|
||||||
|
type,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||||
@@ -922,15 +967,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
if (!product || !product.product_id) return null;
|
if (!product || !product.product_id) return null;
|
||||||
|
|
||||||
const availableStock = getAvailableStock(product.product_id);
|
const availableStock = getAvailableStock(product.product_id);
|
||||||
|
const totalStock = getTotalStock(product.product_id);
|
||||||
const requestedQty = Number(product.product_qty) || 0;
|
const requestedQty = Number(product.product_qty) || 0;
|
||||||
|
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||||
|
|
||||||
if (requestedQty > availableStock) {
|
if (requestedQty > availableStock) {
|
||||||
|
if (isAyamProduct) {
|
||||||
|
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
|
||||||
|
}
|
||||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[formik.values.products, getAvailableStock, type]
|
[
|
||||||
|
formik.values.products,
|
||||||
|
getAvailableStock,
|
||||||
|
getTotalStock,
|
||||||
|
hasAvailableQty,
|
||||||
|
type,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const validateDeliveryQty = useCallback(
|
const validateDeliveryQty = useCallback(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
|||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import PopoverButton from '@/components/popover/PopoverButton';
|
import PopoverButton from '@/components/popover/PopoverButton';
|
||||||
import PopoverContent from '@/components/popover/PopoverContent';
|
import PopoverContent from '@/components/popover/PopoverContent';
|
||||||
|
import Tooltip from '@/components/Tooltip';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { LocationApi } 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 RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -105,30 +107,75 @@ const RowOptionsMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isRecordingEditable = (recording: Recording) => {
|
const isRecordingEditable = (recording: Recording) => {
|
||||||
if (
|
const isGrowingCategory =
|
||||||
recording.executed_at &&
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
recording.project_flock?.project_flock_category === 'GROWING'
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
) {
|
if (isGrowingLockedByLaying) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
const restriction = getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
|
||||||
|
if (restriction.isLocked) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||||
|
const isGrowingCategory =
|
||||||
|
recording.project_flock?.project_flock_category === 'GROWING';
|
||||||
|
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||||
|
if (isGrowingLockedByLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIsLaying =
|
||||||
|
recording.project_flock?.project_flock_category === 'LAYING';
|
||||||
|
|
||||||
|
return getRecordingRestriction(
|
||||||
|
recording.is_laying,
|
||||||
|
recording.is_transition,
|
||||||
|
currentIsLaying
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const isApproved = isRecordingApproved(props.row.original);
|
const isApproved = isRecordingApproved(props.row.original);
|
||||||
const isRejected = isRecordingRejected(props.row.original);
|
const isRejected = isRecordingRejected(props.row.original);
|
||||||
const isEditable = isRecordingEditable(props.row.original);
|
const isEditable = isRecordingEditable(props.row.original);
|
||||||
|
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<PopoverButton
|
<Tooltip
|
||||||
tabIndex={0}
|
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||||
variant='ghost'
|
position='top'
|
||||||
color='none'
|
|
||||||
popoverTarget={popoverId}
|
|
||||||
anchorName={popoverAnchorName}
|
|
||||||
>
|
>
|
||||||
<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='material-symbols:more-vert' width={16} height={16} />
|
||||||
|
</PopoverButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
id={popoverId}
|
id={popoverId}
|
||||||
@@ -560,12 +607,17 @@ const RecordingTable = () => {
|
|||||||
const singleDeleteHandler = async () => {
|
const singleDeleteHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await RecordingApi.delete(selectedRecording?.id as number);
|
const response = await RecordingApi.delete(selectedRecording?.id as number);
|
||||||
refreshRecordings();
|
|
||||||
|
|
||||||
singleDeleteModal.closeModal();
|
singleDeleteModal.closeModal();
|
||||||
toast.success('Successfully delete Recording!');
|
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|
||||||
|
if (isResponseSuccess(response)) {
|
||||||
|
toast.success(response?.message || 'Successfully delete Recording!');
|
||||||
|
refreshRecordings();
|
||||||
|
} else {
|
||||||
|
toast.error(response?.message || 'Failed to delete Recording');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveHandler = async (notes: string) => {
|
const approveHandler = async (notes: string) => {
|
||||||
@@ -761,11 +813,30 @@ const RecordingTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
|
const isTransition = props.row.original.is_transition;
|
||||||
const category =
|
const category =
|
||||||
props.row.original.project_flock?.project_flock_category;
|
props.row.original.project_flock?.project_flock_category ||
|
||||||
if (!category) return '-';
|
'GROWING';
|
||||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
|
||||||
|
const isGrowingLocked =
|
||||||
|
category === 'GROWING' && props.row.original.is_laying;
|
||||||
|
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{isGrowingLocked && (
|
||||||
|
<span className='text-xs text-error font-medium'>
|
||||||
|
(Penguncian)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
export type RecordingRestriction = {
|
||||||
|
canEditStock: boolean;
|
||||||
|
canEditDepletion: boolean;
|
||||||
|
canEditEgg: boolean;
|
||||||
|
isLocked: boolean;
|
||||||
|
lockReason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecordingRestriction = (
|
||||||
|
isLaying: boolean,
|
||||||
|
isTransition: boolean,
|
||||||
|
currentIsLaying?: boolean
|
||||||
|
): RecordingRestriction => {
|
||||||
|
if (isTransition && !isLaying) {
|
||||||
|
const isLayingKandangInTransition = currentIsLaying === true;
|
||||||
|
|
||||||
|
if (isLayingKandangInTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: true,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaying && !isTransition && currentIsLaying) {
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason:
|
||||||
|
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isLaying && !isTransition) {
|
||||||
|
return {
|
||||||
|
canEditStock: true,
|
||||||
|
canEditDepletion: true,
|
||||||
|
canEditEgg: true,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canEditStock: false,
|
||||||
|
canEditDepletion: false,
|
||||||
|
canEditEgg: false,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason: 'Kondisi transisi tidak valid',
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -555,6 +555,12 @@ export const APPROVAL_WORKFLOWS = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PROJECT_FLOCK_STATUS = {
|
||||||
|
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
|
||||||
|
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
|
||||||
|
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const ACCEPTED_FILE_TYPE = {
|
export const ACCEPTED_FILE_TYPE = {
|
||||||
PDF: {
|
PDF: {
|
||||||
'application/pdf': ['.pdf'],
|
'application/pdf': ['.pdf'],
|
||||||
|
|||||||
+11
@@ -9,8 +9,19 @@ export type BaseProductWarehouse = {
|
|||||||
warehouse_id: number;
|
warehouse_id: number;
|
||||||
uom: Uom;
|
uom: Uom;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
transfer_available_qty?: number;
|
||||||
product: Product;
|
product: Product;
|
||||||
warehouse: Warehouse;
|
warehouse: Warehouse;
|
||||||
|
project_flock_kandang?: {
|
||||||
|
id: number;
|
||||||
|
project_flock_id: number;
|
||||||
|
kandang_id: number;
|
||||||
|
period: number;
|
||||||
|
project_flock?: {
|
||||||
|
id: number;
|
||||||
|
flock_name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
week?: number | null;
|
week?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
@@ -74,6 +74,8 @@ export type ProjectFlockKandangLookup = {
|
|||||||
available_quantity?: number;
|
available_quantity?: number;
|
||||||
population: number;
|
population: number;
|
||||||
chick_in_date: string;
|
chick_in_date: string;
|
||||||
|
is_transition: boolean;
|
||||||
|
is_laying: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProjectFlockAvailableQuantity = {
|
export type ProjectFlockAvailableQuantity = {
|
||||||
|
|||||||
+2
-1
@@ -49,7 +49,8 @@ export type BaseRecording = {
|
|||||||
project_flock: ProjectFlock;
|
project_flock: ProjectFlock;
|
||||||
record_datetime: string;
|
record_datetime: string;
|
||||||
day: number;
|
day: number;
|
||||||
executed_at: string;
|
is_transition: boolean;
|
||||||
|
is_laying: boolean;
|
||||||
} & ProductionMetrics;
|
} & ProductionMetrics;
|
||||||
|
|
||||||
export type RecordingDepletion = {
|
export type RecordingDepletion = {
|
||||||
|
|||||||
Reference in New Issue
Block a user