mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
fix(resolve): fix resolve MR
This commit is contained in:
+265
-67
@@ -5,31 +5,38 @@ import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { 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 { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatVechicleNumber } from '@/lib/helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
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 { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
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';
|
||||
|
||||
const RowsOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
deliveryClickHandler,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Marketing, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
deliveryClickHandler?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
@@ -44,7 +51,7 @@ const RowsOptionsMenu = ({
|
||||
>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Button
|
||||
href={`/marketing/sales-orders/detail/?salesOrderId=${props.row.original.id}`}
|
||||
href={`/marketing/detail?marketingId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
@@ -52,15 +59,39 @@ const RowsOptionsMenu = ({
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href={`/marketing/sales-orders/detail/edit/?salesOrderId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
{props.row.original.latest_approval.step_number != 1 && (
|
||||
<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>
|
||||
)}
|
||||
{props.row.original.latest_approval.step_number != 3 && (
|
||||
<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>
|
||||
)}
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
@@ -75,19 +106,18 @@ const RowsOptionsMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SalesOrderTable = () => {
|
||||
const MarketingTable = () => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const [approveAction, setApproveAction] = useState<
|
||||
'approve' | 'reject' | null
|
||||
>(null);
|
||||
const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const selectedRowIds = Object.keys(rowSelection).filter(
|
||||
(id) => rowSelection[id]
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
data: marketing,
|
||||
@@ -98,6 +128,7 @@ const SalesOrderTable = () => {
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const productsModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
|
||||
const searchChangeHandler = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -116,12 +147,12 @@ const SalesOrderTable = () => {
|
||||
);
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApproveAction('approve');
|
||||
setApproveAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApproveAction('reject');
|
||||
setApproveAction('REJECTED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
@@ -130,6 +161,93 @@ const SalesOrderTable = () => {
|
||||
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
|
||||
);
|
||||
const hasRejectable = selectedRowsData.some(
|
||||
(row) => row.latest_approval.step_number === 2
|
||||
);
|
||||
|
||||
const disableApprove = !hasApprovable || hasRejectable;
|
||||
// const disableReject = !hasRejectable || hasApprovable;
|
||||
|
||||
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[] = [];
|
||||
|
||||
if (approveAction === 'APPROVED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id);
|
||||
} else if (approveAction === 'REJECTED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 2)
|
||||
.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,
|
||||
@@ -144,13 +262,18 @@ const SalesOrderTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||
const step = row.original.latest_approval?.step_number;
|
||||
return step === 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<TableToolbar
|
||||
addButton={{
|
||||
href: '/marketing/sales-orders/add',
|
||||
href: '/marketing/add/sales-orders',
|
||||
label: 'Tambah Sales Order',
|
||||
}}
|
||||
search={{
|
||||
@@ -169,51 +292,75 @@ const SalesOrderTable = () => {
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
className='justify-start text-sm'
|
||||
disabled={!selectedRowIds.length}
|
||||
disabled={disableApprove}
|
||||
>
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
className='justify-start text-sm'
|
||||
disabled={!selectedRowIds.length}
|
||||
disabled={disableReject}
|
||||
>
|
||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={isResponseSuccess(marketing) ? marketing.data : []}
|
||||
data={allData}
|
||||
columns={[
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
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',
|
||||
@@ -222,9 +369,12 @@ const SalesOrderTable = () => {
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
header: 'Tanggal',
|
||||
cell: (props) => {
|
||||
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'approval.step_name',
|
||||
accessorKey: 'latest_approval.step_name',
|
||||
header: 'Status',
|
||||
},
|
||||
{
|
||||
@@ -232,15 +382,25 @@ const SalesOrderTable = () => {
|
||||
header: 'Customer',
|
||||
},
|
||||
{
|
||||
accessorKey: 'grand_total',
|
||||
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?.marketing_products?.length) {
|
||||
if (props?.row?.original?.marketing_products?.length > 1) {
|
||||
if (props?.row?.original?.sales_order?.length) {
|
||||
if (props?.row?.original?.sales_order?.length > 1) {
|
||||
return (
|
||||
<Button
|
||||
variant='link'
|
||||
@@ -250,12 +410,11 @@ const SalesOrderTable = () => {
|
||||
productsClickHandler(props?.row?.original);
|
||||
}}
|
||||
>
|
||||
Lihat {props?.row?.original?.marketing_products?.length}{' '}
|
||||
Produk
|
||||
Lihat {props?.row?.original?.sales_order?.length} Produk
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
const product = props?.row?.original?.marketing_products[0];
|
||||
const product = props?.row?.original?.sales_order[0];
|
||||
return <>{product?.product_warehouse?.product?.name}</>;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +433,15 @@ const SalesOrderTable = () => {
|
||||
const isLast2Rows =
|
||||
currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {};
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedItem(props.row.original);
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
setSelectedItem(props.row.original);
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -284,6 +451,7 @@ const SalesOrderTable = () => {
|
||||
type='dropdown'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
deliveryClickHandler={deliveryClickHandler}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
@@ -294,6 +462,7 @@ const SalesOrderTable = () => {
|
||||
type='collapse'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
deliveryClickHandler={deliveryClickHandler}
|
||||
/>
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
@@ -330,16 +499,45 @@ const SalesOrderTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approveAction === 'approve' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approveAction} data penjualan (${selectedRowIds.length} data)?`}
|
||||
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: approveAction === 'approve' ? 'success' : 'error',
|
||||
color: 'success',
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -361,10 +559,10 @@ const SalesOrderTable = () => {
|
||||
<Icon icon='mdi:close' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<Table<MarketingProduct>
|
||||
<Table<BaseSalesOrder>
|
||||
data={
|
||||
isResponseSuccess(marketing) && selectedItem
|
||||
? (selectedItem?.marketing_products ?? [])
|
||||
? (selectedItem?.sales_order ?? [])
|
||||
: []
|
||||
}
|
||||
columns={[
|
||||
@@ -403,4 +601,4 @@ const SalesOrderTable = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SalesOrderTable;
|
||||
export default MarketingTable;
|
||||
@@ -0,0 +1,461 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import Table from '@/components/Table';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import {
|
||||
BaseDelivery,
|
||||
BaseSalesOrder,
|
||||
Marketing,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||
import DeliveryOrderExport from '../pdf/DeliveryOrderExport';
|
||||
|
||||
const MarketingDetail = ({
|
||||
initialValues,
|
||||
refresh,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
refresh?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState(
|
||||
initialValues?.sales_order
|
||||
?.map((item) => item.total_price)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
const {
|
||||
approvals,
|
||||
isLoading: isLoadingApproval,
|
||||
refresh: refreshApproval,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.latest_approval,
|
||||
approvalLines: MARKETING_APPROVAL_LINE,
|
||||
moduleName: 'MARKETINGS',
|
||||
moduleId: initialValues?.id as number as unknown as string,
|
||||
});
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
// const rejectClickHandler = () => {
|
||||
// setApprovalAction('REJECTED');
|
||||
// confirmationModal.openModal();
|
||||
// };
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||
setIsLoading(true);
|
||||
const res = await SalesOrderApi.singleApproval(
|
||||
initialValues?.id as number,
|
||||
approvalAction,
|
||||
notes
|
||||
);
|
||||
setIsLoading(false);
|
||||
confirmationModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async (notes: string) => {
|
||||
setIsLoading(true);
|
||||
const res = await SalesOrderApi.delivery(
|
||||
initialValues?.id as number,
|
||||
notes
|
||||
);
|
||||
setIsLoading(false);
|
||||
deliveryModal.closeModal();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col w-full gap-4'>
|
||||
<FormHeader title='Detail Sales Order' backUrl='/marketing' />
|
||||
{!isLoadingApproval && approvals && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.latest_approval?.step_number != 3 && (
|
||||
<>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 1}
|
||||
>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 2}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button> */}
|
||||
</>
|
||||
)}
|
||||
{initialValues?.latest_approval?.step_number == 2 && (
|
||||
<Button
|
||||
color='success'
|
||||
href={`/marketing/add/delivery-orders?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:truck' width={24} height={24} />
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title='Informasi Penjualan'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width='45%' className='font-semibold'>
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.customer?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Status</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.latest_approval?.step_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{formatDate(initialValues?.so_date, 'DD MMM yyyy')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Total Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{formatCurrency(grandTotal as number)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Catatan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.notes ?? '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Dokumen</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
<SalesOrderExport data={initialValues} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
{initialValues?.sales_order && (
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<Table<BaseSalesOrder>
|
||||
data={initialValues?.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);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order?.length === 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',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{initialValues?.delivery_order && (
|
||||
<Card
|
||||
title='Informasi Pengiriman'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
{initialValues?.delivery_order.map((delivery, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-row gap-3'>
|
||||
<div className='font-semibold'>
|
||||
Nomor DO : {delivery.do_number}
|
||||
</div>
|
||||
</div>
|
||||
<Table<BaseDelivery>
|
||||
data={delivery.deliveries}
|
||||
columns={[
|
||||
{
|
||||
header: 'Tanggal Pengiriman',
|
||||
accessorFn() {
|
||||
return formatDate(
|
||||
delivery.delivery_date,
|
||||
'DD MMM yyyy'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'No. Polisi',
|
||||
accessorFn(row) {
|
||||
return formatVechicleNumber(row.vehicle_number);
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order?.length === 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',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<div className='flex flex-row gap-3 my-3'>
|
||||
<DeliveryOrderExport
|
||||
data={initialValues}
|
||||
deliveryOrder={delivery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button color='error' onClick={deleteClickHandler}>
|
||||
<Icon icon='mdi:delete' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction === 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModalWithNotes
|
||||
ref={deliveryModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin deliver penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingDetail;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
DeliveryOrderProductFormValues,
|
||||
DeliveryOrderProductSchema,
|
||||
} from './repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
|
||||
type MarketingSchemaType = {
|
||||
customer_id: number | undefined;
|
||||
sales_person_id: number | undefined;
|
||||
customer:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
so_date: string | undefined;
|
||||
notes: string | undefined;
|
||||
};
|
||||
|
||||
type SalesOrderSchemaType = MarketingSchemaType & {
|
||||
sales_order: SalesOrderProductFormValues[];
|
||||
};
|
||||
|
||||
type DeliveryOrderSchemaType = {
|
||||
delivery_order: DeliveryOrderProductFormValues[];
|
||||
};
|
||||
|
||||
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||
Yup.object({
|
||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
|
||||
customer: Yup.object({
|
||||
value: Yup.number().required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
so_date: Yup.string().required('Tanggal wajib diisi!'),
|
||||
notes: Yup.string().required('Catatan wajib diisi!'),
|
||||
sales_order: Yup.array()
|
||||
.of(SalesOrderProductSchema)
|
||||
.min(1, 'Produk wajib diisi!')
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
|
||||
export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
||||
Yup.object({
|
||||
delivery_order: Yup.array()
|
||||
.of(DeliveryOrderProductSchema)
|
||||
.min(1, 'Pengiriman wajib diisi!')
|
||||
.required()
|
||||
.test(
|
||||
'at-least-one-delivery-date',
|
||||
'Minimal ada satu tanggal pengiriman yang harus diisi!',
|
||||
(value) => {
|
||||
if (!value || value.length == 0) {
|
||||
return false;
|
||||
}
|
||||
return value.some(
|
||||
(item) =>
|
||||
item.delivery_date !== null &&
|
||||
item.delivery_date !== undefined &&
|
||||
item.delivery_date !== ''
|
||||
);
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const UpdateSalesOrderSchema = SalesOrderSchema;
|
||||
|
||||
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
|
||||
|
||||
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
||||
@@ -0,0 +1,757 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import {
|
||||
BaseDeliveryOrder,
|
||||
BaseSalesOrder,
|
||||
CreateDeliveryOrderPayload,
|
||||
CreateSalesOrderPayload,
|
||||
CreateSalesOrderProductPayload,
|
||||
Marketing,
|
||||
UpdateDeliveryOrderPayload,
|
||||
UpdateSalesOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
DeliveryOrderFormValues,
|
||||
DeliveryOrderSchema,
|
||||
SalesOrderFormValues,
|
||||
SalesOrderSchema,
|
||||
} from './MarketingForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import {
|
||||
DeliveryOrderApi,
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import { SalesOrderProductFormValues } from './repeater/sales-order/SalesOrderProduct.schema';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import SalesOrderProductTable from './table-view/SalesOrderProductTable';
|
||||
import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm';
|
||||
import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
|
||||
import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
|
||||
import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
|
||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
|
||||
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
id: product.id,
|
||||
vehicle_number: product.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const DeliveryProductToFieldValues = (
|
||||
salesOrders: BaseSalesOrder[],
|
||||
delivery: BaseDeliveryOrder
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
const data = delivery.deliveries.map((item) => {
|
||||
const soId = salesOrders.find(
|
||||
(so) => so.product_warehouse.id === item.product_warehouse.id
|
||||
)?.id;
|
||||
return {
|
||||
id: soId,
|
||||
unit_price: item.unit_price,
|
||||
total_weight: item.total_weight,
|
||||
qty: item.qty,
|
||||
avg_weight: item.avg_weight,
|
||||
total_price: item.total_price,
|
||||
vehicle_number: item.vehicle_number,
|
||||
delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
|
||||
do_number: delivery.do_number,
|
||||
marketing_product_id: soId,
|
||||
marketing_product: {
|
||||
id: soId,
|
||||
vehicle_number: item.vehicle_number,
|
||||
kandang_id: item.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: item.product_warehouse.warehouse.id,
|
||||
label: item.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: item.product_warehouse.id,
|
||||
label: item.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: item.product_warehouse.id,
|
||||
unit_price: item.unit_price,
|
||||
total_weight: item.total_weight,
|
||||
qty: item.qty,
|
||||
avg_weight: item.avg_weight,
|
||||
total_price: item.total_price,
|
||||
},
|
||||
} as DeliveryOrderProductFormValues;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const mergeSOwithDO = (
|
||||
salesOrders: SalesOrderProductFormValues[],
|
||||
deliveryOrders: DeliveryOrderProductFormValues[]
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
return salesOrders.map((so) => {
|
||||
const delivery = deliveryOrders.find(
|
||||
(d) => d?.marketing_product_id === so.id
|
||||
);
|
||||
|
||||
return {
|
||||
...so, // nilai dasar dari sales order
|
||||
marketing_product_id: so.id,
|
||||
delivery_date: delivery?.delivery_date || undefined,
|
||||
do_number: delivery?.do_number || undefined,
|
||||
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
||||
unit_price: delivery?.unit_price ?? so.unit_price,
|
||||
total_weight: delivery?.total_weight ?? so.total_weight,
|
||||
qty: delivery?.qty ?? so.qty,
|
||||
avg_weight: delivery?.avg_weight ?? so.avg_weight,
|
||||
total_price: delivery?.total_price ?? so.total_price,
|
||||
marketing_product: so, // jika ada, override
|
||||
} as DeliveryOrderProductFormValues;
|
||||
});
|
||||
};
|
||||
|
||||
const MarketingForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
afterSubmit,
|
||||
}: {
|
||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
initialValues?: Marketing;
|
||||
afterSubmit?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||
useState<SalesOrderProductFormValues | null>(null);
|
||||
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
|
||||
useState<DeliveryOrderProductFormValues | null>(null);
|
||||
|
||||
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
|
||||
DeliveryOrderProductFormValues[]
|
||||
>(
|
||||
mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
// Repeater Props
|
||||
const addSOModal = useModal();
|
||||
const addDOModal = useModal();
|
||||
const [rowSOSelection, setRowSOSelection] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const selectedRowSOIds = Object.keys(rowSOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
const [rowDOSelection, setRowDOSelection] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const selectedRowDOIds = Object.keys(rowDOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
|
||||
// End Repeater Props
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const formikInitialValues = useMemo<
|
||||
SalesOrderFormValues & DeliveryOrderFormValues
|
||||
>(() => {
|
||||
return {
|
||||
so_date: initialValues?.so_date || undefined,
|
||||
notes: initialValues?.notes || undefined,
|
||||
customer_id: initialValues?.customer?.id || undefined,
|
||||
sales_person_id: initialValues?.sales_person?.id || 1,
|
||||
customer: initialValues?.customer
|
||||
? {
|
||||
value: initialValues.customer.id,
|
||||
label: initialValues.customer.name,
|
||||
}
|
||||
: null,
|
||||
sales_order:
|
||||
initialValues?.sales_order?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
delivery_order: mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
),
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema:
|
||||
formType == 'add_deliver' || formType == 'edit_deliver'
|
||||
? DeliveryOrderSchema
|
||||
: SalesOrderSchema,
|
||||
validateOnMount: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload =
|
||||
formType != 'add_deliver' && formType != 'edit_deliver'
|
||||
? ({
|
||||
customer_id: values.customer_id as number,
|
||||
sales_person_id: values.sales_person_id as number,
|
||||
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
|
||||
notes: values.notes as string,
|
||||
marketing_products: values.sales_order.map((product) => {
|
||||
return {
|
||||
vehicle_number: product.vehicle_number as string,
|
||||
kandang_id: product.kandang_id as number,
|
||||
product_warehouse_id: product.product_warehouse_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
: ({
|
||||
marketing_id: initialValues?.id as number,
|
||||
delivery_products: values.delivery_order
|
||||
.map((product) => {
|
||||
if (Boolean(product.delivery_date)) {
|
||||
return {
|
||||
marketing_product_id:
|
||||
product.marketing_product_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
delivery_date: formatDate(
|
||||
product.delivery_date as string,
|
||||
'yyyy-MM-DD'
|
||||
),
|
||||
vehicle_number: product.vehicle_number,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => Boolean(item)),
|
||||
} as UpdateDeliveryOrderPayload);
|
||||
console.log('PAYLOAD');
|
||||
console.log(payload);
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||
break;
|
||||
case 'edit':
|
||||
await updateMarketingHandler(payload as UpdateSalesOrderPayload);
|
||||
break;
|
||||
case 'add_deliver':
|
||||
await createDeliveryHandler(payload as CreateDeliveryOrderPayload);
|
||||
break;
|
||||
case 'edit_deliver':
|
||||
await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
afterSubmit?.();
|
||||
},
|
||||
});
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return formik.values.sales_order.reduce(
|
||||
(total, product) =>
|
||||
total + parseFloat((product.total_price as string) || '0'),
|
||||
0
|
||||
);
|
||||
}, [formik.values.sales_order]);
|
||||
|
||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const createMarketingRes = await SalesOrderApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
toast.success(createMarketingRes?.message as string);
|
||||
router.push('/marketing');
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
toast.error(createMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const updateMarketingRes = await SalesOrderApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(updateMarketingRes)) {
|
||||
toast.success(updateMarketingRes?.message as string);
|
||||
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
}
|
||||
if (isResponseError(updateMarketingRes)) {
|
||||
toast.error(updateMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const createDeliveryRes = await DeliveryOrderApi.create(values);
|
||||
if (isResponseSuccess(createDeliveryRes)) {
|
||||
console.log(createDeliveryRes);
|
||||
toast.success(createDeliveryRes?.message as string);
|
||||
setDeliveryOrderValues(
|
||||
createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
createDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
);
|
||||
}
|
||||
if (isResponseError(createDeliveryRes)) {
|
||||
console.log(createDeliveryRes);
|
||||
toast.error(createDeliveryRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const updateDeliveryRes = await DeliveryOrderApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
// router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
setDeliveryOrderValues(
|
||||
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
updateDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.error(updateDeliveryRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
initialValues?.id as number
|
||||
);
|
||||
if (isResponseSuccess(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
toast.success(deleteMarketingRes?.message as string);
|
||||
}
|
||||
if (isResponseError(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
toast.error(deleteMarketingRes?.message as string);
|
||||
}
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// Repeater Handle
|
||||
const handleDeleteSO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter((p) => p.id != id)
|
||||
);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleBulkDeleteSO = useCallback(() => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter(
|
||||
(product) => !selectedRowSOIds.includes(product.id ?? -1)
|
||||
)
|
||||
);
|
||||
setRowSOSelection({});
|
||||
}, [formik, selectedRowSOIds]);
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteModal.openModal();
|
||||
}, [deleteModal]);
|
||||
const handleAddSOClick = useCallback(() => {
|
||||
setSelectedMarketingProduct(null);
|
||||
addSOModal.openModal();
|
||||
}, [addSOModal]);
|
||||
const handleAddSubmitSO = useCallback(
|
||||
async (values: SalesOrderProductFormValues) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
|
||||
|
||||
addSOModal.closeModal();
|
||||
},
|
||||
[formik, addSOModal]
|
||||
);
|
||||
|
||||
const handleDeleteDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order;
|
||||
setDeliveryOrderValues((prev) => prev.filter((p) => p.id !== id));
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleEditDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order.find(
|
||||
(product) => product.id == id
|
||||
);
|
||||
setSelectedDeliveryProduct(currentProducts ?? null);
|
||||
addDOModal.openModal();
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
const handleBulkDeleteDO = useCallback(() => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.filter((product) => !selectedRowDOIds.includes(product.id ?? -1))
|
||||
);
|
||||
|
||||
setRowDOSelection({});
|
||||
}, [formik, selectedRowDOIds]);
|
||||
|
||||
const handleAddDOClick = useCallback(() => {
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.openModal();
|
||||
}, [addDOModal]);
|
||||
|
||||
const handleAddSubmitDO = useCallback(
|
||||
async (values: DeliveryOrderProductFormValues) => {
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
setDeliveryOrderValues((prev) => [...prev, newValues]);
|
||||
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
const handleInputDate = useCallback(
|
||||
(newData: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.marketing_product_id == newData.marketing_product_id) {
|
||||
return newData;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.map((product) =>
|
||||
product.id === id ? { ...product, ...values } : product
|
||||
)
|
||||
);
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
// End Repeater Handle
|
||||
|
||||
const memoSalesOrder = formik.values.sales_order;
|
||||
|
||||
useEffect(() => {
|
||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||
}, [deliveryOrderValues, initialValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
>
|
||||
<FormHeader
|
||||
title={`${formType == 'add' || formType == 'add_deliver' ? 'Tambah' : 'Edit'} ${formType === 'add_deliver' || formType === 'edit_deliver' ? 'Delivery' : 'Sales'} Order`}
|
||||
backUrl='/marketing'
|
||||
/>
|
||||
<Card
|
||||
title='Informasi Order'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-3 mt-3'>
|
||||
<SelectInput
|
||||
label='Pelanggan'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={formik.values.customer}
|
||||
onChange={handleChangeCustomer}
|
||||
isError={
|
||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||
}
|
||||
errorMessage={formik.errors.customer_id}
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
isDisabled={
|
||||
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||
}
|
||||
/>
|
||||
<DateInput
|
||||
name='so_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.so_date}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
|
||||
errorMessage={formik.errors.so_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
readOnly={formType == 'add_deliver' || formType == 'edit_deliver'}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{(formType == 'add' || formType == 'edit') && (
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
|
||||
<div className='text-green-500'>{JSON.stringify(formik.values)}</div>
|
||||
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={formType}
|
||||
data={memoSalesOrder}
|
||||
rowSelection={rowSOSelection}
|
||||
setRowSelection={setRowSOSelection}
|
||||
selectedRowIds={selectedRowSOIds}
|
||||
onDelete={handleDeleteSO}
|
||||
onBulkDelete={handleBulkDeleteSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
{(formType == 'add_deliver' || formType == 'edit_deliver') &&
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order.length > 0 && (
|
||||
<Card
|
||||
title='Informasi Pengiriman'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* {JSON.stringify(memoSalesOrder)} */}
|
||||
{/* <small>{JSON.stringify(memoDeliveryOrder)}</small> */}
|
||||
{/* <small className='block text-error'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
formType={formType}
|
||||
data={deliveryOrderValues}
|
||||
salesOrder={memoSalesOrder}
|
||||
rowSelection={rowDOSelection}
|
||||
setRowSelection={setRowDOSelection}
|
||||
selectedRowIds={selectedRowDOIds}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onBulkDelete={handleBulkDeleteDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
onInputDate={handleInputDate}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
required
|
||||
name='notes'
|
||||
label='Catatan'
|
||||
rows={3}
|
||||
placeholder='Masukan catatan penjualan'
|
||||
value={formik.values.notes}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
|
||||
/>
|
||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
||||
<span>Total Penjualan</span>
|
||||
<span className='text-lg font-semibold'>
|
||||
{formatCurrency(grandTotal)}{' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
|
||||
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
isLoading={formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{formType == 'edit' && (
|
||||
<div className='flex flex-row justify-start'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={handleDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
ref={addSOModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addSOModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedSalesOrderProductForm
|
||||
onSubmitForm={handleAddSubmitSO}
|
||||
initialValues={selectedMarketingProduct ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
ref={addDOModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
{selectedDeliveryProduct ? 'Edit' : 'Tambah'} Pengiriman
|
||||
</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addDOModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedDeliveryOrderProductForm
|
||||
salesOrders={initialValues?.sales_order ?? []}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingForm;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '../sales-order/SalesOrderProduct.schema';
|
||||
import { de } from 'react-day-picker/locale';
|
||||
|
||||
type DeliveryOrderProductSchemaType = {
|
||||
id?: number | undefined;
|
||||
marketing_product_id: number | undefined; // Sales Order ID
|
||||
marketing_product?: SalesOrderProductFormValues | undefined | null;
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
vehicle_number: string | undefined;
|
||||
delivery_date: string | undefined | null;
|
||||
do_number?: string | undefined | null; // Uncertain
|
||||
};
|
||||
|
||||
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
|
||||
Yup.object({
|
||||
id: Yup.number(),
|
||||
marketing_product_id: Yup.number()
|
||||
.min(1, 'Produk wajib diisi!')
|
||||
.required('Produk wajib diisi!'),
|
||||
marketing_product: Yup.object().nullable().optional(),
|
||||
unit_price: Yup.number()
|
||||
.min(1, 'Harga Satuan wajib diisi!')
|
||||
.required('Harga Satuan wajib diisi!'),
|
||||
total_weight: Yup.number()
|
||||
.min(0, 'Total Bobot wajib diisi!')
|
||||
.required('Total Bobot wajib diisi!'),
|
||||
qty: Yup.number()
|
||||
.min(1, 'Kuantitas wajib diisi!')
|
||||
.required('Kuantitas wajib diisi!'),
|
||||
avg_weight: Yup.number()
|
||||
.min(0, 'Avg. Bobot wajib diisi!')
|
||||
.required('Avg. Bobot wajib diisi!'),
|
||||
total_price: Yup.number()
|
||||
.min(1, 'Total Penjualan wajib diisi!')
|
||||
.required('Total Penjualan wajib diisi!'),
|
||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||
delivery_date: Yup.string()
|
||||
.required('Tanggal Pengiriman wajib diisi!')
|
||||
.nullable()
|
||||
.optional(),
|
||||
do_number: Yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type DeliveryOrderProductFormValues = Yup.InferType<
|
||||
typeof DeliveryOrderProductSchema
|
||||
>;
|
||||
|
||||
// "marketing_product_id": 3,
|
||||
// "qty": 20,
|
||||
// "unit_price": 1000,
|
||||
// "avg_weight": 1.1,
|
||||
// "total_weight": 220,
|
||||
// "total_price": 20000,
|
||||
// "delivery_date": "2025-11-09",
|
||||
// "vehicle_number": "D 4321 XXX"
|
||||
@@ -0,0 +1,354 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DeliveryOrderProductFormValues,
|
||||
DeliveryOrderProductSchema,
|
||||
} from './DeliverOrderProduct.schema';
|
||||
import { useFormik } from 'formik';
|
||||
import Alert from '@/components/Alert';
|
||||
import Button from '@/components/Button';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import { formatVechicleNumber } from '@/lib/helper';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { SalesOrderProductFormValues } from '../sales-order/SalesOrderProduct.schema';
|
||||
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
const DeliveryOrderProductForm = ({
|
||||
salesOrders,
|
||||
initialValues,
|
||||
onSubmitForm,
|
||||
onUpdateForm,
|
||||
}: {
|
||||
salesOrders: BaseSalesOrder[];
|
||||
initialValues?: DeliveryOrderProductFormValues;
|
||||
onSubmitForm?: (value: DeliveryOrderProductFormValues) => Promise<void>;
|
||||
onUpdateForm?: (
|
||||
id: number,
|
||||
value: DeliveryOrderProductFormValues
|
||||
) => Promise<void>;
|
||||
}) => {
|
||||
const [formikErrorMessage, setFormErrorMessage] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const formik = useFormik<DeliveryOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
delivery_date: initialValues?.delivery_date || undefined,
|
||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
||||
marketing_product_id: initialValues?.marketing_product_id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
marketing_product: initialValues?.marketing_product || undefined,
|
||||
},
|
||||
validationSchema: DeliveryOrderProductSchema,
|
||||
validateOnBlur: true,
|
||||
validateOnChange: false,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (initialValues?.id) {
|
||||
await onUpdateForm?.(initialValues.id, values);
|
||||
} else {
|
||||
await onSubmitForm?.(values);
|
||||
}
|
||||
handleResetForm();
|
||||
},
|
||||
});
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
values: {
|
||||
delivery_date: '',
|
||||
vehicle_number: '',
|
||||
marketing_product_id: undefined,
|
||||
unit_price: '',
|
||||
total_weight: '',
|
||||
qty: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
marketing_product: undefined,
|
||||
},
|
||||
});
|
||||
setSelectedProduct(null);
|
||||
};
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
||||
formik.values;
|
||||
|
||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
||||
formik.setFieldValue('total_price', Number(qty) * Number(unit_price));
|
||||
} else if (qty && total_price && field === 'total_price') {
|
||||
formik.setFieldValue('unit_price', Number(total_price) / Number(qty));
|
||||
}
|
||||
}
|
||||
|
||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
||||
formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight));
|
||||
} else if (qty && total_weight && field === 'total_weight') {
|
||||
formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
id: product.id,
|
||||
vehicle_number: product.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const options = salesOrders.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.product_warehouse.product.name} - ${item.product_warehouse.warehouse.name}`,
|
||||
}));
|
||||
|
||||
const { setValues: setFormikValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setFormikValues(initialValues);
|
||||
const value = salesOrders.find(
|
||||
(item) => item.id === initialValues.marketing_product_id
|
||||
);
|
||||
setSelectedProduct({
|
||||
value: value?.id,
|
||||
label: `${value?.product_warehouse.product.name} - ${value?.product_warehouse.warehouse.name}`,
|
||||
} as OptionType);
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='size-full'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{/* <small className='block text-blue-500'>
|
||||
{JSON.stringify(initialValues)}
|
||||
</small>
|
||||
<small className='block text-red-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small>
|
||||
<small className='block text-emerald-500'>
|
||||
{JSON.stringify(formik.values)}
|
||||
</small>
|
||||
<div className='hidden'>
|
||||
{JSON.stringify(formik.values.marketing_product)}
|
||||
</div> */}
|
||||
|
||||
{formikErrorMessage && (
|
||||
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
|
||||
<Alert color='error'>{formikErrorMessage}</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<SelectInput
|
||||
options={options}
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
isDisabled
|
||||
value={
|
||||
selectedProduct
|
||||
? ({
|
||||
value: selectedProduct?.value,
|
||||
label: salesOrders.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse.product.name,
|
||||
} as OptionType)
|
||||
: null
|
||||
}
|
||||
onChange={(value) => {
|
||||
const selected = value as OptionType;
|
||||
setSelectedProduct(selected);
|
||||
|
||||
const so = salesOrders.find(
|
||||
(item) => item.id === selected?.value
|
||||
);
|
||||
if (!so) {
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: undefined,
|
||||
marketing_product: null,
|
||||
qty: formik.values.qty || '',
|
||||
unit_price: '',
|
||||
total_price: '',
|
||||
avg_weight: '',
|
||||
total_weight: '',
|
||||
vehicle_number: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: selected.value as number,
|
||||
marketing_product: MarketingProductToFieldValues(so),
|
||||
qty: formik.values.qty || so.qty,
|
||||
unit_price: so.unit_price,
|
||||
total_price: so.total_price,
|
||||
avg_weight: so.avg_weight,
|
||||
total_weight: so.total_weight,
|
||||
vehicle_number: so.vehicle_number,
|
||||
});
|
||||
}}
|
||||
startAdornment={
|
||||
selectedProduct && (
|
||||
<Badge
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
className={{ badge: 'whitespace-nowrap font-semibold' }}
|
||||
>
|
||||
{
|
||||
salesOrders.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse?.warehouse?.name
|
||||
}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
isClearable
|
||||
isError={Boolean(formik.errors.marketing_product_id)}
|
||||
errorMessage={formik.errors.marketing_product_id}
|
||||
required
|
||||
/>
|
||||
<DateInput
|
||||
name='delivery_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.delivery_date ?? undefined}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.delivery_date &&
|
||||
Boolean(formik.errors.delivery_date)
|
||||
}
|
||||
errorMessage={formik.errors.delivery_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
required
|
||||
/>
|
||||
|
||||
<PatternInput
|
||||
name='vehicle_number'
|
||||
label='No. Polisi'
|
||||
format='AA #### AAA'
|
||||
mask='_'
|
||||
inputVehicleNumber
|
||||
required
|
||||
type='text'
|
||||
placeholder='B 1234 CDE'
|
||||
value={formatVechicleNumber(formik.values.vehicle_number ?? '')}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={Boolean(formik.errors.vehicle_number)}
|
||||
errorMessage={formik.errors.vehicle_number}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Kuantitas'
|
||||
name='qty'
|
||||
value={formik.values.qty}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('qty')}
|
||||
isError={Boolean(formik.errors.qty)}
|
||||
errorMessage={formik.errors.qty}
|
||||
placeholder='Masukan Kuantitas'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Avg. Bobot (Kg)'
|
||||
name='avg_weight'
|
||||
value={formik.values.avg_weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('avg_weight')}
|
||||
isError={Boolean(formik.errors.avg_weight)}
|
||||
errorMessage={formik.errors.avg_weight}
|
||||
placeholder='Masukan Bobot Rata-rata'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Harga Satuan (Rp)'
|
||||
name='unit_price'
|
||||
value={formik.values.unit_price}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('unit_price')}
|
||||
isError={Boolean(formik.errors.unit_price)}
|
||||
errorMessage={formik.errors.unit_price}
|
||||
placeholder='Masukan Harga Satuan'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Total Bobot (Kg)'
|
||||
name='total_weight'
|
||||
value={formik.values.total_weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('total_weight')}
|
||||
isError={Boolean(formik.errors.total_weight)}
|
||||
errorMessage={formik.errors.total_weight}
|
||||
placeholder='Masukan Total Bobot'
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required
|
||||
label='Total Penjualan (Rp)'
|
||||
name='total_price'
|
||||
value={formik.values.total_price}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={() => handleBlurField('total_price')}
|
||||
isError={Boolean(formik.errors.total_price)}
|
||||
errorMessage={formik.errors.total_price}
|
||||
placeholder='Masukan Total Penjualan'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end gap-3 mt-4'>
|
||||
<Button type='reset' color='warning'>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryOrderProductForm;
|
||||
+10
-12
@@ -1,7 +1,7 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
type MarketingProductSchemaType = {
|
||||
vehicle_number: string | undefined;
|
||||
type SalesOrderProductSchemaType = {
|
||||
id?: number | undefined;
|
||||
kandang_id?: number;
|
||||
kandang?: {
|
||||
value: number;
|
||||
@@ -15,15 +15,15 @@ type MarketingProductSchemaType = {
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
uom: string | undefined | null;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
delivery_date?: string | undefined | null;
|
||||
vehicle_number?: string | undefined;
|
||||
};
|
||||
|
||||
export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType> =
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
Yup.object({
|
||||
vehicle_number: Yup.string().required('No. Polisi wajib diisi!'),
|
||||
id: Yup.number(),
|
||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||
kandang: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
@@ -42,21 +42,19 @@ export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType
|
||||
.min(1, 'Harga Satuan wajib diisi!')
|
||||
.required('Harga Satuan wajib diisi!'),
|
||||
total_weight: Yup.number()
|
||||
.min(1, 'Total Bobot wajib diisi!')
|
||||
.min(0, 'Total Bobot wajib diisi!')
|
||||
.required('Total Bobot wajib diisi!'),
|
||||
qty: Yup.number()
|
||||
.min(1, 'Kuantitas wajib diisi!')
|
||||
.required('Kuantitas wajib diisi!'),
|
||||
uom: Yup.string().nullable(),
|
||||
avg_weight: Yup.number()
|
||||
.min(1, 'Avg. Bobot wajib diisi!')
|
||||
.min(0, 'Avg. Bobot wajib diisi!')
|
||||
.required('Avg. Bobot wajib diisi!'),
|
||||
total_price: Yup.number()
|
||||
.min(1, 'Total Penjualan wajib diisi!')
|
||||
.required('Total Penjualan wajib diisi!'),
|
||||
delivery_date: Yup.string().required().nullable(),
|
||||
});
|
||||
|
||||
export type MarketingProductFormValues = Yup.InferType<
|
||||
typeof MarketingProductSchema
|
||||
export type SalesOrderProductFormValues = Yup.InferType<
|
||||
typeof SalesOrderProductSchema
|
||||
>;
|
||||
+59
-123
@@ -1,17 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
} from './MarketingProduct.schema';
|
||||
import { RefObject, use, useEffect, useRef, useState } from 'react';
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import { RefObject, useState } from 'react';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
@@ -23,37 +17,47 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import Button from '@/components/Button';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import { formatVechicleNumber } from '@/lib/helper';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import Alert from '@/components/Alert';
|
||||
|
||||
const MarketingProductForm = ({
|
||||
const SalesOrderProductForm = ({
|
||||
initialValues,
|
||||
data,
|
||||
modalRef,
|
||||
onSubmitForm,
|
||||
}: {
|
||||
initialValues?: MarketingProduct;
|
||||
data: MarketingProduct[];
|
||||
initialValues?: SalesOrderProductFormValues;
|
||||
modalRef?: RefObject<HTMLDialogElement | null>;
|
||||
onSubmitForm?: (
|
||||
tableValues: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
) => Promise<void>;
|
||||
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
|
||||
}) => {
|
||||
// State
|
||||
const [selectedOptionsKandang, setSelectedOptionsKandang] =
|
||||
useState<OptionType | null>(null);
|
||||
const [selectedOptionsWarehouse, setSelectedOptionsWarehouse] = useState<
|
||||
OptionType | null | undefined
|
||||
>(undefined);
|
||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||
|
||||
// Options Data
|
||||
const formik = useFormik<SalesOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
||||
kandang_id: initialValues?.kandang_id || undefined,
|
||||
kandang: initialValues?.kandang || undefined,
|
||||
product_warehouse: initialValues?.product_warehouse || undefined,
|
||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
},
|
||||
validationSchema: SalesOrderProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
onSubmitForm?.(values);
|
||||
handleResetForm();
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
options: kandangSourceOptions,
|
||||
rawData: kandangSourceRawData,
|
||||
isLoadingOptions: isLoadingKandangSourceOptions,
|
||||
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: warehouseSourceOptions,
|
||||
rawData: warehouseSourceRawData,
|
||||
@@ -64,109 +68,35 @@ const MarketingProductForm = ({
|
||||
'product.name',
|
||||
'search',
|
||||
{
|
||||
warehouse_id: selectedOptionsKandang?.value?.toString() ?? '',
|
||||
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
||||
}
|
||||
);
|
||||
|
||||
// Handler
|
||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedOptionsKandang(val as OptionType);
|
||||
formik.setFieldValue('kandang', val as OptionType);
|
||||
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('product_warehouse_id', null);
|
||||
formik.setFieldValue('product_warehouse', null);
|
||||
formik.setFieldValue('qty', null);
|
||||
warehouseChangeHandler(null);
|
||||
};
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedOptionsWarehouse(val as OptionType);
|
||||
formik.setFieldValue('product_warehouse', val as OptionType);
|
||||
formik.setFieldValue('product_warehouse_id', (val as OptionType)?.value);
|
||||
if (isResponseSuccess(warehouseSourceRawData)) {
|
||||
const newId = (val as OptionType)?.value;
|
||||
formik.setFieldValue('product_warehouse_id', newId);
|
||||
|
||||
if (isResponseSuccess(warehouseSourceRawData) && newId) {
|
||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||
(item: ProductWarehouse) => item.id === (val as OptionType)?.value
|
||||
(item: ProductWarehouse) => item.id === newId
|
||||
);
|
||||
if (selectedOptionsWarehouse?.value !== null) {
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', null);
|
||||
}
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', null);
|
||||
}
|
||||
};
|
||||
|
||||
// Formik
|
||||
const formik = useFormik<MarketingProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
vehicle_number:
|
||||
initialValues?.marketing_delivery_products?.vehicle_number || undefined,
|
||||
kandang_id: initialValues?.product_warehouse.warehouse.id || undefined,
|
||||
kandang: {
|
||||
value: initialValues?.product_warehouse.warehouse.id as number,
|
||||
label: initialValues?.product_warehouse.warehouse.name as string,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: initialValues?.product_warehouse.product.id as number,
|
||||
label: initialValues?.product_warehouse.product.name as string,
|
||||
},
|
||||
product_warehouse_id:
|
||||
initialValues?.product_warehouse.product.id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
uom: initialValues?.product_warehouse?.product?.uom?.name || undefined,
|
||||
avg_weight: initialValues?.avg_weight || undefined,
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
delivery_date:
|
||||
initialValues?.marketing_delivery_products?.delivery_date ||
|
||||
new Date().toDateString() ||
|
||||
undefined,
|
||||
},
|
||||
validationSchema: MarketingProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (
|
||||
isResponseSuccess(kandangSourceRawData) &&
|
||||
isResponseSuccess(warehouseSourceRawData)
|
||||
) {
|
||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||
(item: ProductWarehouse) => item.id === values.product_warehouse_id
|
||||
);
|
||||
const kandang = kandangSourceRawData?.data.find(
|
||||
(item: Kandang) => item.id === values.kandang_id
|
||||
);
|
||||
|
||||
const marketingProduct: CreateMarketingProductPayload = {
|
||||
id: initialValues?.id || undefined,
|
||||
vehicle_number: formatVechicleNumber(values.vehicle_number as string),
|
||||
kandang_id: values.kandang_id as number,
|
||||
kandang: kandang,
|
||||
product_warehouse_id: values.product_warehouse_id as number,
|
||||
product_warehouse: productWarehouse,
|
||||
unit_price: values.unit_price as number,
|
||||
total_weight: values.total_weight as number,
|
||||
qty: values.qty as number,
|
||||
uom: values.uom as string,
|
||||
avg_weight: values.avg_weight as number,
|
||||
total_price: values.total_price as number,
|
||||
delivery_date: values.delivery_date as string,
|
||||
};
|
||||
|
||||
onSubmitForm?.(marketingProduct, values);
|
||||
handleResetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formik.initialValues);
|
||||
}, [formikSetValues, formik.initialValues]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setSelectedOptionsKandang(null);
|
||||
setSelectedOptionsWarehouse(null);
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
values: {
|
||||
@@ -178,10 +108,8 @@ const MarketingProductForm = ({
|
||||
unit_price: '',
|
||||
total_weight: '',
|
||||
qty: '',
|
||||
uom: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
delivery_date: new Date().toDateString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -191,7 +119,7 @@ const MarketingProductForm = ({
|
||||
formik.values;
|
||||
|
||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
||||
if (qty && unit_price && field === 'unit_price') {
|
||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
||||
formik.setFieldValue(
|
||||
'total_price',
|
||||
(qty as number) * (unit_price as number)
|
||||
@@ -205,7 +133,7 @@ const MarketingProductForm = ({
|
||||
}
|
||||
|
||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
||||
if (qty && avg_weight && field === 'avg_weight') {
|
||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
||||
formik.setFieldValue(
|
||||
'total_weight',
|
||||
(qty as number) * (avg_weight as number)
|
||||
@@ -226,6 +154,16 @@ const MarketingProductForm = ({
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{formErrorMessage && (
|
||||
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
|
||||
<Alert color='error'>
|
||||
{formErrorMessage ? formErrorMessage : ''}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{/* <small className='block text-rose-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
<div className='grid grid-cols-2 gap-4 z-200'>
|
||||
<PatternInput
|
||||
name='vehicle_number'
|
||||
@@ -250,10 +188,9 @@ const MarketingProductForm = ({
|
||||
label='Kandang'
|
||||
options={kandangSourceOptions}
|
||||
isLoading={isLoadingKandangSourceOptions}
|
||||
value={selectedOptionsKandang}
|
||||
value={formik.values.kandang}
|
||||
onChange={kandangChangeHandler}
|
||||
isClearable
|
||||
menuPortalTarget={modalRef?.current}
|
||||
isError={
|
||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||
}
|
||||
@@ -265,12 +202,11 @@ const MarketingProductForm = ({
|
||||
label='Produk'
|
||||
options={warehouseSourceOptions}
|
||||
isLoading={isLoadingWarehouseSourceOptions}
|
||||
value={selectedOptionsWarehouse}
|
||||
value={formik.values.product_warehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
isClearable
|
||||
menuPortalTarget={modalRef?.current}
|
||||
placeholder='Pilih Kandang Terlebih Dahulu'
|
||||
isDisabled={!selectedOptionsKandang?.value}
|
||||
isDisabled={!formik.values.kandang_id}
|
||||
isError={
|
||||
formik.touched.product_warehouse_id &&
|
||||
Boolean(formik.errors.product_warehouse_id)
|
||||
@@ -358,4 +294,4 @@ const MarketingProductForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketingProductForm;
|
||||
export default SalesOrderProductForm;
|
||||
@@ -0,0 +1,280 @@
|
||||
import Table from '@/components/Table';
|
||||
import { DeliveryOrderProductFormValues } from '../repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import Button from '@/components/Button';
|
||||
import { Icon } from '@iconify/react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { SalesOrderProductFormValues } from '../repeater/sales-order/SalesOrderProduct.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
|
||||
type DeliveryOrderProductTableProps = {
|
||||
data: DeliveryOrderProductFormValues[];
|
||||
salesOrder: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
selectedRowIds: number[];
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (id: number) => void;
|
||||
onBulkDelete: () => void;
|
||||
onAddProductClick: () => void;
|
||||
onInputDate: (data: DeliveryOrderProductFormValues) => void;
|
||||
};
|
||||
|
||||
const DeliveryOrderProductTable = ({
|
||||
data,
|
||||
salesOrder,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
onInputDate,
|
||||
}: DeliveryOrderProductTableProps) => {
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
const onEditRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
onEditRef.current = onEdit;
|
||||
|
||||
const canAddData = salesOrder.reduce((acc, curr) => {
|
||||
const deliveredQty = data.filter(
|
||||
(deliveryItem) => deliveryItem.marketing_product_id == curr.id
|
||||
);
|
||||
return acc && deliveredQty.length != salesOrder.length;
|
||||
}, true);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cols = [
|
||||
// {
|
||||
// id: 'select',
|
||||
// header: ({
|
||||
// table,
|
||||
// }: {
|
||||
// table: TanStack.Table<DeliveryOrderProductFormValues>;
|
||||
// }) => (
|
||||
// <div className='w-full flex flex-row justify-center'>
|
||||
// <CheckboxInput
|
||||
// name='allRow'
|
||||
// checked={table.getIsAllRowsSelected()}
|
||||
// indeterminate={table.getIsSomeRowsSelected()}
|
||||
// onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// cell: ({
|
||||
// row,
|
||||
// }: {
|
||||
// row: TanStack.Row<DeliveryOrderProductFormValues>;
|
||||
// }) => (
|
||||
// <div>
|
||||
// <CheckboxInput
|
||||
// name='row'
|
||||
// checked={row.getIsSelected()}
|
||||
// disabled={!row.getCanSelect()}
|
||||
// indeterminate={row.getIsSomeSelected()}
|
||||
// onChange={row.getToggleSelectedHandler()}
|
||||
// />
|
||||
// </div>
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
|
||||
header: 'No. Pengiriman',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => <div>{props.row.original.do_number}</div>,
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.delivery_date
|
||||
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
|
||||
: '-',
|
||||
header: 'Tanggal Delivery',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<>
|
||||
{formType == 'add_deliver' && (
|
||||
<DateInput
|
||||
name={`delivery_date_${props.row.original.marketing_product_id}`}
|
||||
className={{
|
||||
input: 'p-0',
|
||||
inputWrapper: 'py-1 px-3 h-fit w-fit bg-white',
|
||||
wrapper: 'p-0',
|
||||
}}
|
||||
value={
|
||||
props.row.original.delivery_date
|
||||
? formatDate(props.row.original.delivery_date, 'yyyy-MM-DD')
|
||||
: undefined
|
||||
}
|
||||
onChange={(val) => {
|
||||
onInputDate({
|
||||
...props.row.original,
|
||||
delivery_date: val.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formType == 'edit_deliver' &&
|
||||
formatDate(
|
||||
props.row.original.delivery_date as string,
|
||||
'DD MMM YYYY'
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.marketing_product?.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.marketing_product?.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string)),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string)),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<>
|
||||
<Button
|
||||
color='warning'
|
||||
className='px-2 py-1 text-sm'
|
||||
onClick={() =>
|
||||
onEditRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
{/* <Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button> */}
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (formType == 'add_deliver') {
|
||||
return cols.filter(
|
||||
(col) => col.header != 'Aksi' && col.header != 'No. Pengiriman'
|
||||
);
|
||||
}
|
||||
return cols;
|
||||
}, [formType, onInputDate, onEditRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<DeliveryOrderProductFormValues>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={columns}
|
||||
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-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data pengiriman</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
{/* <Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onAddProductClick}
|
||||
// disabled={!canAddData}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Pengiriman
|
||||
</Button> */}
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onBulkDelete}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Pengiriman
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryOrderProductTable;
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Table from '@/components/Table';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
|
||||
type SalesOrderProductTableProps = {
|
||||
data: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
selectedRowIds: number[];
|
||||
onDelete: (id: number) => void;
|
||||
onBulkDelete: () => void;
|
||||
onAddProductClick: () => void;
|
||||
};
|
||||
|
||||
const SalesOrderProductTable = ({
|
||||
data,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
}: SalesOrderProductTableProps) => {
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({
|
||||
table,
|
||||
}: {
|
||||
table: TanStack.Table<SalesOrderProductFormValues>;
|
||||
}) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
row.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string)),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string)),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<SalesOrderProductFormValues>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={
|
||||
formType == 'deliver'
|
||||
? columns.filter(
|
||||
(col) => col.header != 'Aksi' && col.id != 'select'
|
||||
)
|
||||
: columns
|
||||
}
|
||||
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-2 py-2 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data penjualan</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{formType != 'deliver' && (
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onAddProductClick}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onBulkDelete}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Produk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesOrderProductTable;
|
||||
@@ -0,0 +1,235 @@
|
||||
import Button from '@/components/Button';
|
||||
import { BaseDeliveryOrder, Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import pdfStyles from './styles/MarketingPDFStyles';
|
||||
import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||
import { format } from 'path';
|
||||
import { date } from 'yup';
|
||||
|
||||
interface DeliveryOrderExportProps {
|
||||
data?: Marketing;
|
||||
deliveryOrder: BaseDeliveryOrder;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DeliveryOrderExport = ({
|
||||
data,
|
||||
deliveryOrder,
|
||||
}: DeliveryOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const blob = await pdf(
|
||||
<PDFDocument data={salesData} deliveryOrder={deliveryOrder} />
|
||||
).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${deliveryOrder?.do_number || 'delivery-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!salesData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No sales order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
isLoading={isGeneratingPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{deliveryOrder.do_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default DeliveryOrderExport;
|
||||
const PDFDocument = ({
|
||||
data,
|
||||
deliveryOrder,
|
||||
}: {
|
||||
data: Marketing;
|
||||
deliveryOrder: BaseDeliveryOrder;
|
||||
}) => {
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
deliveryOrder.deliveries?.reduce((a, b) => a + b.total_price, 0) ?? 0
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Delivery Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>DELIVERY ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>{deliveryOrder.do_number || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Depature Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Ship To</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Depature From</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{data?.customer.email || ''} - {data?.customer.phone || ''}
|
||||
</Text>
|
||||
<Text></Text>
|
||||
<Text>{data?.customer.address || ''}</Text>
|
||||
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{deliveryOrder.warehouse?.name || '-'}
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{formatDate(deliveryOrder.delivery_date, 'DD MMM YYYY')}
|
||||
</Text>
|
||||
<Text>{deliveryOrder.warehouse?.area?.name}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Delivery Table */}
|
||||
<Text style={pdfStyles.sectionTitle}>Product Shipped</Text>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Vehicle Number</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{deliveryOrder.deliveries?.map((item, index) => {
|
||||
return (
|
||||
<View key={index} style={[pdfStyles.tableRow]}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{formatVechicleNumber(item.vehicle_number) || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
|
||||
>
|
||||
<Text>Rp{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>From Sales Order</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>
|
||||
{data?.so_number || '-'} -{' '}
|
||||
{formatDate(data.so_date, 'DD MMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
import Button from '@/components/Button';
|
||||
import { Marketing } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import pdfStyles from './styles/MarketingPDFStyles';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
interface SalesOrderExportProps {
|
||||
data?: Marketing;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
||||
const salesData = data;
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const blob = await pdf(<PDFDocument data={salesData} />).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${salesData?.so_number || 'sales-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!salesData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No sales order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return salesData?.so_number && salesData.so_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
isLoading={isGeneratingPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{salesData.so_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SalesOrderExport;
|
||||
const PDFDocument = ({ data }: { data: Marketing }) => {
|
||||
const grandTotal = useMemo(() => {
|
||||
return data?.sales_order?.reduce((a, b) => a + b.total_price, 0) ?? 0;
|
||||
}, [data?.sales_order]);
|
||||
|
||||
return (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Sales Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>SALES ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>SO Number: {data?.so_number || '-'}</Text>
|
||||
<Text>
|
||||
Date:{' '}
|
||||
{data?.so_date
|
||||
? formatDate(data.so_date, 'DD MMM YYYY')
|
||||
: formatDate(new Date(), 'DD MMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Customer Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Customer</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Sales</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{data?.customer?.name || '-'} ({data?.customer?.type || '-'})
|
||||
</Text>
|
||||
<Text style={{ marginTop: '2px' }}>
|
||||
{data?.customer.email || ''} - {data?.customer.phone || ''}
|
||||
</Text>
|
||||
<Text></Text>
|
||||
<Text>{data?.customer.address || ''}</Text>
|
||||
<Text style={{ fontSize: '7px', marginTop: '3px' }}></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={{ fontWeight: 'bold', marginTop: '2px' }}>
|
||||
{data?.sales_person?.name || '-'}
|
||||
</Text>
|
||||
<Text>{data?.sales_person.email}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Product Sales Order Table */}
|
||||
<Text style={pdfStyles.sectionTitle}>Product Sold</Text>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>From</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{data?.sales_order?.map((item, index) => {
|
||||
const isLastItem = index === (data?.sales_order?.length || 0) - 1;
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
// isLastItem ? {} : pdfStyles.tableBorderBottom,
|
||||
]}
|
||||
>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product_warehouse?.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp{formatNumber(item.unit_price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}
|
||||
>
|
||||
<Text>Rp{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Notes</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>{data?.notes || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import { StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
address: {
|
||||
fontSize: 8,
|
||||
color: '#666666',
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
innerCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default pdfStyles;
|
||||
@@ -1,308 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import Table from '@/components/Table';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const SalesOrderDetail = ({
|
||||
initialValues,
|
||||
refreshValues,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
refreshValues?: () => void;
|
||||
}) => {
|
||||
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>(
|
||||
'approve'
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const deleteModal = useModal();
|
||||
const confirmationModal = useModal();
|
||||
const deliveryModal = useModal();
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalAction('approve');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalAction('reject');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully deleted Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.singleApproval(
|
||||
// initialValues?.id as number,
|
||||
// approvalAction
|
||||
// );
|
||||
setIsLoading(false);
|
||||
confirmationModal.closeModal();
|
||||
toast.success('Successfully approved Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.delivery(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deliveryModal.closeModal();
|
||||
toast.success('Successfully delivered Sales Order!');
|
||||
refreshValues?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col w-full gap-4'>
|
||||
<FormHeader
|
||||
title='Detail Sales Order'
|
||||
backUrl='/marketing/sales-orders'
|
||||
/>
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.approval?.step_number != 3 && (
|
||||
<>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
disabled={initialValues?.approval?.step_number != 1}
|
||||
>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.approval?.step_number != 2}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{initialValues?.approval?.step_number == 2 && (
|
||||
<Button color='success' onClick={deliveryClickHandler}>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Card
|
||||
title='Informasi Sales Order'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto rounded-box border border-base-content/5 bg-base-100 mt-3'>
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width='45%' className='font-semibold'>
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.customer?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Status</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.approval?.step_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.so_date}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Total Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
{formatCurrency(initialValues?.grand_total as number)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Catatan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.notes ?? '-'}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
{initialValues?.marketing_products && (
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
>
|
||||
<Table<MarketingProduct>
|
||||
data={initialValues?.marketing_products}
|
||||
columns={[
|
||||
{
|
||||
header: 'No. Polisi',
|
||||
accessorFn(row) {
|
||||
return formatVechicleNumber(
|
||||
row.marketing_delivery_products?.vehicle_number as string
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.total_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Kuantitas',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.qty);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
accessorFn(row) {
|
||||
return formatNumber(row.avg_weight);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Total Penjualan (Rp)',
|
||||
accessorFn(row) {
|
||||
return formatCurrency(row.total_price);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
initialValues?.marketing_products &&
|
||||
initialValues?.marketing_products?.length === 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',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/sales-orders/detail/edit?salesOrderId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button color='error' onClick={deleteClickHandler}>
|
||||
<Icon icon='mdi:delete' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'approve' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction === 'approve' ? 'success' : 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
}}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={deliveryModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin deliver penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalDeliveryClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesOrderDetail;
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import { MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
} from './repeater/MarketingProduct.schema';
|
||||
|
||||
type MarketingSchema = {
|
||||
customer_id: number | undefined;
|
||||
customer:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
so_date: string | undefined;
|
||||
notes: string | undefined;
|
||||
marketing_products: MarketingProductFormValues[];
|
||||
};
|
||||
|
||||
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
|
||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||
customer: Yup.object({
|
||||
value: Yup.number().required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
so_date: Yup.string().required('Tanggal wajib diisi!'),
|
||||
notes: Yup.string().required('Catatan wajib diisi!'),
|
||||
marketing_products: Yup.array()
|
||||
.of(MarketingProductSchema)
|
||||
.min(1, 'Minimal harus ada 1 produk!')
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdateMarketingSchema = MarketingSchema;
|
||||
|
||||
export type MarketingFormValues = Yup.InferType<typeof MarketingSchema>;
|
||||
@@ -1,514 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import Table from '@/components/Table'; // Keep this import
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
Marketing,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import MarketingProductForm from './repeater/MarketingProductForm';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { useFormik } from 'formik';
|
||||
import { MarketingFormValues, MarketingSchema } from './SalesForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { MarketingProductFormValues } from './repeater/MarketingProduct.schema';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SalesForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
}: {
|
||||
formType?: 'add' | 'edit';
|
||||
initialValues?: Marketing;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const addProductModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||
useState<MarketingProduct | null>(null);
|
||||
const [rawMarketingProducts, setRawMarketingProducts] = useState<
|
||||
MarketingProduct[]
|
||||
>(initialValues?.marketing_products || []);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState<OptionType | null>(
|
||||
initialValues?.customer
|
||||
? { value: initialValues.customer.id, label: initialValues.customer.name }
|
||||
: null
|
||||
);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState<number>(
|
||||
initialValues?.grand_total ?? 0
|
||||
);
|
||||
const marketingProducts = useMemo(
|
||||
() => rawMarketingProducts,
|
||||
[rawMarketingProducts]
|
||||
);
|
||||
|
||||
const {
|
||||
options: customerOptions,
|
||||
rawData: customerRawData,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const handleAddProduct = useCallback(() => {
|
||||
addProductModal.openModal();
|
||||
}, [addProductModal]);
|
||||
const handleDeleteProduct = useCallback((id: number) => {
|
||||
setRawMarketingProducts((prev) => prev.filter((p) => p.id !== id));
|
||||
}, []);
|
||||
const handleBulkDeleteProduct = () => {
|
||||
setRawMarketingProducts((prev) =>
|
||||
prev.filter((product) => !selectedRowIds.includes(product.id))
|
||||
);
|
||||
};
|
||||
const handleDelete = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const handleAddSubmitProduct = useCallback(
|
||||
async (
|
||||
tableValue: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
) => {
|
||||
const newMarketingProduct: MarketingProduct = {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
product_warehouse: tableValue.product_warehouse!,
|
||||
unit_price: tableValue.unit_price as number,
|
||||
total_weight: tableValue.total_weight as number,
|
||||
qty: tableValue.qty as number,
|
||||
avg_weight: tableValue.avg_weight as number,
|
||||
total_price: tableValue.total_price as number,
|
||||
marketing_delivery_products: {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
vehicle_number: tableValue.vehicle_number as string,
|
||||
delivery_date: tableValue.delivery_date as string,
|
||||
unit_price: tableValue.unit_price as number,
|
||||
total_weight: tableValue.total_weight as number,
|
||||
qty: tableValue.qty as number,
|
||||
avg_weight: tableValue.avg_weight as number,
|
||||
total_price: tableValue.total_price as number,
|
||||
},
|
||||
};
|
||||
|
||||
setRawMarketingProducts((prev) => [...prev, newMarketingProduct]);
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_products: [...formik.values.marketing_products, fieldValues],
|
||||
});
|
||||
setGrandTotal((prev) => prev + (tableValue.total_price as number));
|
||||
addProductModal.closeModal();
|
||||
},
|
||||
[rawMarketingProducts.length, addProductModal]
|
||||
);
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
setSelectedCustomer(val as OptionType);
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[selectedCustomer, setSelectedCustomer]
|
||||
);
|
||||
|
||||
const createMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
toast.success('Successfully created Sales Order!');
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
const updateMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.update(
|
||||
initialValues?.id as number,
|
||||
values
|
||||
);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
}
|
||||
toast.success('Successfully updated Sales Order!');
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const deleteMarketingRes = await MarketingApi.delete(
|
||||
initialValues?.id as number
|
||||
);
|
||||
if (isResponseSuccess(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
}
|
||||
if (isResponseError(deleteMarketingRes)) {
|
||||
console.log(deleteMarketingRes);
|
||||
}
|
||||
toast.success('Successfully deleted Sales Order!');
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: MarketingProduct
|
||||
): MarketingProductFormValues => {
|
||||
return {
|
||||
vehicle_number: product.marketing_delivery_products?.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.product.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.product.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
uom: product.product_warehouse?.product?.uom?.name,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
delivery_date: product.marketing_delivery_products?.delivery_date,
|
||||
};
|
||||
};
|
||||
|
||||
const formik = useFormik<MarketingFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
so_date: initialValues?.so_date || undefined,
|
||||
notes: initialValues?.notes || undefined,
|
||||
customer_id: initialValues?.customer?.id || undefined,
|
||||
customer: {
|
||||
value: initialValues?.customer?.id as number,
|
||||
label: initialValues?.customer?.name as string,
|
||||
},
|
||||
marketing_products:
|
||||
initialValues?.marketing_products?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
},
|
||||
validationSchema: MarketingSchema,
|
||||
onSubmit: async (values) => {
|
||||
const payload = {
|
||||
customer_id: values.customer_id as number,
|
||||
date: values.so_date as string,
|
||||
notes: values.notes as string,
|
||||
marketing_products: values.marketing_products,
|
||||
} as CreateMarketingPayload;
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
createMarketingHandler(payload);
|
||||
break;
|
||||
case 'edit':
|
||||
updateMarketingHandler(payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formik.initialValues);
|
||||
}, [formikSetValues, formik.initialValues]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }: { table: TanStack.Table<MarketingProduct> }) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<MarketingProduct> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.marketing_delivery_products?.vehicle_number,
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.product_warehouse.warehouse.name,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.product_warehouse.product.name,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatCurrency(row.unit_price),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.total_weight),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.qty),
|
||||
header: 'Kuantitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatNumber(row.avg_weight),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) => formatCurrency(row.total_price),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props: TanStack.CellContext<MarketingProduct, unknown>) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() => handleDeleteProduct(props.row.original.id)}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleDeleteProduct] // dependensi tunggal
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
>
|
||||
<FormHeader
|
||||
title={`${formType === 'add' ? 'Tambah' : 'Edit'} Sales Order`}
|
||||
backUrl='/marketing/sales-orders'
|
||||
/>
|
||||
<Card
|
||||
title='Informasi Order'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-3 mt-3'>
|
||||
<SelectInput
|
||||
label='Pelanggan'
|
||||
options={customerOptions}
|
||||
isLoading={isLoadingCustomerOptions}
|
||||
value={selectedCustomer}
|
||||
onChange={handleChangeCustomer}
|
||||
isError={
|
||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||
}
|
||||
errorMessage={formik.errors.customer_id}
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
/>
|
||||
<DateInput
|
||||
name='so_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.so_date}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
|
||||
errorMessage={formik.errors.so_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<Table<MarketingProduct>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={marketingProducts}
|
||||
columns={columns}
|
||||
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-2 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-2 py-2 last:flex last:flex-row last:justify-end first:flex first:flex-row first:justify-start',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
emptyContent={
|
||||
<div
|
||||
className={cn(
|
||||
'w-full h-16 flex flex-col justify-center items-center gap-2'
|
||||
)}
|
||||
>
|
||||
<span className='text-gray-500'>Belum ada data penjualan</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={handleAddProduct}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
{selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={handleBulkDeleteProduct}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Produk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
required
|
||||
name='notes'
|
||||
label='Catatan'
|
||||
rows={3}
|
||||
placeholder='Masukan catatan penjualan'
|
||||
value={formik.values.notes}
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
/>
|
||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
||||
<span>Total Penjualan</span>
|
||||
<span className='text-lg font-semibold'>
|
||||
{formatCurrency(grandTotal)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
|
||||
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{formType == 'edit' && (
|
||||
<div className='flex flex-row justify-start'>
|
||||
<Button type='button' color='error' onClick={handleDelete}>
|
||||
<Icon icon='mdi:trash' width={24} height={24} />
|
||||
Hapus
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
ref={addProductModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row items-center justify-between'>
|
||||
<h3 className='text-lg font-semibold mb-4'>Tambah Produk</h3>
|
||||
<Button
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addProductModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<MarketingProductForm
|
||||
onSubmitForm={handleAddSubmitProduct}
|
||||
modalRef={addProductModal.ref}
|
||||
data={rawMarketingProducts}
|
||||
initialValues={selectedMarketingProduct ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
onClick: deleteMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesForm;
|
||||
@@ -1,26 +1,48 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const KandangFormSchema = Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
type KandangFormSchemaType = {
|
||||
name: string;
|
||||
locationId: number | undefined;
|
||||
location:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
capacity: number | undefined;
|
||||
picId: number | undefined;
|
||||
pic:
|
||||
| {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
| undefined
|
||||
| null;
|
||||
};
|
||||
|
||||
locationId: Yup.number()
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
export const KandangFormSchema: Yup.ObjectSchema<KandangFormSchemaType> =
|
||||
Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
|
||||
capacity: Yup.number()
|
||||
.min(1, 'Kapasitas wajib diisi!')
|
||||
.required('Kapasitas wajib diisi!'),
|
||||
locationId: Yup.number()
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
|
||||
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
|
||||
pic: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
});
|
||||
capacity: Yup.number()
|
||||
.min(1, 'Kapasitas wajib diisi!')
|
||||
.required('Kapasitas wajib diisi!'),
|
||||
|
||||
picId: Yup.number().min(1, 'PIC wajib diisi!').required('PIC wajib diisi!'),
|
||||
pic: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
});
|
||||
|
||||
export const UpdateKandangFormSchema = KandangFormSchema;
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
capacity: initialValues?.capacity ?? 0,
|
||||
capacity: initialValues?.capacity,
|
||||
picId: initialValues?.pic?.id ?? 0,
|
||||
pic: initialValues?.pic
|
||||
? {
|
||||
@@ -102,9 +102,9 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
|
||||
const kandangPayload: CreateKandangPayload = {
|
||||
name: values.name,
|
||||
location_id: values.locationId,
|
||||
capacity: values.capacity,
|
||||
pic_id: values.picId,
|
||||
location_id: values.locationId!,
|
||||
capacity: values.capacity ? parseInt(values.capacity.toString()) : 0,
|
||||
pic_id: values.picId!,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
@@ -256,7 +256,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
required
|
||||
name='capacity'
|
||||
label='Kapasitas'
|
||||
value={formik.values.capacity ?? undefined}
|
||||
placeholder='Masukan kapasitas kandang'
|
||||
value={formik.values.capacity}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
|
||||
@@ -20,7 +20,6 @@ import { Icon } from '@iconify/react';
|
||||
import { CellContext, SortingState } from '@tanstack/react-table';
|
||||
import { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import ChickinForm from './form/ChickinForm';
|
||||
|
||||
const ChickinTable = () => {
|
||||
const {
|
||||
|
||||
@@ -45,7 +45,7 @@ const ChickinFormKandang = ({
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<FormHeader
|
||||
title='Chick In DOC'
|
||||
title={`Chick In ${initialValues.kandang?.name ?? 'Kandang'}`}
|
||||
backUrl={`/production/project-flock/chickin/add?projectFlockId=${initialValues?.project_flock?.id}`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import PillBadge from '@/components/PillBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -31,12 +32,13 @@ const ChickinLogsView = ({
|
||||
confirmModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
const confirmationModalApproveClickHandler = async (notes?: string) => {
|
||||
setChickinErrorMessage('');
|
||||
setIsApproveLoading(true);
|
||||
const approveChickinRes = await ChickinApi.singleApproval(
|
||||
initialValues?.id as number,
|
||||
'APPROVED'
|
||||
'APPROVED',
|
||||
notes
|
||||
);
|
||||
if (isResponseSuccess(approveChickinRes)) {
|
||||
toast.success(approveChickinRes?.message as string);
|
||||
@@ -151,7 +153,7 @@ const ChickinLogsView = ({
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type='success'
|
||||
text={`Apakah anda yakin ingin approve data Chickin yang Pending?`}
|
||||
@@ -161,7 +163,9 @@ const ChickinLogsView = ({
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
onClick: (notes) => {
|
||||
confirmationModalApproveClickHandler(notes);
|
||||
},
|
||||
isLoading: isApproveLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
ChickinSchema,
|
||||
} from '../ChickinForm.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -24,7 +22,6 @@ import Alert from '@/components/Alert';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
const ChickinFormView = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
afterSubmit,
|
||||
}: {
|
||||
@@ -122,7 +119,7 @@ const ChickinFormView = ({
|
||||
return (
|
||||
<form
|
||||
className='flex flex-col gap-4'
|
||||
onReset={(e) => {
|
||||
onReset={() => {
|
||||
handleReset();
|
||||
}}
|
||||
onSubmit={formik.handleSubmit}
|
||||
|
||||
@@ -6,6 +6,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { 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';
|
||||
@@ -144,6 +145,9 @@ const ProjectFlockTable = () => {
|
||||
useState<ProjectFlock>();
|
||||
const deleteModal = useModal();
|
||||
const confirmModal = useModal();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
|
||||
@@ -226,18 +230,21 @@ const ProjectFlockTable = () => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'APPROVED' | 'REJECTED'
|
||||
) => {
|
||||
setIsApproveLoading(true);
|
||||
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
|
||||
BaseApiResponse<ProjectFlock>,
|
||||
ProjectFlockApprovalPayload
|
||||
>(`/approvals`, {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
action: 'APPROVED',
|
||||
approvable_ids: selectedRowIds.map((id) => id),
|
||||
},
|
||||
});
|
||||
const approveProjectFlockRes =
|
||||
approvalAction === 'APPROVED'
|
||||
? await ProjectFlockApi.bulkApprove(
|
||||
selectedRowIds.map((id) => id),
|
||||
notes
|
||||
)
|
||||
: await ProjectFlockApi.bulkReject(
|
||||
selectedRowIds.map((id) => id),
|
||||
notes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(approveProjectFlockRes)) {
|
||||
toast.success('Project Flock berhasil di-approve!');
|
||||
@@ -271,6 +278,7 @@ const ProjectFlockTable = () => {
|
||||
variant='outline'
|
||||
color='success'
|
||||
onClick={() => {
|
||||
setApprovalAction('APPROVED');
|
||||
confirmModal.openModal();
|
||||
}}
|
||||
disabled={selectedRowIds.length === 0}
|
||||
@@ -279,6 +287,19 @@ const ProjectFlockTable = () => {
|
||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='error'
|
||||
onClick={() => {
|
||||
setApprovalAction('REJECTED');
|
||||
confirmModal.openModal();
|
||||
}}
|
||||
disabled={selectedRowIds.length === 0}
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='mdi:times' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
<div className='ms-auto w-full sm:w-auto'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
@@ -558,7 +579,7 @@ const ProjectFlockTable = () => {
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock?.name})?`}
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
@@ -570,17 +591,19 @@ const ProjectFlockTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type='success'
|
||||
text={`Apakah anda yakin ingin approve data Project Flock ini (${selectedRowIds.length} data)?`}
|
||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
||||
onClick: (notes) => {
|
||||
confirmApprovalHandler(notes, approvalAction);
|
||||
},
|
||||
isLoading: isApproveLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
+12
-11
@@ -14,13 +14,13 @@ import { cn } from '@/lib/helper';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
|
||||
const ProjectFlockChickinDetail = ({
|
||||
projectFlockId,
|
||||
@@ -42,10 +42,7 @@ const ProjectFlockChickinDetail = ({
|
||||
const [projectFlock, setProjectFlock] = useState<ProjectFlock>();
|
||||
|
||||
// Fetch Data
|
||||
const {
|
||||
data: listProjectFlockKandang,
|
||||
isLoading: isLoadingListProjectFlockKandang,
|
||||
} = useSWR(
|
||||
const { data: listProjectFlockKandang } = useSWR(
|
||||
`${ProjectFlockKandangApi.basePath}?${new URLSearchParams({
|
||||
search: searchProjectFlock,
|
||||
project_flock_id:
|
||||
@@ -104,6 +101,10 @@ const ProjectFlockChickinDetail = ({
|
||||
}, [projectFlockId, listProjectFlock]);
|
||||
return (
|
||||
<>
|
||||
<FormHeader
|
||||
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
|
||||
backUrl='/production/project-flock'
|
||||
/>
|
||||
<div className='flex flex-col gap-4 w-full my-4'>
|
||||
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
|
||||
<SelectInput
|
||||
@@ -118,7 +119,7 @@ const ProjectFlockChickinDetail = ({
|
||||
value={
|
||||
projectFlock
|
||||
? {
|
||||
label: `${projectFlock?.flock?.name}`,
|
||||
label: `${projectFlock?.flock_name}`,
|
||||
value: projectFlock?.id,
|
||||
}
|
||||
: null
|
||||
@@ -175,7 +176,7 @@ const ProjectFlockChickinDetail = ({
|
||||
},
|
||||
{
|
||||
header: 'Nama Flock',
|
||||
accessorKey: 'flock.name',
|
||||
accessorKey: 'flock_name',
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
@@ -209,10 +210,6 @@ const ProjectFlockChickinDetail = ({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Periode',
|
||||
accessorKey: 'period',
|
||||
},
|
||||
{
|
||||
header: 'FCR Layer',
|
||||
accessorKey: 'fcr.name',
|
||||
@@ -278,6 +275,10 @@ const ProjectFlockChickinDetail = ({
|
||||
accessorKey: 'kandang.capacity',
|
||||
header: 'Kapasitas',
|
||||
},
|
||||
{
|
||||
accessorFn: () => projectFlock?.period,
|
||||
header: 'Periode',
|
||||
},
|
||||
{
|
||||
accessorKey: 'approval.step_name',
|
||||
header: 'Status',
|
||||
|
||||
@@ -42,11 +42,6 @@ export const ProjectFlockFormSchema = Yup.object({
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!'),
|
||||
|
||||
period: Yup.number()
|
||||
.required('Periode wajib diisi!')
|
||||
.typeError('Periode harus berupa angka')
|
||||
.min(1, 'Minimal periode adalah 1'),
|
||||
|
||||
kandang_ids: Yup.array()
|
||||
.of(Yup.number().typeError('Kandang tidak valid!'))
|
||||
.min(1, 'Minimal harus ada 1 kandang!')
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
ProjectFlock,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import toast from 'react-hot-toast';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import Collapse from '@/components/Collapse';
|
||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
@@ -42,6 +41,7 @@ import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -72,8 +72,11 @@ const ProjectFlockForm = ({
|
||||
const [optionsKandang, setOptionsKandang] = useState<Kandang[]>(
|
||||
initialValues?.kandangs ?? []
|
||||
);
|
||||
const [selectedFlock, setSelectedFlock] = useState<number | undefined>(
|
||||
initialValues?.flock?.id ?? 0
|
||||
const [selectedFlock, setSelectedFlock] = useState<string | undefined>(
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
) ?? ''
|
||||
);
|
||||
|
||||
const deleteModal = useModal();
|
||||
@@ -102,9 +105,13 @@ const ProjectFlockForm = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues?.approval?.step_name) {
|
||||
const approvedDisabled = initialValues.approval.step_name !== 'Pengajuan';
|
||||
const pengajuanRejected =
|
||||
initialValues.approval.step_number == 1 &&
|
||||
initialValues.approval.action == 'REJECTED';
|
||||
const approvedDisabled =
|
||||
initialValues.approval.step_number !== 1 || pengajuanRejected;
|
||||
setIsApprovedDisabled(approvedDisabled);
|
||||
setIsRejectedDisabled(!approvedDisabled);
|
||||
setIsRejectedDisabled(!approvedDisabled || pengajuanRejected);
|
||||
setApprovalAction(!approvedDisabled ? 'APPROVED' : 'REJECTED');
|
||||
}
|
||||
}, [initialValues]);
|
||||
@@ -143,15 +150,14 @@ const ProjectFlockForm = ({
|
||||
mutate: refreshKandang,
|
||||
} = useSWR(kandangUrl, KandangApi.getAllFetcher);
|
||||
|
||||
const { data: periodFlocks, isLoading: isLoadingPeriodFlocks } = useSWR(
|
||||
const { data: periodFlocks, mutate: refreshPeriodFlocks } = useSWR(
|
||||
`${selectedFlock?.toString()}/periods`,
|
||||
(id: string) => ProjectFlockApi.getNextPeriod(id)
|
||||
() => ProjectFlockApi.getNextPeriod(parseInt(selectedLocation as string))
|
||||
);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
isLoading: approvalsLoading,
|
||||
rawDataApprovals: rawDataApprovals,
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.approval,
|
||||
@@ -182,6 +188,7 @@ const ProjectFlockForm = ({
|
||||
formik.setFieldValue('kandang_ids', selectedRowIds);
|
||||
}
|
||||
}
|
||||
refreshPeriodFlocks();
|
||||
}
|
||||
}, [kandang, selectedLocation]);
|
||||
useEffect(() => {
|
||||
@@ -278,13 +285,24 @@ const ProjectFlockForm = ({
|
||||
|
||||
// Formik InitialValue
|
||||
const formikInitialValues = useMemo<ProjectFlockFormValues>(() => {
|
||||
const trimFlock =
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
) ?? '';
|
||||
return {
|
||||
name: initialValues?.flock_name,
|
||||
flock: initialValues?.flock
|
||||
flock: initialValues?.flock_name
|
||||
? {
|
||||
value: initialValues?.flock?.id ?? 0,
|
||||
value:
|
||||
optionsFlock.find((flock) => {
|
||||
return flock.label == trimFlock;
|
||||
})?.value ?? 0,
|
||||
label:
|
||||
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
|
||||
formType != 'detail'
|
||||
? (optionsFlock.find((flock) => {
|
||||
return flock.label == trimFlock;
|
||||
})?.label ?? '')
|
||||
: initialValues?.flock_name,
|
||||
}
|
||||
: null,
|
||||
area: initialValues?.area
|
||||
@@ -311,31 +329,56 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
flock_id: initialValues?.flock?.id ?? 0,
|
||||
flock_name: initialValues?.flock_name ?? '',
|
||||
flock_name:
|
||||
optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label ?? '',
|
||||
area_id: initialValues?.area?.id ?? 0,
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
period: initialValues?.period ?? 1,
|
||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
||||
| number
|
||||
| undefined
|
||||
)[],
|
||||
};
|
||||
}, [initialValues]);
|
||||
}, [initialValues, optionsFlock]);
|
||||
|
||||
// Formik
|
||||
const formik = useFormik<ProjectFlockFormValues>({
|
||||
initialValues: {
|
||||
name: initialValues?.flock_name,
|
||||
flock: initialValues?.flock
|
||||
flock: initialValues?.flock_name
|
||||
? {
|
||||
value: initialValues?.flock?.id ?? 0,
|
||||
value:
|
||||
optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.value ?? 0,
|
||||
label:
|
||||
initialValues?.flock?.name ?? initialValues?.flock_name ?? '',
|
||||
formType != 'detail'
|
||||
? (optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label ?? '')
|
||||
: initialValues?.flock_name,
|
||||
}
|
||||
: null,
|
||||
area: initialValues?.area
|
||||
@@ -362,15 +405,24 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
flock_id: initialValues?.flock?.id ?? 0,
|
||||
flock_name: initialValues?.flock_name ?? '',
|
||||
flock_name:
|
||||
formType != 'detail'
|
||||
? optionsFlock.find((flock) => {
|
||||
return (
|
||||
flock.label ==
|
||||
initialValues?.flock_name?.slice(
|
||||
0,
|
||||
initialValues?.flock_name?.lastIndexOf(' ')
|
||||
)
|
||||
);
|
||||
})?.label
|
||||
: (initialValues?.flock_name ?? ''),
|
||||
area_id: initialValues?.area?.id ?? 0,
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
period: initialValues?.period ?? 1,
|
||||
kandang_ids: initialValues?.kandangs?.map((k: Kandang) => k.id) as (
|
||||
| number
|
||||
| undefined
|
||||
@@ -385,12 +437,11 @@ const ProjectFlockForm = ({
|
||||
onSubmit: async (values) => {
|
||||
setProjectFlockFormErrorMessage('');
|
||||
const payload: CreateProjectFlockPayload = {
|
||||
flock_name: values.flock?.label as string,
|
||||
flock_name: values.flock_name as string,
|
||||
area_id: values.area_id as number,
|
||||
category: values.category as string,
|
||||
fcr_id: values.fcr_id as number,
|
||||
location_id: values.location_id as number,
|
||||
period: values.period as number,
|
||||
kandang_ids: values.kandang_ids as number[],
|
||||
};
|
||||
|
||||
@@ -419,8 +470,6 @@ const ProjectFlockForm = ({
|
||||
if (initialValues?.area_id) {
|
||||
setSelectedArea(initialValues?.area_id.toString() as string);
|
||||
}
|
||||
|
||||
formik.setFieldValue('period', initialValues?.period);
|
||||
}
|
||||
}, [initialValues, setSelectedArea, formType]);
|
||||
|
||||
@@ -449,15 +498,6 @@ const ProjectFlockForm = ({
|
||||
formik.validateForm();
|
||||
}, [formik.values]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isResponseSuccess(periodFlocks)) {
|
||||
formik.setFieldValue('period', periodFlocks.data.next_period);
|
||||
}
|
||||
if (isResponseError(periodFlocks)) {
|
||||
console.log(periodFlocks?.message as string);
|
||||
}
|
||||
}, [periodFlocks]);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedRowIds = Object.keys(rowSelection)
|
||||
.filter((id) => rowSelection[id])
|
||||
@@ -485,32 +525,23 @@ const ProjectFlockForm = ({
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const confirmationModalClickHandler = async ({
|
||||
action = 'APPROVED',
|
||||
}: {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
}) => {
|
||||
const confirmApprovalHandler = async (
|
||||
notes: string,
|
||||
approvalAction: 'REJECTED' | 'APPROVED'
|
||||
) => {
|
||||
if (initialValues?.id === undefined) return;
|
||||
setIsApproveLoading(true);
|
||||
const approveProjectFlockRes = await ProjectFlockApi.customRequest<
|
||||
BaseApiResponse<ProjectFlock>,
|
||||
ProjectFlockApprovalPayload
|
||||
>(`/approvals`, {
|
||||
method: 'POST',
|
||||
payload: {
|
||||
action: action,
|
||||
approvable_ids: [initialValues?.id],
|
||||
},
|
||||
});
|
||||
|
||||
if (isResponseSuccess(approveProjectFlockRes)) {
|
||||
if (refreshProjectFlocks) {
|
||||
await refreshProjectFlocks();
|
||||
}
|
||||
toast.success(approveProjectFlockRes.message as string);
|
||||
const approvalRes =
|
||||
approvalAction == 'APPROVED'
|
||||
? await ProjectFlockApi.approve(initialValues?.id, notes)
|
||||
: await ProjectFlockApi.reject(initialValues?.id, notes);
|
||||
if (isResponseSuccess(approvalRes)) {
|
||||
refreshProjectFlocks?.();
|
||||
toast.success(approvalRes.message as string);
|
||||
}
|
||||
if (isResponseError(approveProjectFlockRes)) {
|
||||
toast.error(approveProjectFlockRes?.message as string);
|
||||
if (isResponseError(approvalRes)) {
|
||||
toast.error(approvalRes?.message as string);
|
||||
}
|
||||
refreshApprovals();
|
||||
confirmModal.closeModal();
|
||||
@@ -555,7 +586,7 @@ const ProjectFlockForm = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{approvals && !approvalsLoading && (
|
||||
{approvals && !approvalsLoading && formType == 'detail' && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
{formType == 'detail' && (
|
||||
@@ -615,7 +646,6 @@ const ProjectFlockForm = ({
|
||||
<div className='card bg-base-100 shadow w-full mb-6'>
|
||||
<div className='card-body'>
|
||||
<div className='card-title mb-4'>Informasi Umum</div>
|
||||
|
||||
<div className='grid sm:grid-cols-2 gap-4'>
|
||||
<SelectInput
|
||||
required
|
||||
@@ -634,10 +664,19 @@ const ProjectFlockForm = ({
|
||||
<SelectInput
|
||||
required
|
||||
label='Flock'
|
||||
value={formik.values.flock as OptionType}
|
||||
value={
|
||||
formik.values.flock_name
|
||||
? ({
|
||||
label: formik.values.flock_name,
|
||||
value: optionsFlock.find((flock) => {
|
||||
return flock.label === formik.values.flock_name;
|
||||
})?.value,
|
||||
} as OptionType)
|
||||
: undefined
|
||||
}
|
||||
onChange={(val) => {
|
||||
optionChangeHandler(val, 'flock');
|
||||
setSelectedFlock((val as OptionType)?.value as number);
|
||||
setSelectedFlock((val as OptionType)?.label);
|
||||
formik.setFieldValue(
|
||||
'flock_name',
|
||||
(val as OptionType)?.label
|
||||
@@ -701,22 +740,6 @@ const ProjectFlockForm = ({
|
||||
isClearable
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name='period'
|
||||
label='Periode'
|
||||
placeholder='Masukkan periode yang project'
|
||||
value={formik.values.period ?? (1 as number)}
|
||||
onChange={formik.handleChange}
|
||||
isError={
|
||||
formik.touched.period && Boolean(formik.errors.period)
|
||||
}
|
||||
errorMessage={formik.errors.period as string}
|
||||
readOnly={formType === 'detail'}
|
||||
disabled={true}
|
||||
isLoading={isLoadingPeriodFlocks}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -750,6 +773,9 @@ const ProjectFlockForm = ({
|
||||
<span className='loading loading-dots loading-xl'></span>
|
||||
)}
|
||||
<ProjectFlockKandangTable
|
||||
listPeriods={
|
||||
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
|
||||
}
|
||||
listKandang={optionsKandang}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
@@ -832,7 +858,7 @@ const ProjectFlockForm = ({
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock?.name} - ${initialValues?.area?.name})?`}
|
||||
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${initialValues?.flock_name} - ${initialValues?.area?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
@@ -844,12 +870,12 @@ const ProjectFlockForm = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmModal.ref}
|
||||
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${
|
||||
approvalAction == 'APPROVED' ? 'approve' : 'reject'
|
||||
} Project Flock berikut? (${initialValues?.flock?.name} - ${
|
||||
} Project Flock berikut? (${initialValues?.flock_name} - ${
|
||||
initialValues?.area?.name
|
||||
})?`}
|
||||
secondaryButton={{
|
||||
@@ -859,10 +885,8 @@ const ProjectFlockForm = ({
|
||||
text: 'Ya',
|
||||
color: approvalAction == 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isApproveLoading,
|
||||
onClick: () => {
|
||||
confirmationModalClickHandler({
|
||||
action: approvalAction,
|
||||
});
|
||||
onClick: (notes) => {
|
||||
confirmApprovalHandler(notes, approvalAction);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,10 +5,12 @@ import PillBadge from '@/components/PillBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlockPeriods } from '@/types/api/production/project-flock';
|
||||
import { OnChangeFn, Row } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const ProjectFlockKandangTable = ({
|
||||
listPeriods,
|
||||
listKandang,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
@@ -16,6 +18,7 @@ const ProjectFlockKandangTable = ({
|
||||
initialValues,
|
||||
formType = 'add',
|
||||
}: {
|
||||
listPeriods: ProjectFlockPeriods;
|
||||
listKandang: Kandang[];
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: OnChangeFn<Record<string, boolean>>;
|
||||
@@ -134,6 +137,19 @@ const ProjectFlockKandangTable = ({
|
||||
accessorFn: (row) => row.capacity,
|
||||
header: 'Kapasitas',
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.location?.name,
|
||||
header: 'Periode',
|
||||
cell: (props) => {
|
||||
console.log('listPeriods');
|
||||
console.log(listPeriods);
|
||||
const period =
|
||||
listPeriods.length > 0
|
||||
? listPeriods.find((p) => p.id == props.row.original.id)
|
||||
: undefined;
|
||||
return period?.period ?? '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorFn: (row) => row.pic?.name,
|
||||
header: 'Penanggung Jawab',
|
||||
|
||||
Reference in New Issue
Block a user