fix(resolve): fix resolve MR

This commit is contained in:
rstubryan
2025-11-22 10:25:37 +07:00
55 changed files with 4078 additions and 1907 deletions
@@ -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;
@@ -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;
@@ -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
>;
@@ -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,
}}
/>
@@ -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',