mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE-181-179-220): Slicing UI, Client Side Validation and API Integration for Delivery Order
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
'use client';
|
||||
|
||||
import SalesForm from '@/components/pages/marketing/form/MarketingForm';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const EditMarketingDelivery = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(`get-so-${soId}`, () =>
|
||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && (!marketing || isResponseError(marketing))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<SalesForm
|
||||
formType='deliver'
|
||||
initialValues={marketing.data}
|
||||
afterSubmit={() => {
|
||||
refreshMarketing();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default EditMarketingDelivery;
|
||||
@@ -0,0 +1,11 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -12,9 +12,12 @@ const EditSalesOrder = () => {
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(
|
||||
`get-so-${soId}`,
|
||||
() => MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(`get-so-${soId}`, () =>
|
||||
MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
@@ -35,7 +38,13 @@ const EditSalesOrder = () => {
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<SalesForm formType='edit' initialValues={marketing.data} />
|
||||
<SalesForm
|
||||
formType='edit'
|
||||
initialValues={marketing.data}
|
||||
afterSubmit={() => {
|
||||
refreshMarketing();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,10 @@ const DateInput = ({
|
||||
|
||||
// --- Sync value props ---
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
if (!value) {
|
||||
setDisplayValue('');
|
||||
return;
|
||||
}
|
||||
if (isRange && typeof value === 'object') {
|
||||
const from = value.from ? new Date(value.from) : undefined;
|
||||
const to = value.to ? new Date(value.to) : undefined;
|
||||
|
||||
@@ -56,15 +56,28 @@ 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={`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${props.row.original.id}`}
|
||||
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/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>
|
||||
)}
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
@@ -160,7 +173,7 @@ const SalesOrderTable = () => {
|
||||
);
|
||||
|
||||
const disableApprove = !hasApprovable || hasRejectable;
|
||||
const disableReject = !hasRejectable || hasApprovable;
|
||||
// const disableReject = !hasRejectable || hasApprovable;
|
||||
|
||||
const idsToProcess =
|
||||
approveAction === 'APPROVED'
|
||||
@@ -224,7 +237,7 @@ const SalesOrderTable = () => {
|
||||
|
||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||
const step = row.original.latest_approval?.step_number;
|
||||
return step === 1 || step === 2;
|
||||
return step === 1;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -258,7 +271,7 @@ const SalesOrderTable = () => {
|
||||
Approve
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
className='justify-start text-sm'
|
||||
@@ -266,7 +279,7 @@ const SalesOrderTable = () => {
|
||||
>
|
||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
|
||||
@@ -11,13 +11,25 @@ import ApprovalSteps, {
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import Table from '@/components/Table';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
import { BaseSalesOrder, Marketing } from '@/types/api/marketing/marketing';
|
||||
import {
|
||||
BaseDelivery,
|
||||
BaseDeliveryOrder,
|
||||
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';
|
||||
|
||||
@@ -28,6 +40,7 @@ const SalesOrderDetail = ({
|
||||
initialValues?: Marketing;
|
||||
refresh?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
@@ -57,10 +70,10 @@ const SalesOrderDetail = ({
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalAction('REJECTED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
// const rejectClickHandler = () => {
|
||||
// setApprovalAction('REJECTED');
|
||||
// confirmationModal.openModal();
|
||||
// };
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
@@ -104,6 +117,9 @@ const SalesOrderDetail = ({
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
router.push(
|
||||
`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${initialValues?.id}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -127,26 +143,30 @@ const SalesOrderDetail = ({
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 2}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button>
|
||||
</Button> */}
|
||||
</>
|
||||
)}
|
||||
{initialValues?.latest_approval?.step_number == 2 && (
|
||||
<Button color='success' onClick={deliveryClickHandler}>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
<Button
|
||||
color='success'
|
||||
// href={`/marketing/sales-orders/detail/edit/delivery?salesOrderId=${initialValues?.id}`}
|
||||
onClick={deliveryClickHandler}
|
||||
>
|
||||
<Icon icon='mdi:truck' width={24} height={24} />
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title='Informasi Sales Order'
|
||||
title='Informasi Penjualan'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
@@ -159,7 +179,7 @@ const SalesOrderDetail = ({
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.name}</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
@@ -186,13 +206,23 @@ const SalesOrderDetail = ({
|
||||
<td>:</td>
|
||||
<td>{initialValues?.notes ?? '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Dokumen</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
<Button className='py-2 px-3 font-medium text-md'>
|
||||
<Icon icon='mdi:file-pdf' width={16} height={16} />
|
||||
{initialValues?.so_number}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
{initialValues?.sales_order && (
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'w-full bg-white',
|
||||
}}
|
||||
@@ -262,6 +292,117 @@ const SalesOrderDetail = ({
|
||||
/>
|
||||
</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'>
|
||||
<Button className='py-2 px-3 font-medium text-md'>
|
||||
<Icon icon='mdi:file-pdf' width={16} height={16} />
|
||||
{delivery.do_number}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
|
||||
@@ -3,6 +3,10 @@ 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;
|
||||
@@ -22,6 +26,10 @@ 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!'),
|
||||
@@ -38,6 +46,16 @@ export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
|
||||
export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
||||
Yup.object({
|
||||
delivery_order: Yup.array()
|
||||
.of(DeliveryOrderProductSchema)
|
||||
.min(1, 'Pengiriman wajib diisi!')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const UpdateSalesOrderSchema = SalesOrderSchema;
|
||||
|
||||
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
|
||||
|
||||
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
||||
|
||||
@@ -12,19 +12,27 @@ import TextArea from '@/components/input/TextArea';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||
import {
|
||||
BaseDeliveryOrder,
|
||||
BaseSalesOrder,
|
||||
CreateSalesOrderPayload,
|
||||
CreateSalesOrderProductPayload,
|
||||
Marketing,
|
||||
UpdateDeliveryOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { useFormik } from 'formik';
|
||||
import { SalesOrderFormValues, SalesOrderSchema } from './MarketingForm.schema';
|
||||
import {
|
||||
DeliveryOrderFormValues,
|
||||
DeliveryOrderSchema,
|
||||
SalesOrderFormValues,
|
||||
SalesOrderSchema,
|
||||
} from './MarketingForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import {
|
||||
DeliveryOrderApi,
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
} from '@/services/api/marketing/marketing';
|
||||
@@ -34,6 +42,9 @@ 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 MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
@@ -59,6 +70,49 @@ const MarketingProductToFieldValues = (
|
||||
};
|
||||
};
|
||||
|
||||
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 SalesForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
@@ -69,24 +123,39 @@ const SalesForm = ({
|
||||
afterSubmit?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const addProductModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedMarketingProduct, setSelectedMarketingProduct] =
|
||||
useState<SalesOrderProductFormValues | null>(null);
|
||||
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
|
||||
useState<DeliveryOrderProductFormValues | null>(null);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const selectedRowIds = Object.keys(rowSelection).map((item) =>
|
||||
// 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(() => {
|
||||
const formikInitialValues = useMemo<
|
||||
SalesOrderFormValues & DeliveryOrderFormValues
|
||||
>(() => {
|
||||
return {
|
||||
so_date: initialValues?.so_date || undefined,
|
||||
notes: initialValues?.notes || undefined,
|
||||
@@ -102,43 +171,69 @@ const SalesForm = ({
|
||||
initialValues?.sales_order?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
delivery_order:
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? [],
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<SalesOrderFormValues>({
|
||||
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: SalesOrderSchema,
|
||||
validationSchema:
|
||||
formType === 'deliver' ? DeliveryOrderSchema : SalesOrderSchema,
|
||||
validateOnMount: true,
|
||||
onSubmit: async (values) => {
|
||||
console.log('VALUES');
|
||||
console.log(values);
|
||||
const payload = {
|
||||
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;
|
||||
const payload =
|
||||
formType != '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) => {
|
||||
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,
|
||||
};
|
||||
}),
|
||||
} as UpdateDeliveryOrderPayload);
|
||||
console.log('PAYLOAD');
|
||||
console.log(payload);
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
await createMarketingHandler(payload);
|
||||
await createMarketingHandler(payload as CreateSalesOrderPayload);
|
||||
break;
|
||||
case 'edit':
|
||||
await updateMarketingHandler(payload);
|
||||
await updateMarketingHandler(payload as CreateSalesOrderPayload);
|
||||
break;
|
||||
case 'deliver':
|
||||
await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -156,6 +251,7 @@ const SalesForm = ({
|
||||
}, [formik.values.sales_order]);
|
||||
|
||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const createMarketingRes = await SalesOrderApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
@@ -165,8 +261,11 @@ const SalesForm = ({
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
toast.error(createMarketingRes?.message as string);
|
||||
}
|
||||
afterSubmit?.();
|
||||
setIsLoading(false);
|
||||
};
|
||||
const updateMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
const updateMarketingRes = await SalesOrderApi.update(
|
||||
initialValues?.id as number,
|
||||
@@ -174,12 +273,45 @@ const SalesForm = ({
|
||||
);
|
||||
if (isResponseSuccess(updateMarketingRes)) {
|
||||
toast.success(updateMarketingRes?.message as string);
|
||||
router.push('/marketing/sales-orders');
|
||||
router.push(
|
||||
`/marketing/sales-orders/detail?salesOrderId=${initialValues?.id}`
|
||||
);
|
||||
}
|
||||
if (isResponseError(updateMarketingRes)) {
|
||||
toast.error(updateMarketingRes?.message as string);
|
||||
}
|
||||
afterSubmit?.();
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
const updateDeliveryRes =
|
||||
initialValues?.delivery_order && initialValues?.delivery_order.length > 0
|
||||
? await DeliveryOrderApi.update(initialValues?.id as number, values)
|
||||
: await DeliveryOrderApi.update(initialValues?.id as number, values);
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
formik.setFieldValue(
|
||||
'delivery_order',
|
||||
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
updateDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.error(updateDeliveryRes?.message as string);
|
||||
}
|
||||
afterSubmit?.();
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
@@ -199,7 +331,16 @@ const SalesForm = ({
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
|
||||
const handleDeleteProduct = useCallback(
|
||||
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(
|
||||
@@ -209,28 +350,24 @@ const SalesForm = ({
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleBulkDeleteProduct = useCallback(() => {
|
||||
const handleBulkDeleteSO = useCallback(() => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter(
|
||||
(product) => !selectedRowIds.includes(product.id ?? -1)
|
||||
(product) => !selectedRowSOIds.includes(product.id ?? -1)
|
||||
)
|
||||
);
|
||||
setRowSelection({});
|
||||
}, [formik, selectedRowIds]);
|
||||
|
||||
setRowSOSelection({});
|
||||
}, [formik, selectedRowSOIds]);
|
||||
const handleDelete = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const handleAddProductClick = useCallback(() => {
|
||||
const handleAddSOClick = useCallback(() => {
|
||||
setSelectedMarketingProduct(null);
|
||||
addProductModal.openModal();
|
||||
}, [addProductModal]);
|
||||
|
||||
const handleAddSubmitProduct = useCallback(
|
||||
addSOModal.openModal();
|
||||
}, [addSOModal]);
|
||||
const handleAddSubmitSO = useCallback(
|
||||
async (values: SalesOrderProductFormValues) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
const newValues = {
|
||||
@@ -240,21 +377,84 @@ const SalesForm = ({
|
||||
|
||||
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
|
||||
|
||||
addProductModal.closeModal();
|
||||
addSOModal.closeModal();
|
||||
},
|
||||
[formik, addProductModal]
|
||||
[formik, addSOModal]
|
||||
);
|
||||
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
const handleDeleteDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order;
|
||||
formik.setFieldValue(
|
||||
'delivery_order',
|
||||
currentProducts.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(() => {
|
||||
const currentProducts = formik.values.delivery_order;
|
||||
formik.setFieldValue(
|
||||
'delivery_order',
|
||||
currentProducts.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 currentProducts = formik.values.delivery_order;
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
formik.setFieldValue('delivery_order', [...currentProducts, newValues]);
|
||||
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
formik.setFieldValue(
|
||||
'delivery_order',
|
||||
formik.values.delivery_order.map((product) => {
|
||||
if (product.id === id) {
|
||||
return {
|
||||
...product,
|
||||
...values,
|
||||
};
|
||||
}
|
||||
return product;
|
||||
})
|
||||
);
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.closeModal();
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
// End Repeater Handle
|
||||
|
||||
const memoSalesOrder = formik.values.sales_order;
|
||||
|
||||
const memoDeliveryOrder = formik.values.delivery_order;
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
@@ -285,6 +485,7 @@ const SalesForm = ({
|
||||
errorMessage={formik.errors.customer_id}
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
isDisabled={formType === 'deliver'}
|
||||
/>
|
||||
<DateInput
|
||||
name='so_date'
|
||||
@@ -294,28 +495,56 @@ const SalesForm = ({
|
||||
isError={formik.touched.so_date && Boolean(formik.errors.so_date)}
|
||||
errorMessage={formik.errors.so_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
readOnly={formType == 'deliver'}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title='Daftar Produk'
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
|
||||
{/* <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>
|
||||
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
|
||||
<SalesOrderProductTable
|
||||
formType={formType}
|
||||
data={memoSalesOrder}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
selectedRowIds={selectedRowIds}
|
||||
onDelete={handleDeleteProduct}
|
||||
onBulkDelete={handleBulkDeleteProduct}
|
||||
onAddProductClick={handleAddProductClick}
|
||||
rowSelection={rowSOSelection}
|
||||
setRowSelection={setRowSOSelection}
|
||||
selectedRowIds={selectedRowSOIds}
|
||||
onDelete={handleDeleteSO}
|
||||
onBulkDelete={handleBulkDeleteSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</Card>
|
||||
{formType == 'deliver' &&
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order.length > 0 && (
|
||||
<Card
|
||||
title='Informasi Pengiriman'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* {JSON.stringify(memoSalesOrder)}
|
||||
{JSON.stringify(memoDeliveryOrder)} */}
|
||||
<DeliveryOrderProductTable
|
||||
formType={formType}
|
||||
data={memoDeliveryOrder}
|
||||
salesOrder={memoSalesOrder}
|
||||
rowSelection={rowDOSelection}
|
||||
setRowSelection={setRowDOSelection}
|
||||
selectedRowIds={selectedRowDOIds}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onBulkDelete={handleBulkDeleteDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
required
|
||||
@@ -327,6 +556,7 @@ const SalesForm = ({
|
||||
onChange={formik.handleChange}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes}
|
||||
disabled={formType === 'deliver'}
|
||||
/>
|
||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
||||
<span>Total Penjualan</span>
|
||||
@@ -362,7 +592,7 @@ const SalesForm = ({
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
ref={addProductModal.ref}
|
||||
ref={addSOModal.ref}
|
||||
closeOnBackdrop
|
||||
className={{
|
||||
modalBox: 'max-w-4/5 z-100',
|
||||
@@ -375,20 +605,50 @@ const SalesForm = ({
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='rounded-full'
|
||||
onClick={addProductModal.closeModal}
|
||||
onClick={addSOModal.closeModal}
|
||||
>
|
||||
<Icon icon='mdi:close' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<SalesOrderProductForm
|
||||
onSubmitForm={handleAddSubmitProduct}
|
||||
modalRef={addProductModal.ref}
|
||||
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>
|
||||
<DeliveryOrderProductForm
|
||||
salesOrders={initialValues?.sales_order ?? []}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
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(1, '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!'),
|
||||
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,345 @@
|
||||
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: false,
|
||||
validateOnChange: true,
|
||||
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'>
|
||||
<DateInput
|
||||
name='delivery_date'
|
||||
label='Tanggal'
|
||||
value={formik.values.delivery_date}
|
||||
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}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
options={options}
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
value={selectedProduct}
|
||||
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
|
||||
/>
|
||||
|
||||
<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;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
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';
|
||||
|
||||
type DeliveryOrderProductTableProps = {
|
||||
data: DeliveryOrderProductFormValues[];
|
||||
salesOrder: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | '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;
|
||||
};
|
||||
|
||||
const DeliveryOrderProductTable = ({
|
||||
data,
|
||||
salesOrder,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
}: 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(
|
||||
() => [
|
||||
{
|
||||
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>
|
||||
) => props.row.original.do_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatDate(row.delivery_date as string, 'DD MMM YYYY'),
|
||||
header: 'Tanggal Delivery',
|
||||
},
|
||||
{
|
||||
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='success'
|
||||
className='p-1'
|
||||
onClick={() => onEditRef.current(props.row.original.id as number)}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:edit' width={16} height={16} />
|
||||
</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>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
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 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 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;
|
||||
@@ -16,6 +16,7 @@ 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>>
|
||||
@@ -28,6 +29,7 @@ type SalesOrderProductTableProps = {
|
||||
|
||||
const SalesOrderProductTable = ({
|
||||
data,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
@@ -137,7 +139,13 @@ const SalesOrderProductTable = ({
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={columns}
|
||||
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!',
|
||||
@@ -159,33 +167,35 @@ const SalesOrderProductTable = ({
|
||||
</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}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
{selectedRowIds.length > 0 && (
|
||||
{formType != 'deliver' && (
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='error'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onBulkDelete}
|
||||
onClick={onAddProductClick}
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
Hapus
|
||||
{selectedRowIds.length > 0
|
||||
? ` (${selectedRowIds.length})`
|
||||
: ''}{' '}
|
||||
Produk
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Produk
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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';
|
||||
@@ -42,10 +41,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:
|
||||
|
||||
@@ -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';
|
||||
@@ -143,15 +142,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 +180,7 @@ const ProjectFlockForm = ({
|
||||
formik.setFieldValue('kandang_ids', selectedRowIds);
|
||||
}
|
||||
}
|
||||
refreshPeriodFlocks();
|
||||
}
|
||||
}, [kandang, selectedLocation]);
|
||||
useEffect(() => {
|
||||
@@ -319,7 +318,6 @@ const ProjectFlockForm = ({
|
||||
>,
|
||||
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
|
||||
@@ -370,7 +368,6 @@ const ProjectFlockForm = ({
|
||||
>,
|
||||
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
|
||||
@@ -390,7 +387,6 @@ const ProjectFlockForm = ({
|
||||
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 +415,6 @@ const ProjectFlockForm = ({
|
||||
if (initialValues?.area_id) {
|
||||
setSelectedArea(initialValues?.area_id.toString() as string);
|
||||
}
|
||||
|
||||
formik.setFieldValue('period', initialValues?.period);
|
||||
}
|
||||
}, [initialValues, setSelectedArea, formType]);
|
||||
|
||||
@@ -449,15 +443,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])
|
||||
@@ -555,7 +540,7 @@ const ProjectFlockForm = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{approvals && !approvalsLoading && (
|
||||
{approvals && !approvalsLoading && formType == 'detail' && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
{formType == 'detail' && (
|
||||
@@ -701,22 +686,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 +719,9 @@ const ProjectFlockForm = ({
|
||||
<span className='loading loading-dots loading-xl'></span>
|
||||
)}
|
||||
<ProjectFlockKandangTable
|
||||
listPeriods={
|
||||
isResponseSuccess(periodFlocks) ? periodFlocks.data : []
|
||||
}
|
||||
listKandang={optionsKandang}
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -277,7 +277,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'DRAFT',
|
||||
name: 'SO-001-2025',
|
||||
// name: 'SO-001-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-001-2025',
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 1,
|
||||
@@ -313,7 +314,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 2,
|
||||
status: 'APPROVED',
|
||||
name: 'SO-002-2025',
|
||||
// name: 'SO-002-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-002-2025',
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 2,
|
||||
@@ -350,7 +352,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 3,
|
||||
status: 'DELIVERED', // Asumsi status DELIVERED berarti DO sudah selesai/terbuat
|
||||
name: 'SO-003-2025',
|
||||
// name: 'SO-003-2025', // `name` is not part of BaseMarketing
|
||||
so_number: 'SO-003-2025',
|
||||
so_date: today,
|
||||
customer: {
|
||||
id: 3,
|
||||
|
||||
@@ -2,7 +2,6 @@ import moment from 'moment';
|
||||
import 'moment/locale/id';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import clsx, { ClassValue } from 'clsx';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
// set locale globally
|
||||
moment.locale('id');
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Marketing,
|
||||
CreateSalesOrderPayload,
|
||||
UpdateSalesOrderPayload,
|
||||
CreateDeliveryOrderPayload,
|
||||
UpdateDeliveryOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
|
||||
/**
|
||||
@@ -120,7 +122,7 @@ export class SalesOrderService extends BaseApiService<
|
||||
notes?: string
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/deliveries`;
|
||||
const path = `${this.basePath}/approvals`;
|
||||
return await httpClient<BaseApiResponse<{ message: string }>>(path, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -137,6 +139,11 @@ export class SalesOrderService extends BaseApiService<
|
||||
}
|
||||
|
||||
export const SalesOrderApi = new SalesOrderService('/marketing/sales-orders');
|
||||
export const DeliveryOrderApi = new BaseApiService<
|
||||
Marketing,
|
||||
CreateDeliveryOrderPayload,
|
||||
UpdateDeliveryOrderPayload
|
||||
>('/marketing/delivery-orders');
|
||||
export const MarketingApi = new BaseApiService<Marketing, unknown, unknown>(
|
||||
'/marketing'
|
||||
);
|
||||
|
||||
@@ -8,13 +8,10 @@ import {
|
||||
BaseApiResponse,
|
||||
BaseGroupedApproval,
|
||||
ErrorApiResponse,
|
||||
GroupedApprovals,
|
||||
SuccessApiResponse,
|
||||
} from '@/types/api/api-general';
|
||||
import { sleep } from '@/lib/helper';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
import axios from 'axios';
|
||||
import { Flock } from '@/types/api/master-data/flock';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { RequestOptions } from '@/services/http/base';
|
||||
|
||||
@@ -104,25 +101,34 @@ export class ProjectFlockService extends BaseApiService<
|
||||
/**
|
||||
* Get Next Period of Project Flock
|
||||
*/
|
||||
async getNextPeriod(id: string): Promise<
|
||||
| BaseApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
async getNextPeriod(locationId: number): Promise<
|
||||
| BaseApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
| ErrorApiResponse
|
||||
| SuccessApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
| SuccessApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const path = `${this.basePath}/kandangs/${id}`;
|
||||
const path = `${this.basePath}/kandangs/${locationId.toString()}/periods`;
|
||||
return await httpClient<
|
||||
SuccessApiResponse<{
|
||||
flock: Flock;
|
||||
next_period: number;
|
||||
}>
|
||||
SuccessApiResponse<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[]
|
||||
>
|
||||
>(path, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
+19
-2
@@ -15,9 +15,9 @@ import { Warehouse } from '../master-data/warehouse';
|
||||
export type BaseMarketing = {
|
||||
id: number;
|
||||
status?: string;
|
||||
name: string;
|
||||
customer: Customer;
|
||||
so_number: string;
|
||||
so_date: string;
|
||||
customer: Customer;
|
||||
sales_person: CreatedUser;
|
||||
notes: string;
|
||||
latest_approval: BaseApproval;
|
||||
@@ -118,6 +118,23 @@ export type CreateSalesOrderProductPayload =
|
||||
product_warehouse?: ProductWarehouse | undefined;
|
||||
};
|
||||
|
||||
export type CreateDeliveryOrderPayload = {
|
||||
marketing_id?: number;
|
||||
delivery_products: CreateDeliveryOrderProductPayload[];
|
||||
};
|
||||
|
||||
export type CreateDeliveryOrderProductPayload =
|
||||
BaseCreateMarketingProductPayload & {
|
||||
id?: number;
|
||||
marketing_product_id: number;
|
||||
delivery_date: string;
|
||||
};
|
||||
|
||||
export type UpdateSalesOrderProductPayload = CreateSalesOrderProductPayload;
|
||||
|
||||
export type UpdateDeliveryOrderProductPayload =
|
||||
CreateDeliveryOrderProductPayload;
|
||||
|
||||
export type UpdateSalesOrderPayload = CreateSalesOrderPayload;
|
||||
|
||||
export type UpdateDeliveryOrderPayload = CreateDeliveryOrderPayload;
|
||||
|
||||
+6
-1
@@ -41,7 +41,6 @@ export type CreateProjectFlockPayload = {
|
||||
category: string;
|
||||
fcr_id: number;
|
||||
location_id: number;
|
||||
period: number;
|
||||
kandang_ids: number[];
|
||||
};
|
||||
|
||||
@@ -61,3 +60,9 @@ export type ProjectFlockAvailableQuantity = {
|
||||
available_qty: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ProjectFlockPeriods = {
|
||||
id: number;
|
||||
name: string;
|
||||
period: number;
|
||||
}[];
|
||||
|
||||
Reference in New Issue
Block a user