mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-web-client!474
This commit is contained in:
@@ -15,8 +15,8 @@ const ExpenseDetailPage = () => {
|
|||||||
const expenseId = searchParams.get('expenseId');
|
const expenseId = searchParams.get('expenseId');
|
||||||
|
|
||||||
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
const { data: expense, isLoading: isLoadingExpense } = useSWR(
|
||||||
expenseId,
|
['expense-detail', expenseId],
|
||||||
(id: number) => ExpenseApi.getSingle(id)
|
([_, id]) => ExpenseApi.getSingle(Number(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!expenseId) {
|
if (!expenseId) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { useSWRConfig } from 'swr';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
@@ -46,6 +48,11 @@ const ExpenseRequestContent = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const returnTo = getExpenseListReturnTo(searchParams);
|
const returnTo = getExpenseListReturnTo(searchParams);
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
|
const refreshExpense = () => {
|
||||||
|
mutate((key) => Array.isArray(key) && key[0] === 'expense-detail');
|
||||||
|
};
|
||||||
|
|
||||||
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
|
||||||
useApprovalSteps({
|
useApprovalSteps({
|
||||||
@@ -95,17 +102,24 @@ const ExpenseRequestContent = ({
|
|||||||
!isLatestApprovalRejected &&
|
!isLatestApprovalRejected &&
|
||||||
initialValues?.latest_approval.step_number === 4;
|
initialValues?.latest_approval.step_number === 4;
|
||||||
|
|
||||||
|
const isExpensePaidOff = initialValues?.is_paid;
|
||||||
|
|
||||||
|
const showPaidOffButton =
|
||||||
|
!isExpensePaidOff && (initialValues?.latest_approval.step_number ?? 0) >= 4;
|
||||||
|
|
||||||
// Modal hooks
|
// Modal hooks
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const completeModal = useModal();
|
const completeModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
|
|
||||||
// Modal loading state
|
// Modal loading state
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
const [isCompleteLoading, setIsCompleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
|
|
||||||
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
const formik = useFormik<UploadRequestDocumentsFormValues>({
|
||||||
@@ -146,7 +160,31 @@ const ExpenseRequestContent = ({
|
|||||||
rejectModal.openModal();
|
rejectModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
// Modal confirm click handler
|
// Modal confirm click handler
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
initialValues?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpense();
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
@@ -388,6 +426,24 @@ const ExpenseRequestContent = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
color='success'
|
||||||
|
onClick={paidOffClickHandler}
|
||||||
|
className='w-full sm:w-fit'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<RequirePermission permissions='lti.expense.update'>
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
@@ -533,6 +589,19 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status Lunas</th>
|
||||||
|
<th>:</th>
|
||||||
|
<td>
|
||||||
|
<StatusBadge
|
||||||
|
color={initialValues?.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={initialValues?.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Dokumen Pengajuan</th>
|
<th>Dokumen Pengajuan</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
@@ -752,6 +821,21 @@ const ExpenseRequestContent = ({
|
|||||||
onClick: confirmationModalRejectClickHandler,
|
onClick: confirmationModalRejectClickHandler,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import ButtonFilter from '@/components/helper/ButtonFilter';
|
|||||||
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal';
|
||||||
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
|
import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
@@ -87,10 +88,12 @@ const RowOptionsMenu = ({
|
|||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
|
paidOffClickHandler,
|
||||||
}: {
|
}: {
|
||||||
popoverPosition: 'bottom' | 'top';
|
popoverPosition: 'bottom' | 'top';
|
||||||
props: CellContext<Expense, unknown>;
|
props: CellContext<Expense, unknown>;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
|
paidOffClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const popoverId = `expense#${props.row.original.id}`;
|
const popoverId = `expense#${props.row.original.id}`;
|
||||||
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
|
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
|
||||||
@@ -112,6 +115,11 @@ const RowOptionsMenu = ({
|
|||||||
props.row.original.latest_approval.step_number === 4
|
props.row.original.latest_approval.step_number === 4
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
const showPaidOffButton = props.row.original.latest_approval
|
||||||
|
? props.row.original.latest_approval.step_number >= 4 &&
|
||||||
|
!props.row.original.is_paid
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<PopoverButton
|
<PopoverButton
|
||||||
@@ -179,6 +187,28 @@ const RowOptionsMenu = ({
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPaidOffButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
paidOffClickHandler();
|
||||||
|
closePopover();
|
||||||
|
}}
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 justify-start text-sm font-semibold w-full'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:check-circle-outline'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='text-success'
|
||||||
|
/>
|
||||||
|
Tandai Lunas
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
<RequirePermission permissions='lti.expense.delete'>
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -264,6 +294,7 @@ const ExpensesTable = () => {
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const approveModal = useModal();
|
const approveModal = useModal();
|
||||||
const rejectModal = useModal();
|
const rejectModal = useModal();
|
||||||
|
const paidOffModal = useModal();
|
||||||
const bulkApproveFormModal = useModal();
|
const bulkApproveFormModal = useModal();
|
||||||
const exportProgressInputModal = useModal();
|
const exportProgressInputModal = useModal();
|
||||||
|
|
||||||
@@ -276,6 +307,7 @@ const ExpensesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isPaidOffLoading, setIsPaidOffLoading] = useState(false);
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
@@ -432,6 +464,20 @@ const ExpensesTable = () => {
|
|||||||
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
<ExpenseStatusBadge approval={props.row.original.latest_approval} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Status Lunas',
|
||||||
|
cell: (props) => {
|
||||||
|
return (
|
||||||
|
<StatusBadge
|
||||||
|
color={props.row.original.is_paid ? 'primary' : 'warning'}
|
||||||
|
text={props.row.original.is_paid ? 'Lunas' : 'Belum Lunas'}
|
||||||
|
className={{
|
||||||
|
badge: 'w-fit whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Aksi',
|
header: 'Aksi',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -447,11 +493,17 @@ const ExpensesTable = () => {
|
|||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const paidOffClickHandler = () => {
|
||||||
|
setSelectedExpense(props.row.original);
|
||||||
|
paidOffModal.openModal();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenu
|
<RowOptionsMenu
|
||||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||||
props={props}
|
props={props}
|
||||||
deleteClickHandler={deleteClickHandler}
|
deleteClickHandler={deleteClickHandler}
|
||||||
|
paidOffClickHandler={paidOffClickHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -593,6 +645,29 @@ const ExpensesTable = () => {
|
|||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmationModalPaidOffClickHandler = async () => {
|
||||||
|
setIsPaidOffLoading(true);
|
||||||
|
|
||||||
|
const paidOffResponse = await ExpenseApi.setExpensePaidOff(
|
||||||
|
selectedExpense?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseSuccess(paidOffResponse)) {
|
||||||
|
refreshExpenses();
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
toast.success('Berhasil menandai biaya operasional sebagai lunas!');
|
||||||
|
refreshExpenses();
|
||||||
|
} else {
|
||||||
|
paidOffModal.closeModal();
|
||||||
|
toast.error(
|
||||||
|
'Gagal menandai biaya operasional sebagai lunas!: ' +
|
||||||
|
paidOffResponse?.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPaidOffLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const confirmationModalApproveClickHandler = async (notes: string) => {
|
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||||
setIsApproveLoading(true);
|
setIsApproveLoading(true);
|
||||||
|
|
||||||
@@ -1105,6 +1180,21 @@ const ExpensesTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
ref={paidOffModal.ref}
|
||||||
|
type='success'
|
||||||
|
text='Apakah anda yakin ingin menandai biaya operasional ini sebagai lunas?'
|
||||||
|
secondaryButton={{
|
||||||
|
text: 'Tidak',
|
||||||
|
}}
|
||||||
|
primaryButton={{
|
||||||
|
text: 'Ya',
|
||||||
|
color: 'success',
|
||||||
|
isLoading: isPaidOffLoading,
|
||||||
|
onClick: confirmationModalPaidOffClickHandler,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmationModalWithNotes
|
<ConfirmationModalWithNotes
|
||||||
ref={approveModal.ref}
|
ref={approveModal.ref}
|
||||||
type='success'
|
type='success'
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import StockLogFilterModal from '@/components/pages/inventory/product/detail/StockLogFilterModal';
|
||||||
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
||||||
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@@ -12,6 +19,35 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
const { state: filterState, updateFilter } = useTableFilter<{
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}>({
|
||||||
|
initial: {
|
||||||
|
warehouse_ids: [],
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-product-stock-log-filter',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredProductWarehouses = useMemo(() => {
|
||||||
|
const warehouses = inventoryProduct?.product_warehouses ?? [];
|
||||||
|
if (!filterState.warehouse_ids?.length) return warehouses;
|
||||||
|
const selectedIds = new Set(filterState.warehouse_ids.map((w) => w.value));
|
||||||
|
return warehouses.filter((pw) => selectedIds.has(pw.warehouse_id));
|
||||||
|
}, [inventoryProduct?.product_warehouses, filterState.warehouse_ids]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}) => {
|
||||||
|
updateFilter('warehouse_ids', values.warehouse_ids, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResetHandler = () => {
|
||||||
|
updateFilter('warehouse_ids', [], true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
<FormHeader
|
<FormHeader
|
||||||
@@ -104,13 +140,28 @@ const InventoryProductDetail = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
||||||
{inventoryProduct?.product_warehouses?.map((productWarehouse) => (
|
<div className='flex justify-end'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onClick={filterModal.openModal}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filteredProductWarehouses.map((productWarehouse) => (
|
||||||
<StockLogTable
|
<StockLogTable
|
||||||
key={productWarehouse.id}
|
key={productWarehouse.id}
|
||||||
productWarehouse={productWarehouse}
|
productWarehouse={productWarehouse}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
|
<StockLogFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
productWarehouses={inventoryProduct?.product_warehouses ?? []}
|
||||||
|
initialValues={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onSubmit={filterSubmitHandler}
|
||||||
|
onReset={filterResetHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { RefObject, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface StockLogFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
productWarehouses: ProductWarehouseStock[];
|
||||||
|
initialValues: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
};
|
||||||
|
onSubmit: (values: { warehouse_ids: OptionType<number>[] }) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockLogFilterModal = ({
|
||||||
|
ref,
|
||||||
|
productWarehouses,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: StockLogFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const warehouseOptions: OptionType<number>[] = productWarehouses.map(
|
||||||
|
(pw) => ({
|
||||||
|
label: pw.warehouse_name,
|
||||||
|
value: pw.warehouse_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: (values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({ values: { warehouse_ids: [] } });
|
||||||
|
onReset();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal ref={ref} className={{ modalBox: 'p-0 rounded-xl' }}>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formikResetHandler}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Stock Log</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Gudang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
value={formik.values.warehouse_ids}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('warehouse_ids', val as OptionType<number>[])
|
||||||
|
}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockLogFilterModal;
|
||||||
@@ -9,7 +9,7 @@ import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { FileDown } from 'lucide-react';
|
import { FileDown } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
||||||
@@ -80,6 +80,23 @@ const StockLogTable = ({
|
|||||||
productWarehouse: ProductWarehouseStock;
|
productWarehouse: ProductWarehouseStock;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExportLoading, setIsExportLoading] = useState(false);
|
const [isExportLoading, setIsExportLoading] = useState(false);
|
||||||
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (containerRef.current) observer.observe(containerRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
@@ -108,7 +125,9 @@ const StockLogTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
||||||
`${StockLogApi.basePath}${getTableFilterQueryString()}`,
|
hasBeenVisible
|
||||||
|
? `${StockLogApi.basePath}${getTableFilterQueryString()}`
|
||||||
|
: null,
|
||||||
StockLogApi.getAllFetcher
|
StockLogApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,46 +136,48 @@ const StockLogTable = ({
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div ref={containerRef}>
|
||||||
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
<Card
|
||||||
collapsible
|
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
||||||
variant='bordered'
|
collapsible
|
||||||
className={{
|
variant='bordered'
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-end px-6 pt-4'>
|
|
||||||
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
|
||||||
<FileDown size={16} />
|
|
||||||
Export Excel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Table<StockLog>
|
|
||||||
data={stockLogs}
|
|
||||||
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
|
||||||
page={tableFilterState.page ?? 0}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
isLoading={isLoadingStockLogs}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(stockLogsResponse)
|
|
||||||
? stockLogsResponse.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-4 mb-0',
|
wrapper: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Card>
|
<div className='flex justify-end px-6 pt-4'>
|
||||||
|
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
||||||
|
<FileDown size={16} />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table<StockLog>
|
||||||
|
data={stockLogs}
|
||||||
|
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
||||||
|
page={tableFilterState.page ?? 0}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoadingStockLogs}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mt-4 mb-0',
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -866,7 +866,8 @@ const RecordingTable = () => {
|
|||||||
<>
|
<>
|
||||||
<span>
|
<span>
|
||||||
{props.row.original.day} (Minggu ke-
|
{props.row.original.day} (Minggu ke-
|
||||||
{props.row.original.project_flock.production_standart.week})
|
{props.row.original.week} hari ke-
|
||||||
|
{props.row.original.excess_days})
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+35
-23
@@ -40,6 +40,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
? transferToLayingResponse.data
|
? transferToLayingResponse.data
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isTransferToLayingApproved =
|
||||||
|
transferToLaying?.approval.step_number === 2;
|
||||||
|
|
||||||
const { data: transferToLayingApprovalResponse } = useSWR(
|
const { data: transferToLayingApprovalResponse } = useSWR(
|
||||||
transferToLayingId
|
transferToLayingId
|
||||||
? ['approval-transfer-to-laying', transferToLayingId]
|
? ['approval-transfer-to-laying', transferToLayingId]
|
||||||
@@ -55,9 +58,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
const detailModal = useModal();
|
const detailModal = useModal();
|
||||||
|
|
||||||
const totalEnteredChickenForTransfer =
|
const maxSourceQuantity =
|
||||||
transferToLaying?.sources.reduce(
|
transferToLaying?.sources.reduce(
|
||||||
(acc, item) => acc + Number(item.qty),
|
(acc, item) => acc + Number(item.product_warehouse.quantity),
|
||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
@@ -67,8 +70,9 @@ const TransferToLayingDetailModal = () => {
|
|||||||
0
|
0
|
||||||
) ?? 0;
|
) ?? 0;
|
||||||
|
|
||||||
|
// Sisa transfer = Max available dari kandang asal - Total yang sudah diisi di kandang tujuan
|
||||||
const totalAvailableChickenForTransfer =
|
const totalAvailableChickenForTransfer =
|
||||||
totalEnteredChickenForTransfer - totalTransferedChicken;
|
maxSourceQuantity - totalTransferedChicken;
|
||||||
|
|
||||||
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
const closeModalHandler = (shouldPushToRoute: boolean = true) => {
|
||||||
if (shouldPushToRoute) {
|
if (shouldPushToRoute) {
|
||||||
@@ -161,11 +165,34 @@ const TransferToLayingDetailModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span className='tooltip tooltip-error' data-tip='required'>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{!isTransferToLayingApproved && (
|
||||||
|
<>
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.sources.length === 0 && (
|
{transferToLaying?.sources.length === 0 && (
|
||||||
@@ -225,21 +252,6 @@ const TransferToLayingDetailModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0 ? 'error' : 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{transferToLaying?.targets.length === 0 && (
|
{transferToLaying?.targets.length === 0 && (
|
||||||
@@ -304,7 +316,7 @@ const TransferToLayingDetailModal = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
errorMessage={
|
errorMessage={
|
||||||
totalAvailableChickenForTransfer < 0
|
totalAvailableChickenForTransfer < 0
|
||||||
? `Jumlah transfer melebihi ketersediaan (${formatNumber(totalEnteredChickenForTransfer, 'en-US')} ayam)`
|
? `Jumlah transfer melebihi ketersediaan (${formatNumber(maxSourceQuantity, 'en-US')} ayam)`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -822,11 +822,33 @@ const TransferToLayingFormModal = () => {
|
|||||||
|
|
||||||
{/* Source Kandang */}
|
{/* Source Kandang */}
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<span className='w-full py-2 text-xs font-semibold'>
|
<span className='w-fit py-2 text-xs font-semibold flex flex-row items-center gap-3'>
|
||||||
Kandang Asal{' '}
|
<span className='text-nowrap'>
|
||||||
<span className='tooltip tooltip-error' data-tip='required'>
|
Kandang Asal{' '}
|
||||||
<span className='text-error'> *</span>
|
<span
|
||||||
|
className='tooltip tooltip-error'
|
||||||
|
data-tip='required'
|
||||||
|
>
|
||||||
|
<span className='text-error'> *</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div className='w-px h-5 bg-base-content/10' />
|
||||||
|
|
||||||
|
<StatusBadge
|
||||||
|
color={
|
||||||
|
totalAvailableChickenForTransfer < 0
|
||||||
|
? 'error'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
text={`Sisa ayam: ${formatNumber(
|
||||||
|
totalAvailableChickenForTransfer,
|
||||||
|
'en-US'
|
||||||
|
)} ekor`}
|
||||||
|
className={{
|
||||||
|
badge: 'text-nowrap',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockSourceKandangs.length === 0 && (
|
{formik.values.flockSourceKandangs.length === 0 && (
|
||||||
@@ -906,23 +928,6 @@ const TransferToLayingFormModal = () => {
|
|||||||
<span className='text-error'> *</span>
|
<span className='text-error'> *</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='w-px h-5 bg-base-content/10' />
|
|
||||||
|
|
||||||
<StatusBadge
|
|
||||||
color={
|
|
||||||
totalAvailableChickenForTransfer < 0
|
|
||||||
? 'error'
|
|
||||||
: 'neutral'
|
|
||||||
}
|
|
||||||
text={`Sisa transfer: ${formatNumber(
|
|
||||||
totalAvailableChickenForTransfer,
|
|
||||||
'en-US'
|
|
||||||
)} ekor`}
|
|
||||||
className={{
|
|
||||||
badge: 'text-nowrap',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{formik.values.flockDestinationKandangs.length === 0 && (
|
{formik.values.flockDestinationKandangs.length === 0 && (
|
||||||
|
|||||||
@@ -554,7 +554,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
cell: (props) => formatNumber(props.row.original.qty),
|
cell: (props) => formatNumber(props.row.original.qty),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_qty
|
{summaryTotal?.total_qty
|
||||||
? formatNumber(summaryTotal.total_qty)
|
? formatNumber(summaryTotal.total_qty)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -567,7 +567,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'average_weight_kg',
|
accessorKey: 'average_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_weight_kg
|
{summaryTotal?.average_weight_kg
|
||||||
? formatNumber(summaryTotal.average_weight_kg)
|
? formatNumber(summaryTotal.average_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -580,7 +580,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'total_weight_kg',
|
accessorKey: 'total_weight_kg',
|
||||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_weight_kg
|
{summaryTotal?.total_weight_kg
|
||||||
? formatNumber(summaryTotal.total_weight_kg)
|
? formatNumber(summaryTotal.total_weight_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -593,9 +593,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_price_per_kg',
|
accessorKey: 'sales_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.average_sales_price
|
{summaryTotal?.average_sales_price
|
||||||
? formatNumber(summaryTotal.average_sales_price)
|
? formatCurrency(summaryTotal.average_sales_price)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -606,7 +606,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'hpp_price_per_kg',
|
accessorKey: 'hpp_price_per_kg',
|
||||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_hpp_price_per_kg
|
{summaryTotal?.total_hpp_price_per_kg
|
||||||
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
? formatCurrency(summaryTotal.total_hpp_price_per_kg)
|
||||||
: '-'}
|
: '-'}
|
||||||
@@ -619,7 +619,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
|
|||||||
accessorKey: 'sales_amount',
|
accessorKey: 'sales_amount',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
cell: (props) => formatCurrency(props.row.original.sales_amount),
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='font-semibold text-gray-900'>
|
||||||
{summaryTotal?.total_sales_amount
|
{summaryTotal?.total_sales_amount
|
||||||
? formatCurrency(summaryTotal.total_sales_amount)
|
? formatCurrency(summaryTotal.total_sales_amount)
|
||||||
: '-'}
|
: '-'}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -160,13 +160,7 @@ export function ListDailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item: DailyChecklist) => {
|
const handleEdit = (item: DailyChecklist) => {
|
||||||
const formattedDate = new Date(item.date).toISOString().split('T')[0];
|
router.push(`/daily-checklist/daily-checklist?checklistId=${item.id}`);
|
||||||
const kandangId = item.kandang?.id ?? '';
|
|
||||||
const category = item.category;
|
|
||||||
|
|
||||||
router.push(
|
|
||||||
`/daily-checklist/daily-checklist?date=${formattedDate}&kandang_id=${kandangId}&category=${category}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = (item: DailyChecklist) => {
|
const handleApprove = (item: DailyChecklist) => {
|
||||||
|
|||||||
+1
@@ -113,6 +113,7 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
|
empty_kandang: 'Kandang Kosong',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
DailyChecklistReport,
|
DailyChecklistReport,
|
||||||
DetailDailyChecklist,
|
DetailDailyChecklist,
|
||||||
|
UpdateDailyChecklistPayload,
|
||||||
} from '@/types/api/daily-checklist/daily-checklist';
|
} from '@/types/api/daily-checklist/daily-checklist';
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -16,12 +17,39 @@ import { toast } from 'sonner';
|
|||||||
export class DailyChecklistApiService extends BaseApiService<
|
export class DailyChecklistApiService extends BaseApiService<
|
||||||
DailyChecklist,
|
DailyChecklist,
|
||||||
CreateDailyChecklistPayload,
|
CreateDailyChecklistPayload,
|
||||||
unknown
|
UpdateDailyChecklistPayload
|
||||||
> {
|
> {
|
||||||
constructor(basePath: string = '/daily-checklists') {
|
constructor(basePath: string = '/daily-checklists') {
|
||||||
super(basePath);
|
super(basePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(id: number, payload: UpdateDailyChecklistPayload) {
|
||||||
|
const isFormData =
|
||||||
|
typeof FormData !== 'undefined' && payload instanceof FormData;
|
||||||
|
try {
|
||||||
|
const updatePath = `${this.basePath}/${id}`;
|
||||||
|
|
||||||
|
const headers = isFormData
|
||||||
|
? { ...(this.header ?? {}) }
|
||||||
|
: { 'Content-Type': 'application/json', ...(this.header ?? {}) };
|
||||||
|
|
||||||
|
const updateRes = await httpClient<BaseApiResponse<DailyChecklist>>(
|
||||||
|
updatePath,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: payload,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return updateRes;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<DailyChecklist>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getOneDailyChecklist(id: string) {
|
async getOneDailyChecklist(id: string) {
|
||||||
try {
|
try {
|
||||||
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
|
const getOneDailyChecklistPath = `${this.basePath}/relation/${id}`;
|
||||||
|
|||||||
@@ -572,6 +572,25 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setExpensePaidOff(id: number) {
|
||||||
|
try {
|
||||||
|
const res = await httpClient<BaseApiResponse<Expense>>(
|
||||||
|
`${this.basePath}/${id}/pay`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<Expense>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async deleteExpenseRequestDocument(
|
async deleteExpenseRequestDocument(
|
||||||
expenseId: number,
|
expenseId: number,
|
||||||
documentId: number
|
documentId: number
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type BaseDailyChecklist = {
|
|||||||
category: string;
|
category: string;
|
||||||
date: string;
|
date: string;
|
||||||
empty_kandang?: boolean;
|
empty_kandang?: boolean;
|
||||||
|
empty_kandang_end_date?: string | null;
|
||||||
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
||||||
total_phase: number;
|
total_phase: number;
|
||||||
total_activity: number;
|
total_activity: number;
|
||||||
@@ -59,8 +60,11 @@ export type CreateDailyChecklistPayload = {
|
|||||||
category: string;
|
category: string;
|
||||||
status: string;
|
status: string;
|
||||||
empty_kandang: boolean;
|
empty_kandang: boolean;
|
||||||
|
empty_kandang_end_date?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateDailyChecklistPayload = CreateDailyChecklistPayload;
|
||||||
|
|
||||||
export type PerformanceOverviewItem = {
|
export type PerformanceOverviewItem = {
|
||||||
employee_id: number;
|
employee_id: number;
|
||||||
employee_name: string;
|
employee_name: string;
|
||||||
|
|||||||
Vendored
+1
@@ -50,6 +50,7 @@ export type BaseExpense = {
|
|||||||
total_pengajuan: number;
|
total_pengajuan: number;
|
||||||
total_realisasi: number;
|
total_realisasi: number;
|
||||||
latest_approval: BaseApproval;
|
latest_approval: BaseApproval;
|
||||||
|
is_paid: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Expense = BaseMetadata & BaseExpense;
|
export type Expense = BaseMetadata & BaseExpense;
|
||||||
|
|||||||
+2
@@ -51,6 +51,8 @@ export type BaseRecording = {
|
|||||||
day: number;
|
day: number;
|
||||||
is_transition: boolean;
|
is_transition: boolean;
|
||||||
is_laying: boolean;
|
is_laying: boolean;
|
||||||
|
week: number;
|
||||||
|
excess_days: number;
|
||||||
} & ProductionMetrics;
|
} & ProductionMetrics;
|
||||||
|
|
||||||
export type RecordingDepletion = {
|
export type RecordingDepletion = {
|
||||||
|
|||||||
+9
-1
@@ -1,4 +1,8 @@
|
|||||||
import { BaseMetadata, CreatedUser } from '@/types/api/api-general';
|
import {
|
||||||
|
BaseApproval,
|
||||||
|
BaseMetadata,
|
||||||
|
CreatedUser,
|
||||||
|
} from '@/types/api/api-general';
|
||||||
import { BaseKandang } from '@/types/api/master-data/kandang';
|
import { BaseKandang } from '@/types/api/master-data/kandang';
|
||||||
import { WarehouseType } from '@/types/api/master-data/warehouse';
|
import { WarehouseType } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
@@ -27,6 +31,10 @@ export type BaseTransferToLaying = {
|
|||||||
};
|
};
|
||||||
qty: number;
|
qty: number;
|
||||||
product_warehouse: {
|
product_warehouse: {
|
||||||
|
id: number;
|
||||||
|
product_id: number;
|
||||||
|
warehouse_id: number;
|
||||||
|
quantity: number;
|
||||||
product: {
|
product: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user