mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-177): refactor sales order management with new schema and API integration
This commit is contained in:
@@ -12,8 +12,9 @@ const EditSalesOrder = () => {
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
|
||||
MarketingApi.getSingle(id)
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(
|
||||
`get-so-${soId}`,
|
||||
() => MarketingApi.getSingle(soId ? parseInt(soId) : 0)
|
||||
);
|
||||
|
||||
if (!soId) {
|
||||
|
||||
@@ -12,9 +12,11 @@ const DetailSalesOrder = () => {
|
||||
|
||||
const soId = searchParams.get('salesOrderId');
|
||||
|
||||
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) =>
|
||||
MarketingApi.getSingle(id)
|
||||
);
|
||||
const {
|
||||
data: marketing,
|
||||
isLoading: isLoading,
|
||||
mutate: refreshMarketing,
|
||||
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
|
||||
|
||||
if (!soId) {
|
||||
router.back();
|
||||
@@ -35,7 +37,10 @@ const DetailSalesOrder = () => {
|
||||
<div className='w-full p-4'>
|
||||
{isLoading && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoading && isResponseSuccess(marketing) && (
|
||||
<SalesOrderDetail initialValues={marketing.data} />
|
||||
<SalesOrderDetail
|
||||
initialValues={marketing.data}
|
||||
refresh={refreshMarketing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,12 @@ 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 {
|
||||
cn,
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
@@ -216,12 +221,15 @@ const SalesOrderTable = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_number',
|
||||
accessorKey: 'name',
|
||||
header: 'No. Order',
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
header: 'Tanggal',
|
||||
cell: (props) => {
|
||||
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'approval.step_name',
|
||||
@@ -232,8 +240,18 @@ const SalesOrderTable = () => {
|
||||
header: 'Customer',
|
||||
},
|
||||
{
|
||||
accessorKey: 'grand_total',
|
||||
accessorFn: (row) =>
|
||||
row.marketing_products
|
||||
?.map((product) => product.total_price)
|
||||
.reduce((a, b) => a + b, 0) ?? 0,
|
||||
header: 'Grand Total',
|
||||
cell: (props) => {
|
||||
return formatCurrency(
|
||||
props.row.original?.marketing_products
|
||||
?.map((product) => product.total_price)
|
||||
.reduce((a, b) => a + b, 0) ?? 0
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'marketing_products.length',
|
||||
|
||||
@@ -5,10 +5,15 @@ import Card from '@/components/Card';
|
||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
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';
|
||||
@@ -20,27 +25,42 @@ import toast from 'react-hot-toast';
|
||||
|
||||
const SalesOrderDetail = ({
|
||||
initialValues,
|
||||
refreshValues,
|
||||
refresh,
|
||||
}: {
|
||||
initialValues?: Marketing;
|
||||
refreshValues?: () => void;
|
||||
refresh?: () => void;
|
||||
}) => {
|
||||
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>(
|
||||
'approve'
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState(
|
||||
initialValues?.marketing_products
|
||||
?.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?.approval,
|
||||
approvalLines: MARKETING_APPROVAL_LINE,
|
||||
moduleName: 'MARKETINGS',
|
||||
moduleId: initialValues?.id as number as unknown as string,
|
||||
});
|
||||
|
||||
const approveClickHandler = () => {
|
||||
setApprovalAction('approve');
|
||||
setApprovalAction('APPROVED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalAction('reject');
|
||||
setApprovalAction('REJECTED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
@@ -54,23 +74,24 @@ const SalesOrderDetail = ({
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.delete(initialValues?.id as number);
|
||||
const res = await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully deleted Sales Order!');
|
||||
refreshValues?.();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
// await MarketingApi.singleApproval(
|
||||
// initialValues?.id as number,
|
||||
// approvalAction
|
||||
// );
|
||||
const res = await MarketingApi.singleApproval(
|
||||
initialValues?.id as number,
|
||||
approvalAction
|
||||
);
|
||||
setIsLoading(false);
|
||||
confirmationModal.closeModal();
|
||||
toast.success('Successfully approved Sales Order!');
|
||||
refreshValues?.();
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
refreshApproval?.();
|
||||
};
|
||||
|
||||
const confirmationModalDeliveryClickHandler = async () => {
|
||||
@@ -79,7 +100,7 @@ const SalesOrderDetail = ({
|
||||
setIsLoading(false);
|
||||
deliveryModal.closeModal();
|
||||
toast.success('Successfully delivered Sales Order!');
|
||||
refreshValues?.();
|
||||
refresh?.();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -89,6 +110,9 @@ const SalesOrderDetail = ({
|
||||
title='Detail Sales Order'
|
||||
backUrl='/marketing/sales-orders'
|
||||
/>
|
||||
{!isLoadingApproval && approvals && (
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.approval?.step_number != 3 && (
|
||||
<>
|
||||
@@ -117,6 +141,7 @@ const SalesOrderDetail = ({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title='Informasi Sales Order'
|
||||
className={{
|
||||
@@ -131,7 +156,7 @@ const SalesOrderDetail = ({
|
||||
No. Sales Order
|
||||
</td>
|
||||
<td>:</td>
|
||||
<td width='50%'>{initialValues?.so_number}</td>
|
||||
<td width='50%'>{initialValues?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Nama Pelanggan</td>
|
||||
@@ -146,14 +171,12 @@ const SalesOrderDetail = ({
|
||||
<tr>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>{initialValues?.so_date}</td>
|
||||
<td>{formatDate(initialValues?.so_date, 'DD MMM yyyy')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Total Penjualan</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
{formatCurrency(initialValues?.grand_total as number)}
|
||||
</td>
|
||||
<td>{formatCurrency(grandTotal as number)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='font-semibold'>Catatan</td>
|
||||
@@ -178,7 +201,7 @@ const SalesOrderDetail = ({
|
||||
header: 'No. Polisi',
|
||||
accessorFn(row) {
|
||||
return formatVechicleNumber(
|
||||
row.marketing_delivery_products?.vehicle_number as string
|
||||
row.delivery_product?.vehicle_number as string
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -275,14 +298,14 @@ const SalesOrderDetail = ({
|
||||
/>
|
||||
<ConfirmationModal
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'approve' ? 'success' : 'error'}
|
||||
type={approvalAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: approvalAction === 'approve' ? 'success' : 'error',
|
||||
color: approvalAction === 'APPROVED' ? 'success' : 'error',
|
||||
isLoading: isLoading,
|
||||
onClick: confirmationModalApproveClickHandler,
|
||||
}}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as Yup from 'yup';
|
||||
import { MarketingProduct } from '@/types/api/marketing/marketing';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from './repeater/MarketingProduct.schema';
|
||||
|
||||
type MarketingSchema = {
|
||||
type MarketingSchemaType = {
|
||||
customer_id: number | undefined;
|
||||
sales_person_id: number | undefined;
|
||||
customer:
|
||||
| {
|
||||
value: number;
|
||||
@@ -16,11 +17,16 @@ type MarketingSchema = {
|
||||
| null;
|
||||
so_date: string | undefined;
|
||||
notes: string | undefined;
|
||||
marketing_products: MarketingProductFormValues[];
|
||||
};
|
||||
|
||||
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
|
||||
type SalesOrderSchemaType = MarketingSchemaType & {
|
||||
marketing_products: SalesOrderProductFormValues[];
|
||||
};
|
||||
|
||||
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(),
|
||||
@@ -28,11 +34,11 @@ export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
|
||||
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!')
|
||||
.of(SalesOrderProductSchema)
|
||||
.min(1, 'Produk wajib diisi!')
|
||||
.required('Produk wajib diisi!'),
|
||||
});
|
||||
});
|
||||
|
||||
export const UpdateMarketingSchema = MarketingSchema;
|
||||
export const UpdateSalesOrderSchema = SalesOrderSchema;
|
||||
|
||||
export type MarketingFormValues = Yup.InferType<typeof MarketingSchema>;
|
||||
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
|
||||
|
||||
@@ -12,10 +12,10 @@ 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 { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
CreateSalesOrderPayload,
|
||||
CreateSalesOrderProductPayload,
|
||||
Marketing,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
@@ -26,10 +26,10 @@ 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 { SalesOrderFormValues, SalesOrderSchema } from './SalesForm.schema';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { MarketingProductFormValues } from './repeater/MarketingProduct.schema';
|
||||
import { SalesOrderProductFormValues } from './repeater/MarketingProduct.schema';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -37,9 +37,11 @@ import { useRouter } from 'next/navigation';
|
||||
const SalesForm = ({
|
||||
formType = 'add',
|
||||
initialValues,
|
||||
afterSubmit,
|
||||
}: {
|
||||
formType?: 'add' | 'edit';
|
||||
initialValues?: Marketing;
|
||||
afterSubmit?: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const addProductModal = useModal();
|
||||
@@ -61,11 +63,9 @@ const SalesForm = ({
|
||||
parseInt(item)
|
||||
);
|
||||
const [grandTotal, setGrandTotal] = useState<number>(
|
||||
initialValues?.grand_total ?? 0
|
||||
);
|
||||
const marketingProducts = useMemo(
|
||||
() => rawMarketingProducts,
|
||||
[rawMarketingProducts]
|
||||
initialValues?.marketing_products
|
||||
?.map((item) => item.total_price)
|
||||
.reduce((a, b) => a + b, 0) ?? 0
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -91,8 +91,8 @@ const SalesForm = ({
|
||||
|
||||
const handleAddSubmitProduct = useCallback(
|
||||
async (
|
||||
tableValue: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
tableValue: CreateSalesOrderProductPayload,
|
||||
fieldValues: SalesOrderProductFormValues
|
||||
) => {
|
||||
const newMarketingProduct: MarketingProduct = {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
@@ -102,10 +102,10 @@ const SalesForm = ({
|
||||
qty: tableValue.qty as number,
|
||||
avg_weight: tableValue.avg_weight as number,
|
||||
total_price: tableValue.total_price as number,
|
||||
marketing_delivery_products: {
|
||||
delivery_product: {
|
||||
id: rawMarketingProducts.length + 1,
|
||||
vehicle_number: tableValue.vehicle_number as string,
|
||||
delivery_date: tableValue.delivery_date as string,
|
||||
delivery_date: '' as string,
|
||||
unit_price: tableValue.unit_price as number,
|
||||
total_weight: tableValue.total_weight as number,
|
||||
qty: tableValue.qty as number,
|
||||
@@ -133,32 +133,30 @@ const SalesForm = ({
|
||||
[selectedCustomer, setSelectedCustomer]
|
||||
);
|
||||
|
||||
const createMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.create(values);
|
||||
if (isResponseSuccess(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
toast.success(createMarketingRes?.message as string);
|
||||
router.push('/marketing/sales-orders');
|
||||
}
|
||||
if (isResponseError(createMarketingRes)) {
|
||||
console.log(createMarketingRes);
|
||||
toast.error(createMarketingRes?.message as string);
|
||||
}
|
||||
toast.success('Successfully created Sales Order!');
|
||||
router.push('/marketing/sales-orders');
|
||||
};
|
||||
const updateMarketingHandler = async (values: CreateMarketingPayload) => {
|
||||
const updateMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
console.log(values);
|
||||
const createMarketingRes = await MarketingApi.update(
|
||||
const updateMarketingRes = 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!');
|
||||
if (isResponseSuccess(updateMarketingRes)) {
|
||||
toast.success(updateMarketingRes?.message as string);
|
||||
router.push('/marketing/sales-orders');
|
||||
}
|
||||
if (isResponseError(updateMarketingRes)) {
|
||||
toast.error(updateMarketingRes?.message as string);
|
||||
}
|
||||
};
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -180,9 +178,9 @@ const SalesForm = ({
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: MarketingProduct
|
||||
): MarketingProductFormValues => {
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
vehicle_number: product.marketing_delivery_products?.vehicle_number,
|
||||
vehicle_number: product.delivery_product?.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
@@ -196,19 +194,17 @@ const SalesForm = ({
|
||||
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: {
|
||||
const formikInitialValues = useMemo(() => {
|
||||
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: {
|
||||
value: initialValues?.customer?.id as number,
|
||||
label: initialValues?.customer?.name as string,
|
||||
@@ -217,15 +213,33 @@ const SalesForm = ({
|
||||
initialValues?.marketing_products?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
) ?? [],
|
||||
},
|
||||
validationSchema: MarketingSchema,
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<SalesOrderFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: SalesOrderSchema,
|
||||
validateOnMount: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload = {
|
||||
customer_id: values.customer_id as number,
|
||||
date: values.so_date as string,
|
||||
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.marketing_products,
|
||||
} as CreateMarketingPayload;
|
||||
marketing_products: values.marketing_products.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;
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
createMarketingHandler(payload);
|
||||
@@ -236,6 +250,7 @@ const SalesForm = ({
|
||||
default:
|
||||
break;
|
||||
}
|
||||
afterSubmit?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -245,6 +260,33 @@ const SalesForm = ({
|
||||
formikSetValues(formik.initialValues);
|
||||
}, [formikSetValues, formik.initialValues]);
|
||||
|
||||
useEffect(() => {
|
||||
// Konversi array MarketingProduct ke format SalesOrderProductFormValues
|
||||
const newMarketingProductValues = rawMarketingProducts.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
);
|
||||
|
||||
// Hitung Grand Total baru
|
||||
const newGrandTotal = rawMarketingProducts.reduce(
|
||||
(total, product) => total + product.total_price,
|
||||
0
|
||||
);
|
||||
|
||||
// Perbarui nilai formik.values.marketing_products
|
||||
formik.setFieldValue(
|
||||
'marketing_products',
|
||||
newMarketingProductValues,
|
||||
false
|
||||
);
|
||||
// Parameter ketiga (false) untuk menghindari validasi secara langsung
|
||||
|
||||
// Perbarui state grandTotal
|
||||
setGrandTotal(newGrandTotal);
|
||||
|
||||
// Reset row selection setiap kali daftar produk berubah (opsional, tapi disarankan)
|
||||
setRowSelection({});
|
||||
}, [rawMarketingProducts]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -273,7 +315,7 @@ const SalesForm = ({
|
||||
},
|
||||
{
|
||||
accessorFn: (row: MarketingProduct) =>
|
||||
row.marketing_delivery_products?.vehicle_number,
|
||||
row.delivery_product?.vehicle_number,
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
@@ -321,7 +363,7 @@ const SalesForm = ({
|
||||
),
|
||||
},
|
||||
],
|
||||
[handleDeleteProduct] // dependensi tunggal
|
||||
[handleDeleteProduct, initialValues, rawMarketingProducts] // dependensi tunggal
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -372,10 +414,15 @@ const SalesForm = ({
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(formik.values.marketing_products)}
|
||||
<div className='text-green-500'>
|
||||
{JSON.stringify(formik.values.marketing_products)}
|
||||
</div>
|
||||
<span className='text-red-500'>{JSON.stringify(formik.errors)}</span>
|
||||
<Table<MarketingProduct>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={marketingProducts}
|
||||
data={rawMarketingProducts}
|
||||
columns={columns}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
|
||||
+8
-7
@@ -15,13 +15,13 @@ 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;
|
||||
};
|
||||
|
||||
export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType> =
|
||||
type SalesOrderProductSchemaType = MarketingProductSchemaType;
|
||||
|
||||
const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType> =
|
||||
Yup.object({
|
||||
vehicle_number: Yup.string().required('No. Polisi wajib diisi!'),
|
||||
kandang: Yup.object({
|
||||
@@ -47,16 +47,17 @@ export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType
|
||||
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!')
|
||||
.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 const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
MarketingProductSchema;
|
||||
|
||||
export type SalesOrderProductFormValues = Yup.InferType<
|
||||
typeof SalesOrderProductSchema
|
||||
>;
|
||||
|
||||
+10
-21
@@ -1,17 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import {
|
||||
CreateMarketingPayload,
|
||||
CreateMarketingProductPayload,
|
||||
CreateSalesOrderProductPayload,
|
||||
MarketingProduct,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
MarketingProductFormValues,
|
||||
MarketingProductSchema,
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from './MarketingProduct.schema';
|
||||
import { RefObject, use, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useEffect, useState } from 'react';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
@@ -36,8 +34,8 @@ const MarketingProductForm = ({
|
||||
data: MarketingProduct[];
|
||||
modalRef?: RefObject<HTMLDialogElement | null>;
|
||||
onSubmitForm?: (
|
||||
tableValues: CreateMarketingProductPayload,
|
||||
fieldValues: MarketingProductFormValues
|
||||
tableValues: CreateSalesOrderProductPayload,
|
||||
fieldValues: SalesOrderProductFormValues
|
||||
) => Promise<void>;
|
||||
}) => {
|
||||
// State
|
||||
@@ -96,11 +94,11 @@ const MarketingProductForm = ({
|
||||
};
|
||||
|
||||
// Formik
|
||||
const formik = useFormik<MarketingProductFormValues>({
|
||||
const formik = useFormik<SalesOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
vehicle_number:
|
||||
initialValues?.marketing_delivery_products?.vehicle_number || undefined,
|
||||
initialValues?.delivery_product?.vehicle_number || undefined,
|
||||
kandang_id: initialValues?.product_warehouse.warehouse.id || undefined,
|
||||
kandang: {
|
||||
value: initialValues?.product_warehouse.warehouse.id as number,
|
||||
@@ -115,15 +113,10 @@ const MarketingProductForm = ({
|
||||
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,
|
||||
validationSchema: SalesOrderProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (
|
||||
@@ -137,7 +130,7 @@ const MarketingProductForm = ({
|
||||
(item: Kandang) => item.id === values.kandang_id
|
||||
);
|
||||
|
||||
const marketingProduct: CreateMarketingProductPayload = {
|
||||
const marketingProduct: CreateSalesOrderProductPayload = {
|
||||
id: initialValues?.id || undefined,
|
||||
vehicle_number: formatVechicleNumber(values.vehicle_number as string),
|
||||
kandang_id: values.kandang_id as number,
|
||||
@@ -147,10 +140,8 @@ const MarketingProductForm = ({
|
||||
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);
|
||||
@@ -178,10 +169,8 @@ const MarketingProductForm = ({
|
||||
unit_price: '',
|
||||
total_weight: '',
|
||||
qty: '',
|
||||
uom: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
delivery_date: new Date().toDateString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -32,3 +32,18 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
|
||||
step_name: 'Disetujui',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const MARKETING_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
},
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Sales Order',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Delivery Order',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -176,8 +176,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'APPROVED',
|
||||
so_number: 'SO-001-2025',
|
||||
so_docs: 'https://example.com/docs/so001.pdf',
|
||||
name: 'SO-001-2025',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
customer: {
|
||||
id: 1,
|
||||
@@ -193,9 +192,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
sales_person: createdUser,
|
||||
sales_person_id: createdUser.id,
|
||||
notes: 'Pengiriman awal bulan.',
|
||||
grand_total: 7500000,
|
||||
approval: {
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan Order',
|
||||
@@ -212,7 +210,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
total_weight: 250,
|
||||
total_price: 7500000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
delivery_product: {
|
||||
id: 1,
|
||||
qty: 100,
|
||||
unit_price: 75000,
|
||||
@@ -233,8 +231,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 2,
|
||||
status: 'APPROVED',
|
||||
so_number: 'SO-002-2025',
|
||||
so_docs: 'https://example.com/docs/so002.pdf',
|
||||
name: 'SO-002-2025',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
customer: {
|
||||
id: 2,
|
||||
@@ -250,9 +247,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
sales_person: createdUser,
|
||||
sales_person_id: createdUser.id,
|
||||
notes: 'Pesanan kedua untuk stok akhir tahun.',
|
||||
grand_total: 3750000,
|
||||
approval: {
|
||||
step_number: 2,
|
||||
step_name: 'Sales Order',
|
||||
@@ -269,7 +265,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
total_weight: 125,
|
||||
total_price: 3750000,
|
||||
product_warehouse: dummyProductWarehouses[1],
|
||||
marketing_delivery_products: {
|
||||
delivery_product: {
|
||||
id: 2,
|
||||
qty: 50,
|
||||
unit_price: 75000,
|
||||
@@ -290,8 +286,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
{
|
||||
id: 3,
|
||||
status: 'APPROVED',
|
||||
so_number: 'SO-003-2025',
|
||||
so_docs: 'https://example.com/docs/so003.pdf',
|
||||
name: 'SO-003-2025',
|
||||
so_date: format(new Date(), 'yyyy-MM-dd'),
|
||||
customer: {
|
||||
id: 3,
|
||||
@@ -307,9 +302,8 @@ export const dummyMarketings: Marketing[] = [
|
||||
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
updated_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
sales_person: createdUser,
|
||||
sales_person_id: createdUser.id,
|
||||
notes: 'Order untuk pengiriman ke luar kota.',
|
||||
grand_total: 5600000,
|
||||
approval: {
|
||||
step_number: 3,
|
||||
step_name: 'Delivery Order',
|
||||
@@ -326,7 +320,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
delivery_product: {
|
||||
id: 3,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
@@ -345,7 +339,7 @@ export const dummyMarketings: Marketing[] = [
|
||||
total_weight: 192,
|
||||
total_price: 5600000,
|
||||
product_warehouse: dummyProductWarehouses[0],
|
||||
marketing_delivery_products: {
|
||||
delivery_product: {
|
||||
id: 3,
|
||||
qty: 80,
|
||||
unit_price: 70000,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const formatNumber = (
|
||||
|
||||
export function formatVechicleNumber(value: string): string {
|
||||
let result = '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
for (let i = 0; i < (value?.length ?? 0); i++) {
|
||||
const curr = value[i];
|
||||
const prev = value[i - 1];
|
||||
|
||||
|
||||
@@ -5,82 +5,25 @@ import { httpClient } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import {
|
||||
Marketing,
|
||||
CreateMarketingPayload,
|
||||
UpdateMarketingPayload,
|
||||
CreateSalesOrderPayload,
|
||||
UpdateSalesOrderPayload,
|
||||
} from '@/types/api/marketing/marketing';
|
||||
|
||||
export class MarketingService extends BaseApiService<
|
||||
Marketing,
|
||||
CreateMarketingPayload,
|
||||
UpdateMarketingPayload
|
||||
CreateSalesOrderPayload,
|
||||
UpdateSalesOrderPayload
|
||||
> {
|
||||
constructor(basePath: string = '/marketing') {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override: Get all marketing data (dummy mode)
|
||||
*/
|
||||
override async getAllFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<Marketing[]>> {
|
||||
// simulasi loading
|
||||
await sleep(750);
|
||||
|
||||
// data dummy sementara
|
||||
const DUMMY_MARKETING_DATA: BaseApiResponse<Marketing[]> = {
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Berhasil mengambil data marketing (dummy)',
|
||||
data: dummyMarketings,
|
||||
};
|
||||
|
||||
return DUMMY_MARKETING_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override: Get single marketing data (dummy mode)
|
||||
*/
|
||||
override async getSingle(
|
||||
id: number
|
||||
): Promise<BaseApiResponse<Marketing> | undefined> {
|
||||
// simulasi delay
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
|
||||
const marketing = dummyMarketings.find((marketing) => {
|
||||
console.log('marketing', marketing);
|
||||
console.log('id-m', marketing.id);
|
||||
console.log('id-p', id);
|
||||
console.log('id', marketing.id == id);
|
||||
return marketing.id == id;
|
||||
});
|
||||
console.log('marketings', dummyMarketings);
|
||||
console.log('marketing', marketing);
|
||||
|
||||
if (marketing) {
|
||||
// misalnya fetch dari dummy
|
||||
return {
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Data marketing berhasil diambil.',
|
||||
data: marketing,
|
||||
};
|
||||
} else {
|
||||
// jika tidak ditemukan
|
||||
throw {
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: 'Data marketing tidak ditemukan.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve single marketing data
|
||||
*/
|
||||
async singleApproval(
|
||||
id: number,
|
||||
action: 'approve' | 'reject'
|
||||
action: 'APPROVED' | 'REJECTED'
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -88,7 +31,7 @@ export class MarketingService extends BaseApiService<
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: action,
|
||||
approval_ids: [id],
|
||||
approvable_ids: [id],
|
||||
notes: `${action} marketing ${id}`,
|
||||
},
|
||||
});
|
||||
@@ -103,7 +46,7 @@ export class MarketingService extends BaseApiService<
|
||||
*/
|
||||
async bulkApprovals(
|
||||
ids: number[],
|
||||
action: 'approve' | 'reject'
|
||||
action: 'APPROVED' | 'REJECTED'
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> {
|
||||
try {
|
||||
const path = `${this.basePath}/approvals`;
|
||||
@@ -111,7 +54,7 @@ export class MarketingService extends BaseApiService<
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: action,
|
||||
approval_ids: ids,
|
||||
approvable_ids: ids,
|
||||
notes: `${action} marketing ${ids.join(', ')}`,
|
||||
},
|
||||
});
|
||||
@@ -122,4 +65,4 @@ export class MarketingService extends BaseApiService<
|
||||
}
|
||||
}
|
||||
|
||||
export const MarketingApi = new MarketingService('/marketing');
|
||||
export const MarketingApi = new MarketingService('/marketing/sales-orders');
|
||||
|
||||
+33
-17
@@ -7,16 +7,17 @@ import {
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
|
||||
/**
|
||||
* Base Data Response
|
||||
*/
|
||||
export type BaseMarketing = {
|
||||
id: number;
|
||||
status?: string;
|
||||
so_number: string;
|
||||
name: string;
|
||||
customer: Customer;
|
||||
so_docs: string;
|
||||
so_date: string;
|
||||
sales_person: CreatedUser;
|
||||
sales_person_id: number;
|
||||
notes: string;
|
||||
grand_total: number;
|
||||
approval: BaseApproval;
|
||||
marketing_products?: MarketingProduct[];
|
||||
};
|
||||
@@ -29,7 +30,7 @@ export type MarketingProduct = {
|
||||
total_weight: number;
|
||||
total_price: number;
|
||||
product_warehouse: ProductWarehouse;
|
||||
marketing_delivery_products?: MarketingDeliveryProducts;
|
||||
delivery_product?: MarketingDeliveryProducts;
|
||||
};
|
||||
|
||||
export type MarketingDeliveryProducts = {
|
||||
@@ -39,34 +40,49 @@ export type MarketingDeliveryProducts = {
|
||||
avg_weight: number;
|
||||
total_weight: number;
|
||||
total_price: number;
|
||||
delivery_date: string;
|
||||
delivery_date: string | null;
|
||||
vehicle_number: string;
|
||||
do_number?: string | undefined;
|
||||
do_number?: string | undefined; // Uncertain
|
||||
};
|
||||
|
||||
export type Marketing = BaseMetadata & BaseMarketing;
|
||||
|
||||
export type CreateMarketingPayload = {
|
||||
/**
|
||||
* Base Data Payload
|
||||
*/
|
||||
export type BaseCreateMarketingPayload = {
|
||||
customer_id: number;
|
||||
sales_person_id: number;
|
||||
date: string;
|
||||
notes: string;
|
||||
marketing_products: CreateMarketingProductPayload[];
|
||||
};
|
||||
export type UpdateMarketingPayload = CreateMarketingPayload;
|
||||
|
||||
export type CreateMarketingProductPayload = {
|
||||
id?: number;
|
||||
export type BaseCreateMarketingProductPayload = {
|
||||
vehicle_number: string;
|
||||
kandang_id: string | number | undefined;
|
||||
kandang: Kandang | undefined;
|
||||
product_warehouse_id: string | number | undefined;
|
||||
product_warehouse: ProductWarehouse | undefined;
|
||||
unit_price: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
qty: string | number | undefined;
|
||||
uom: string | undefined;
|
||||
avg_weight: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
delivery_date?: string | null;
|
||||
};
|
||||
export type UpdateMarketingProductPayload = CreateMarketingProductPayload;
|
||||
|
||||
/**
|
||||
* Payload Data Types Sales Order
|
||||
*/
|
||||
|
||||
export type CreateSalesOrderPayload = BaseCreateMarketingPayload & {
|
||||
marketing_products: CreateSalesOrderProductPayload[];
|
||||
};
|
||||
|
||||
export type CreateSalesOrderProductPayload =
|
||||
BaseCreateMarketingProductPayload & {
|
||||
id?: number;
|
||||
kandang?: Kandang | undefined;
|
||||
product_warehouse?: ProductWarehouse | undefined;
|
||||
};
|
||||
|
||||
export type UpdateSalesOrderProductPayload = CreateSalesOrderProductPayload;
|
||||
|
||||
export type UpdateSalesOrderPayload = CreateSalesOrderPayload;
|
||||
|
||||
Reference in New Issue
Block a user