mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
651 lines
21 KiB
TypeScript
651 lines
21 KiB
TypeScript
'use client';
|
|
|
|
import Button from '@/components/Button';
|
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|
import Modal, { useModal } from '@/components/Modal';
|
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
|
import Table from '@/components/Table';
|
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
|
import { TableToolbar } from '@/components/table/TableToolbar';
|
|
import { ROWS_OPTIONS } from '@/config/constant';
|
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
|
import {
|
|
MarketingApi,
|
|
SalesOrderApi,
|
|
} from '@/services/api/marketing/marketing';
|
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
|
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
|
|
import { Icon } from '@iconify/react';
|
|
import { CellContext, Row } from '@tanstack/react-table';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useCallback, useState } from 'react';
|
|
import toast from 'react-hot-toast';
|
|
import useSWR from 'swr';
|
|
import RequirePermission from '@/components/helper/RequirePermission';
|
|
import { useAuth } from '@/services/hooks/useAuth';
|
|
|
|
const RowsOptionsMenu = ({
|
|
type = 'dropdown',
|
|
props,
|
|
deleteClickHandler,
|
|
deliveryClickHandler,
|
|
}: {
|
|
type: 'dropdown' | 'collapse';
|
|
props: CellContext<Marketing, unknown>;
|
|
deleteClickHandler: () => void;
|
|
deliveryClickHandler?: () => void;
|
|
}) => {
|
|
return (
|
|
<div
|
|
tabIndex={type === 'dropdown' ? 0 : undefined}
|
|
className={cn(
|
|
{
|
|
'dropdown-content': type === 'dropdown',
|
|
'mt-2': type === 'collapse',
|
|
},
|
|
'p-2.5 mr-2 bg-base-100 rounded-box z-10 border border-black/10 shadow'
|
|
)}
|
|
>
|
|
<div className='flex flex-col gap-1'>
|
|
<RequirePermission permissions='lti.marketing.delivery_order.detail'>
|
|
<Button
|
|
href={`/marketing/detail?marketingId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='primary'
|
|
className='justify-start text-sm'
|
|
>
|
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
|
Detail
|
|
</Button>
|
|
</RequirePermission>
|
|
{props.row.original.latest_approval.step_number != 1 && (
|
|
<RequirePermission
|
|
permissions={
|
|
props.row.original.latest_approval.step_number == 3
|
|
? 'lti.marketing.delivery_order.update'
|
|
: 'lti.marketing.delivery_order.create'
|
|
}
|
|
>
|
|
<Button
|
|
href={
|
|
props.row.original.latest_approval.step_number == 3
|
|
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
|
|
: props.row.original.latest_approval.step_number == 2
|
|
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
|
|
: undefined
|
|
}
|
|
onClick={() => {
|
|
if (props.row.original.latest_approval.step_number == 2) {
|
|
deliveryClickHandler?.();
|
|
}
|
|
}}
|
|
variant='ghost'
|
|
color='success'
|
|
className='justify-start text-sm'
|
|
>
|
|
<Icon icon='mdi:truck' width={16} height={16} />
|
|
Deliver
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
{props.row.original.latest_approval.step_number != 3 && (
|
|
<RequirePermission permissions='lti.marketing.sales_order.update'>
|
|
<Button
|
|
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
|
|
variant='ghost'
|
|
color='warning'
|
|
className='justify-start text-sm'
|
|
>
|
|
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
<RequirePermission permissions='lti.marketing.sales_order.delete'>
|
|
<Button
|
|
onClick={deleteClickHandler}
|
|
variant='ghost'
|
|
color='error'
|
|
className='text-error hover:text-inherit justify-start text-sm'
|
|
>
|
|
<Icon icon='mdi:delete-outline' width={16} height={16} />
|
|
Delete
|
|
</Button>
|
|
</RequirePermission>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MarketingTable = () => {
|
|
const [search, setSearch] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
|
|
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
|
|
'APPROVED'
|
|
);
|
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
|
const { permissionCheck } = useAuth();
|
|
|
|
const router = useRouter();
|
|
|
|
const {
|
|
data: marketing,
|
|
isLoading: isLoadingMarketing,
|
|
mutate: refreshMarketing,
|
|
} = useSWR(MarketingApi.basePath, MarketingApi.getAllFetcher);
|
|
|
|
const deleteModal = useModal();
|
|
const confirmationModal = useModal();
|
|
const productsModal = useModal();
|
|
const deliveryModal = useModal();
|
|
|
|
const searchChangeHandler = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setSearch(e.target.value);
|
|
setPage(1);
|
|
},
|
|
[]
|
|
);
|
|
const pageSizeChangeHandler = useCallback(
|
|
(val: OptionType | OptionType[] | null) => {
|
|
const newVal = val as OptionType;
|
|
setPageSize(newVal.value as number);
|
|
setPage(1);
|
|
},
|
|
[]
|
|
);
|
|
|
|
const approveClickHandler = () => {
|
|
setApproveAction('APPROVED');
|
|
confirmationModal.openModal();
|
|
};
|
|
|
|
const rejectClickHandler = () => {
|
|
setApproveAction('REJECTED');
|
|
confirmationModal.openModal();
|
|
};
|
|
|
|
const productsClickHandler = (item: Marketing) => {
|
|
setSelectedItem(item);
|
|
productsModal.openModal();
|
|
};
|
|
|
|
const deleteMarketingHandler = async () => {
|
|
const deleteMarketingRes = await MarketingApi.delete(
|
|
selectedItem?.id as number
|
|
);
|
|
if (isResponseSuccess(deleteMarketingRes)) {
|
|
confirmationModal.closeModal();
|
|
toast.success(deleteMarketingRes?.message as string);
|
|
}
|
|
if (isResponseError(deleteMarketingRes)) {
|
|
confirmationModal.closeModal();
|
|
toast.error(deleteMarketingRes?.message as string);
|
|
}
|
|
refreshMarketing();
|
|
deleteModal.closeModal();
|
|
};
|
|
|
|
const allData = isResponseSuccess(marketing) ? marketing.data : [];
|
|
const selectedRowsData = allData.filter(
|
|
(row) => rowSelection[row.id.toString()]
|
|
);
|
|
|
|
const hasApprovable = selectedRowsData.some(
|
|
(row) =>
|
|
row.latest_approval.step_number === 1 &&
|
|
row.latest_approval.action !== 'REJECTED'
|
|
);
|
|
const hasRejectable = selectedRowsData.some(
|
|
(row) =>
|
|
row.latest_approval.step_number === 1 &&
|
|
row.latest_approval.action !== 'REJECTED'
|
|
);
|
|
|
|
const disableApprove = !hasApprovable;
|
|
const disableReject = !hasRejectable;
|
|
|
|
const idsToProcess =
|
|
approveAction === 'APPROVED'
|
|
? selectedRowsData
|
|
.filter((row) => row.latest_approval.step_number === 1)
|
|
.map((row) => row.id)
|
|
: selectedRowsData
|
|
.filter((row) => row.latest_approval.step_number === 2)
|
|
.map((row) => row.id);
|
|
|
|
const approveMarketingHandler = async (notes: string) => {
|
|
let idsToProcess: number[] = [];
|
|
|
|
idsToProcess = selectedRowsData
|
|
.filter((row) => row.latest_approval.step_number === 1)
|
|
.map((row) => row.id);
|
|
|
|
if (idsToProcess.length === 0) {
|
|
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
|
confirmationModal.closeModal();
|
|
return;
|
|
}
|
|
|
|
const approveMarketingRes = await SalesOrderApi.bulkApprovals(
|
|
idsToProcess,
|
|
approveAction,
|
|
notes
|
|
);
|
|
|
|
if (isResponseSuccess(approveMarketingRes)) {
|
|
confirmationModal.closeModal();
|
|
toast.success(approveMarketingRes?.message as string);
|
|
setRowSelection({});
|
|
}
|
|
if (isResponseError(approveMarketingRes)) {
|
|
confirmationModal.closeModal();
|
|
toast.error(approveMarketingRes?.message as string);
|
|
}
|
|
refreshMarketing();
|
|
};
|
|
|
|
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
|
const res = await SalesOrderApi.delivery(selectedItem?.id as number, notes);
|
|
deliveryModal.closeModal();
|
|
toast.success(res?.message as string);
|
|
refreshMarketing?.();
|
|
router.push(
|
|
`/marketing/detail/delivery-orders/edit?marketingId=${selectedItem?.id}`
|
|
);
|
|
};
|
|
|
|
const {
|
|
state: tableFilterState,
|
|
updateFilter,
|
|
toQueryString: getTableFilterToQueryString,
|
|
} = useTableFilter({
|
|
initial: {
|
|
search: '',
|
|
},
|
|
paramMap: {
|
|
page: 'page',
|
|
pageSize: 'limit',
|
|
},
|
|
});
|
|
|
|
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
|
const approval = row.original.latest_approval;
|
|
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className='flex flex-col gap-4'>
|
|
<div className='flex flex-col gap-2 mb-4'>
|
|
<TableToolbar
|
|
addButton={
|
|
permissionCheck('lti.marketing.sales_order.create')
|
|
? {
|
|
href: '/marketing/add/sales-orders',
|
|
label: 'Tambah Sales Order',
|
|
}
|
|
: undefined
|
|
}
|
|
search={{
|
|
value: search,
|
|
onChange: searchChangeHandler,
|
|
placeholder: 'Cari Sales Order',
|
|
}}
|
|
/>
|
|
<div className='flex flex-row gap-2'>
|
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
|
<Button
|
|
color='success'
|
|
onClick={approveClickHandler}
|
|
className='justify-start text-sm'
|
|
disabled={disableApprove}
|
|
>
|
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
Approve
|
|
</Button>
|
|
</RequirePermission>
|
|
|
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
|
<Button
|
|
color='error'
|
|
onClick={rejectClickHandler}
|
|
className='justify-start text-sm'
|
|
disabled={disableReject}
|
|
>
|
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
|
Reject
|
|
</Button>
|
|
</RequirePermission>
|
|
</div>
|
|
<TableRowSizeSelector
|
|
value={pageSize}
|
|
onChange={pageSizeChangeHandler}
|
|
options={ROWS_OPTIONS}
|
|
>
|
|
{/* select multiple product */}
|
|
<SelectInput
|
|
label='Product'
|
|
isClearable
|
|
placeholder='Pilih product'
|
|
options={[]}
|
|
isMulti
|
|
/>
|
|
{/* select status */}
|
|
<SelectInput
|
|
label='Status'
|
|
isClearable
|
|
placeholder='Pilih status'
|
|
options={[]}
|
|
/>
|
|
{/* select customer */}
|
|
<SelectInput
|
|
label='Customer'
|
|
isClearable
|
|
placeholder='Pilih customer'
|
|
options={[]}
|
|
/>
|
|
</TableRowSizeSelector>
|
|
</div>
|
|
<Table
|
|
rowSelection={rowSelection}
|
|
setRowSelection={setRowSelection}
|
|
data={allData}
|
|
columns={[
|
|
{
|
|
id: 'select',
|
|
header: ({ table }) => {
|
|
const allRows = table.getRowModel().rows;
|
|
const selectableRows = allRows.filter(getRowCanSelect);
|
|
|
|
const allSelected =
|
|
selectableRows.length > 0 &&
|
|
selectableRows.every((row) => row.getIsSelected());
|
|
|
|
const someSelected =
|
|
selectableRows.some((row) => row.getIsSelected()) &&
|
|
!allSelected;
|
|
|
|
const toggleSelectableRows = () => {
|
|
const shouldSelect = !allSelected;
|
|
selectableRows.forEach((row) =>
|
|
row.toggleSelected(shouldSelect)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className='w-full flex flex-row justify-center'>
|
|
<CheckboxInput
|
|
name='allRow'
|
|
checked={allSelected}
|
|
indeterminate={someSelected}
|
|
onChange={toggleSelectableRows}
|
|
disabled={selectableRows.length === 0}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const canSelect = getRowCanSelect(row);
|
|
return (
|
|
<div>
|
|
<CheckboxInput
|
|
name='row'
|
|
checked={row.getIsSelected()}
|
|
disabled={!canSelect}
|
|
indeterminate={row.getIsSomeSelected()}
|
|
onChange={row.getToggleSelectedHandler()}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'so_number',
|
|
header: 'No. Order',
|
|
},
|
|
{
|
|
accessorKey: 'so_date',
|
|
header: 'Tanggal',
|
|
cell: (props) => {
|
|
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'latest_approval.step_name',
|
|
header: 'Status',
|
|
},
|
|
{
|
|
accessorKey: 'customer.name',
|
|
header: 'Customer',
|
|
},
|
|
{
|
|
accessorFn: (row) =>
|
|
row.sales_order
|
|
?.map((product) => product.total_price)
|
|
.reduce((a, b) => a + b, 0) ?? 0,
|
|
header: 'Grand Total',
|
|
cell: (props) => {
|
|
return formatCurrency(
|
|
props.row.original?.sales_order
|
|
?.map((product) => product.total_price)
|
|
.reduce((a, b) => a + b, 0) ?? 0
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'marketing_products.length',
|
|
header: 'Product Details',
|
|
cell: (props) => {
|
|
if (props?.row?.original?.sales_order?.length) {
|
|
if (props?.row?.original?.sales_order?.length > 1) {
|
|
return (
|
|
<Button
|
|
variant='link'
|
|
color='success'
|
|
className='p-0 text-none'
|
|
onClick={() => {
|
|
productsClickHandler(props?.row?.original);
|
|
}}
|
|
>
|
|
Lihat {props?.row?.original?.sales_order?.length} Produk
|
|
</Button>
|
|
);
|
|
} else {
|
|
const product = props?.row?.original?.sales_order[0];
|
|
return <>{product?.product_warehouse?.product?.name}</>;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
header: 'Aksi',
|
|
cell: (props) => {
|
|
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 = () => {
|
|
setSelectedItem(props.row.original);
|
|
deleteModal.openModal();
|
|
};
|
|
|
|
const deliveryClickHandler = () => {
|
|
setSelectedItem(props.row.original);
|
|
deliveryModal.openModal();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{currentPageSize > 2 && (
|
|
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
|
<RowsOptionsMenu
|
|
type='dropdown'
|
|
props={props}
|
|
deleteClickHandler={deleteClickHandler}
|
|
deliveryClickHandler={deliveryClickHandler}
|
|
/>
|
|
</RowDropdownOptions>
|
|
)}
|
|
|
|
{currentPageSize <= 2 && (
|
|
<RowCollapseOptions>
|
|
<RowsOptionsMenu
|
|
type='collapse'
|
|
props={props}
|
|
deleteClickHandler={deleteClickHandler}
|
|
deliveryClickHandler={deliveryClickHandler}
|
|
/>
|
|
</RowCollapseOptions>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
]}
|
|
pageSize={pageSize}
|
|
page={page}
|
|
onPageChange={setPage}
|
|
className={{
|
|
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',
|
|
}}
|
|
/>
|
|
</div>
|
|
<ConfirmationModal
|
|
ref={deleteModal.ref}
|
|
type='error'
|
|
text={`Apakah anda yakin ingin menghapus data Project Flock ini?`}
|
|
secondaryButton={{
|
|
text: 'Tidak',
|
|
}}
|
|
primaryButton={{
|
|
text: 'Ya',
|
|
color: 'error',
|
|
}}
|
|
/>
|
|
|
|
<ConfirmationModalWithNotes
|
|
ref={confirmationModal.ref}
|
|
type={approveAction === 'APPROVED' ? 'success' : 'error'}
|
|
text={`Apakah anda yakin ingin ${approveAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan (${idsToProcess.length} data)?`}
|
|
secondaryButton={{
|
|
text: 'Tidak',
|
|
onClick: confirmationModal.closeModal,
|
|
}}
|
|
primaryButton={{
|
|
text: 'Ya',
|
|
color: approveAction === 'APPROVED' ? 'success' : 'error',
|
|
onClick: approveMarketingHandler,
|
|
}}
|
|
/>
|
|
<ConfirmationModal
|
|
ref={deleteModal.ref}
|
|
type='error'
|
|
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
|
secondaryButton={{
|
|
text: 'Tidak',
|
|
onClick: deleteModal.closeModal,
|
|
}}
|
|
primaryButton={{
|
|
text: 'Ya',
|
|
color: 'error',
|
|
onClick: deleteMarketingHandler,
|
|
}}
|
|
/>
|
|
<ConfirmationModalWithNotes
|
|
ref={deliveryModal.ref}
|
|
type={'success'}
|
|
text={`Apakah anda yakin ingin deliver penjualan ${selectedItem?.so_number}?`}
|
|
secondaryButton={{
|
|
text: 'Tidak',
|
|
}}
|
|
primaryButton={{
|
|
text: 'Ya',
|
|
color: 'success',
|
|
onClick: confirmationModalDeliveryClickHandler,
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
ref={productsModal.ref}
|
|
className={{
|
|
modalBox: 'max-w-2/5 z-100',
|
|
}}
|
|
closeOnBackdrop
|
|
>
|
|
<div className='flex flex-row justify-between items-center mb-3'>
|
|
<h4 className='text-xl font-semibold'>Daftar Produk</h4>
|
|
<Button
|
|
variant='ghost'
|
|
color='error'
|
|
onClick={productsModal.closeModal}
|
|
className='justify-start text-sm rounded-full'
|
|
>
|
|
<Icon icon='mdi:close' width={16} height={16} />
|
|
</Button>
|
|
</div>
|
|
<Table<BaseSalesOrder>
|
|
data={
|
|
isResponseSuccess(marketing) && selectedItem
|
|
? (selectedItem?.sales_order ?? [])
|
|
: []
|
|
}
|
|
columns={[
|
|
{
|
|
header: 'Kandang',
|
|
accessorFn(row) {
|
|
return row.product_warehouse.warehouse.name;
|
|
},
|
|
},
|
|
{
|
|
header: 'Produk',
|
|
accessorFn(row) {
|
|
return row.product_warehouse.product.name;
|
|
},
|
|
},
|
|
{
|
|
header: 'Harga Satuan (Rp)',
|
|
accessorFn(row) {
|
|
return formatCurrency(row.unit_price);
|
|
},
|
|
},
|
|
]}
|
|
className={{
|
|
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',
|
|
paginationClassName: 'hidden',
|
|
}}
|
|
/>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
export default MarketingTable;
|