feat(FE-177): refactor sales order management with new schema and API integration

This commit is contained in:
randy-ar
2025-11-14 15:52:58 +07:00
parent 10976452f5
commit 3fdb10ec7f
15 changed files with 280 additions and 222 deletions
@@ -12,8 +12,9 @@ const EditSalesOrder = () => {
const soId = searchParams.get('salesOrderId'); const soId = searchParams.get('salesOrderId');
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => const { data: marketing, isLoading: isLoading } = useSWR(
MarketingApi.getSingle(id) `get-so-${soId}`,
() => MarketingApi.getSingle(soId ? parseInt(soId) : 0)
); );
if (!soId) { if (!soId) {
@@ -12,9 +12,11 @@ const DetailSalesOrder = () => {
const soId = searchParams.get('salesOrderId'); const soId = searchParams.get('salesOrderId');
const { data: marketing, isLoading: isLoading } = useSWR(soId, (id: number) => const {
MarketingApi.getSingle(id) data: marketing,
); isLoading: isLoading,
mutate: refreshMarketing,
} = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
if (!soId) { if (!soId) {
router.back(); router.back();
@@ -35,7 +37,10 @@ const DetailSalesOrder = () => {
<div className='w-full p-4'> <div className='w-full p-4'>
{isLoading && <span className='loading loading-spinner loading-xl' />} {isLoading && <span className='loading loading-spinner loading-xl' />}
{!isLoading && isResponseSuccess(marketing) && ( {!isLoading && isResponseSuccess(marketing) && (
<SalesOrderDetail initialValues={marketing.data} /> <SalesOrderDetail
initialValues={marketing.data}
refresh={refreshMarketing}
/>
)} )}
</div> </div>
); );
@@ -12,7 +12,12 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { TableToolbar } from '@/components/table/TableToolbar'; import { TableToolbar } from '@/components/table/TableToolbar';
import { ROWS_OPTIONS } from '@/config/constant'; import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper'; 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 { MarketingApi } from '@/services/api/marketing/marketing';
import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing'; import { Marketing, MarketingProduct } from '@/types/api/marketing/marketing';
@@ -216,12 +221,15 @@ const SalesOrderTable = () => {
), ),
}, },
{ {
accessorKey: 'so_number', accessorKey: 'name',
header: 'No. Order', header: 'No. Order',
}, },
{ {
accessorKey: 'so_date', accessorKey: 'so_date',
header: 'Tanggal', header: 'Tanggal',
cell: (props) => {
return formatDate(props.row.original.so_date, 'DD MMM yyyy');
},
}, },
{ {
accessorKey: 'approval.step_name', accessorKey: 'approval.step_name',
@@ -232,8 +240,18 @@ const SalesOrderTable = () => {
header: 'Customer', 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', 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', accessorKey: 'marketing_products.length',
@@ -5,10 +5,15 @@ import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader'; import { FormHeader } from '@/components/helper/form/FormHeader';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
import { import {
cn, cn,
formatCurrency, formatCurrency,
formatDate,
formatNumber, formatNumber,
formatVechicleNumber, formatVechicleNumber,
} from '@/lib/helper'; } from '@/lib/helper';
@@ -20,27 +25,42 @@ import toast from 'react-hot-toast';
const SalesOrderDetail = ({ const SalesOrderDetail = ({
initialValues, initialValues,
refreshValues, refresh,
}: { }: {
initialValues?: Marketing; initialValues?: Marketing;
refreshValues?: () => void; refresh?: () => void;
}) => { }) => {
const [approvalAction, setApprovalAction] = useState<'approve' | 'reject'>( const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
'approve' '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 [isLoading, setIsLoading] = useState(false);
const deleteModal = useModal(); const deleteModal = useModal();
const confirmationModal = useModal(); const confirmationModal = useModal();
const deliveryModal = 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 = () => { const approveClickHandler = () => {
setApprovalAction('approve'); setApprovalAction('APPROVED');
confirmationModal.openModal(); confirmationModal.openModal();
}; };
const rejectClickHandler = () => { const rejectClickHandler = () => {
setApprovalAction('reject'); setApprovalAction('REJECTED');
confirmationModal.openModal(); confirmationModal.openModal();
}; };
@@ -54,23 +74,24 @@ const SalesOrderDetail = ({
const confirmationModalDeleteClickHandler = async () => { const confirmationModalDeleteClickHandler = async () => {
setIsLoading(true); setIsLoading(true);
// await MarketingApi.delete(initialValues?.id as number); const res = await MarketingApi.delete(initialValues?.id as number);
setIsLoading(false); setIsLoading(false);
deleteModal.closeModal(); deleteModal.closeModal();
toast.success('Successfully deleted Sales Order!'); toast.success(res?.message as string);
refreshValues?.(); refresh?.();
}; };
const confirmationModalApproveClickHandler = async () => { const confirmationModalApproveClickHandler = async () => {
setIsLoading(true); setIsLoading(true);
// await MarketingApi.singleApproval( const res = await MarketingApi.singleApproval(
// initialValues?.id as number, initialValues?.id as number,
// approvalAction approvalAction
// ); );
setIsLoading(false); setIsLoading(false);
confirmationModal.closeModal(); confirmationModal.closeModal();
toast.success('Successfully approved Sales Order!'); toast.success(res?.message as string);
refreshValues?.(); refresh?.();
refreshApproval?.();
}; };
const confirmationModalDeliveryClickHandler = async () => { const confirmationModalDeliveryClickHandler = async () => {
@@ -79,7 +100,7 @@ const SalesOrderDetail = ({
setIsLoading(false); setIsLoading(false);
deliveryModal.closeModal(); deliveryModal.closeModal();
toast.success('Successfully delivered Sales Order!'); toast.success('Successfully delivered Sales Order!');
refreshValues?.(); refresh?.();
}; };
return ( return (
@@ -89,6 +110,9 @@ const SalesOrderDetail = ({
title='Detail Sales Order' title='Detail Sales Order'
backUrl='/marketing/sales-orders' backUrl='/marketing/sales-orders'
/> />
{!isLoadingApproval && approvals && (
<ApprovalSteps approvals={approvals} />
)}
<div className='flex-row flex gap-3'> <div className='flex-row flex gap-3'>
{initialValues?.approval?.step_number != 3 && ( {initialValues?.approval?.step_number != 3 && (
<> <>
@@ -117,6 +141,7 @@ const SalesOrderDetail = ({
</Button> </Button>
)} )}
</div> </div>
<Card <Card
title='Informasi Sales Order' title='Informasi Sales Order'
className={{ className={{
@@ -131,7 +156,7 @@ const SalesOrderDetail = ({
No. Sales Order No. Sales Order
</td> </td>
<td>:</td> <td>:</td>
<td width='50%'>{initialValues?.so_number}</td> <td width='50%'>{initialValues?.name}</td>
</tr> </tr>
<tr> <tr>
<td className='font-semibold'>Nama Pelanggan</td> <td className='font-semibold'>Nama Pelanggan</td>
@@ -146,14 +171,12 @@ const SalesOrderDetail = ({
<tr> <tr>
<td className='font-semibold'>Tanggal Penjualan</td> <td className='font-semibold'>Tanggal Penjualan</td>
<td>:</td> <td>:</td>
<td>{initialValues?.so_date}</td> <td>{formatDate(initialValues?.so_date, 'DD MMM yyyy')}</td>
</tr> </tr>
<tr> <tr>
<td className='font-semibold'>Total Penjualan</td> <td className='font-semibold'>Total Penjualan</td>
<td>:</td> <td>:</td>
<td> <td>{formatCurrency(grandTotal as number)}</td>
{formatCurrency(initialValues?.grand_total as number)}
</td>
</tr> </tr>
<tr> <tr>
<td className='font-semibold'>Catatan</td> <td className='font-semibold'>Catatan</td>
@@ -178,7 +201,7 @@ const SalesOrderDetail = ({
header: 'No. Polisi', header: 'No. Polisi',
accessorFn(row) { accessorFn(row) {
return formatVechicleNumber( return formatVechicleNumber(
row.marketing_delivery_products?.vehicle_number as string row.delivery_product?.vehicle_number as string
); );
}, },
}, },
@@ -275,14 +298,14 @@ const SalesOrderDetail = ({
/> />
<ConfirmationModal <ConfirmationModal
ref={confirmationModal.ref} ref={confirmationModal.ref}
type={approvalAction === 'approve' ? 'success' : 'error'} type={approvalAction === 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`} text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
secondaryButton={{ secondaryButton={{
text: 'Tidak', text: 'Tidak',
}} }}
primaryButton={{ primaryButton={{
text: 'Ya', text: 'Ya',
color: approvalAction === 'approve' ? 'success' : 'error', color: approvalAction === 'APPROVED' ? 'success' : 'error',
isLoading: isLoading, isLoading: isLoading,
onClick: confirmationModalApproveClickHandler, onClick: confirmationModalApproveClickHandler,
}} }}
@@ -1,12 +1,13 @@
import * as Yup from 'yup'; import * as Yup from 'yup';
import { MarketingProduct } from '@/types/api/marketing/marketing'; import { MarketingProduct } from '@/types/api/marketing/marketing';
import { import {
MarketingProductFormValues, SalesOrderProductFormValues,
MarketingProductSchema, SalesOrderProductSchema,
} from './repeater/MarketingProduct.schema'; } from './repeater/MarketingProduct.schema';
type MarketingSchema = { type MarketingSchemaType = {
customer_id: number | undefined; customer_id: number | undefined;
sales_person_id: number | undefined;
customer: customer:
| { | {
value: number; value: number;
@@ -16,23 +17,28 @@ type MarketingSchema = {
| null; | null;
so_date: string | undefined; so_date: string | undefined;
notes: string | undefined; notes: string | undefined;
marketing_products: MarketingProductFormValues[];
}; };
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({ type SalesOrderSchemaType = MarketingSchemaType & {
customer_id: Yup.number().required('Customer wajib diisi!'), marketing_products: SalesOrderProductFormValues[];
customer: Yup.object({ };
value: Yup.number().required(),
label: Yup.string().required(),
}).nullable(),
so_date: Yup.string().required('Tanggal wajib diisi!'),
notes: Yup.string().required('Catatan wajib diisi!'),
marketing_products: Yup.array()
.of(MarketingProductSchema)
.min(1, 'Minimal harus ada 1 produk!')
.required('Produk wajib diisi!'),
});
export const UpdateMarketingSchema = MarketingSchema; export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
Yup.object({
customer_id: Yup.number().required('Customer wajib diisi!'),
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
customer: Yup.object({
value: Yup.number().required(),
label: Yup.string().required(),
}).nullable(),
so_date: Yup.string().required('Tanggal wajib diisi!'),
notes: Yup.string().required('Catatan wajib diisi!'),
marketing_products: Yup.array()
.of(SalesOrderProductSchema)
.min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!'),
});
export type MarketingFormValues = Yup.InferType<typeof MarketingSchema>; export const UpdateSalesOrderSchema = SalesOrderSchema;
export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
@@ -12,10 +12,10 @@ import TextArea from '@/components/input/TextArea';
import Modal, { useModal } from '@/components/Modal'; import Modal, { useModal } from '@/components/Modal';
import * as TanStack from '@tanstack/react-table'; import * as TanStack from '@tanstack/react-table';
import Table from '@/components/Table'; // Keep this import import Table from '@/components/Table'; // Keep this import
import { cn, formatCurrency, formatNumber } from '@/lib/helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { import {
CreateMarketingPayload, CreateSalesOrderPayload,
CreateMarketingProductPayload, CreateSalesOrderProductPayload,
Marketing, Marketing,
MarketingProduct, MarketingProduct,
} from '@/types/api/marketing/marketing'; } from '@/types/api/marketing/marketing';
@@ -26,10 +26,10 @@ import CheckboxInput from '@/components/input/CheckboxInput';
import { Customer } from '@/types/api/master-data/customer'; import { Customer } from '@/types/api/master-data/customer';
import { CustomerApi } from '@/services/api/master-data'; import { CustomerApi } from '@/services/api/master-data';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { MarketingFormValues, MarketingSchema } from './SalesForm.schema'; import { SalesOrderFormValues, SalesOrderSchema } from './SalesForm.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { MarketingApi } from '@/services/api/marketing/marketing'; 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 ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -37,9 +37,11 @@ import { useRouter } from 'next/navigation';
const SalesForm = ({ const SalesForm = ({
formType = 'add', formType = 'add',
initialValues, initialValues,
afterSubmit,
}: { }: {
formType?: 'add' | 'edit'; formType?: 'add' | 'edit';
initialValues?: Marketing; initialValues?: Marketing;
afterSubmit?: () => void;
}) => { }) => {
const router = useRouter(); const router = useRouter();
const addProductModal = useModal(); const addProductModal = useModal();
@@ -61,11 +63,9 @@ const SalesForm = ({
parseInt(item) parseInt(item)
); );
const [grandTotal, setGrandTotal] = useState<number>( const [grandTotal, setGrandTotal] = useState<number>(
initialValues?.grand_total ?? 0 initialValues?.marketing_products
); ?.map((item) => item.total_price)
const marketingProducts = useMemo( .reduce((a, b) => a + b, 0) ?? 0
() => rawMarketingProducts,
[rawMarketingProducts]
); );
const { const {
@@ -91,8 +91,8 @@ const SalesForm = ({
const handleAddSubmitProduct = useCallback( const handleAddSubmitProduct = useCallback(
async ( async (
tableValue: CreateMarketingProductPayload, tableValue: CreateSalesOrderProductPayload,
fieldValues: MarketingProductFormValues fieldValues: SalesOrderProductFormValues
) => { ) => {
const newMarketingProduct: MarketingProduct = { const newMarketingProduct: MarketingProduct = {
id: rawMarketingProducts.length + 1, id: rawMarketingProducts.length + 1,
@@ -102,10 +102,10 @@ const SalesForm = ({
qty: tableValue.qty as number, qty: tableValue.qty as number,
avg_weight: tableValue.avg_weight as number, avg_weight: tableValue.avg_weight as number,
total_price: tableValue.total_price as number, total_price: tableValue.total_price as number,
marketing_delivery_products: { delivery_product: {
id: rawMarketingProducts.length + 1, id: rawMarketingProducts.length + 1,
vehicle_number: tableValue.vehicle_number as string, 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, unit_price: tableValue.unit_price as number,
total_weight: tableValue.total_weight as number, total_weight: tableValue.total_weight as number,
qty: tableValue.qty as number, qty: tableValue.qty as number,
@@ -133,32 +133,30 @@ const SalesForm = ({
[selectedCustomer, setSelectedCustomer] [selectedCustomer, setSelectedCustomer]
); );
const createMarketingHandler = async (values: CreateMarketingPayload) => { const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
console.log(values); console.log(values);
const createMarketingRes = await MarketingApi.create(values); const createMarketingRes = await MarketingApi.create(values);
if (isResponseSuccess(createMarketingRes)) { if (isResponseSuccess(createMarketingRes)) {
console.log(createMarketingRes); toast.success(createMarketingRes?.message as string);
router.push('/marketing/sales-orders');
} }
if (isResponseError(createMarketingRes)) { 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); console.log(values);
const createMarketingRes = await MarketingApi.update( const updateMarketingRes = await MarketingApi.update(
initialValues?.id as number, initialValues?.id as number,
values values
); );
if (isResponseSuccess(createMarketingRes)) { if (isResponseSuccess(updateMarketingRes)) {
console.log(createMarketingRes); toast.success(updateMarketingRes?.message as string);
router.push('/marketing/sales-orders');
} }
if (isResponseError(createMarketingRes)) { if (isResponseError(updateMarketingRes)) {
console.log(createMarketingRes); toast.error(updateMarketingRes?.message as string);
} }
toast.success('Successfully updated Sales Order!');
router.push('/marketing/sales-orders');
}; };
const deleteMarketingHandler = async () => { const deleteMarketingHandler = async () => {
setIsLoading(true); setIsLoading(true);
@@ -180,9 +178,9 @@ const SalesForm = ({
const MarketingProductToFieldValues = ( const MarketingProductToFieldValues = (
product: MarketingProduct product: MarketingProduct
): MarketingProductFormValues => { ): SalesOrderProductFormValues => {
return { return {
vehicle_number: product.marketing_delivery_products?.vehicle_number, vehicle_number: product.delivery_product?.vehicle_number,
kandang_id: product.product_warehouse.warehouse.id, kandang_id: product.product_warehouse.warehouse.id,
kandang: { kandang: {
value: product.product_warehouse.warehouse.id, value: product.product_warehouse.warehouse.id,
@@ -196,19 +194,17 @@ const SalesForm = ({
unit_price: product.unit_price, unit_price: product.unit_price,
total_weight: product.total_weight, total_weight: product.total_weight,
qty: product.qty, qty: product.qty,
uom: product.product_warehouse?.product?.uom?.name,
avg_weight: product.avg_weight, avg_weight: product.avg_weight,
total_price: product.total_price, total_price: product.total_price,
delivery_date: product.marketing_delivery_products?.delivery_date,
}; };
}; };
const formik = useFormik<MarketingFormValues>({ const formikInitialValues = useMemo(() => {
enableReinitialize: true, return {
initialValues: {
so_date: initialValues?.so_date || undefined, so_date: initialValues?.so_date || undefined,
notes: initialValues?.notes || undefined, notes: initialValues?.notes || undefined,
customer_id: initialValues?.customer?.id || undefined, customer_id: initialValues?.customer?.id || undefined,
sales_person_id: initialValues?.sales_person_id || 1,
customer: { customer: {
value: initialValues?.customer?.id as number, value: initialValues?.customer?.id as number,
label: initialValues?.customer?.name as string, label: initialValues?.customer?.name as string,
@@ -217,15 +213,33 @@ const SalesForm = ({
initialValues?.marketing_products?.map((product) => initialValues?.marketing_products?.map((product) =>
MarketingProductToFieldValues(product) MarketingProductToFieldValues(product)
) ?? [], ) ?? [],
}, };
validationSchema: MarketingSchema, }, [initialValues]);
const formik = useFormik<SalesOrderFormValues>({
enableReinitialize: true,
initialValues: formikInitialValues,
validationSchema: SalesOrderSchema,
validateOnMount: true,
onSubmit: async (values) => { onSubmit: async (values) => {
const payload = { const payload = {
customer_id: values.customer_id as number, 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, notes: values.notes as string,
marketing_products: values.marketing_products, marketing_products: values.marketing_products.map((product) => {
} as CreateMarketingPayload; 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) { switch (formType) {
case 'add': case 'add':
createMarketingHandler(payload); createMarketingHandler(payload);
@@ -236,6 +250,7 @@ const SalesForm = ({
default: default:
break; break;
} }
afterSubmit?.();
}, },
}); });
@@ -245,6 +260,33 @@ const SalesForm = ({
formikSetValues(formik.initialValues); formikSetValues(formik.initialValues);
}, [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( const columns = useMemo(
() => [ () => [
{ {
@@ -273,7 +315,7 @@ const SalesForm = ({
}, },
{ {
accessorFn: (row: MarketingProduct) => accessorFn: (row: MarketingProduct) =>
row.marketing_delivery_products?.vehicle_number, row.delivery_product?.vehicle_number,
header: 'No. Polisi', header: 'No. Polisi',
}, },
{ {
@@ -321,7 +363,7 @@ const SalesForm = ({
), ),
}, },
], ],
[handleDeleteProduct] // dependensi tunggal [handleDeleteProduct, initialValues, rawMarketingProducts] // dependensi tunggal
); );
return ( return (
@@ -372,10 +414,15 @@ const SalesForm = ({
wrapper: 'bg-white w-full', 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> <Table<MarketingProduct>
rowSelection={rowSelection} rowSelection={rowSelection}
setRowSelection={setRowSelection} setRowSelection={setRowSelection}
data={marketingProducts} data={rawMarketingProducts}
columns={columns} columns={columns}
className={{ className={{
tableWrapperClassName: 'overflow-x-auto min-h-full!', tableWrapperClassName: 'overflow-x-auto min-h-full!',
@@ -15,13 +15,13 @@ type MarketingProductSchemaType = {
unit_price: string | number | undefined; unit_price: string | number | undefined;
total_weight: string | number | undefined; total_weight: string | number | undefined;
qty: string | number | undefined; qty: string | number | undefined;
uom: string | undefined | null;
avg_weight: string | number | undefined; avg_weight: string | number | undefined;
total_price: 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({ Yup.object({
vehicle_number: Yup.string().required('No. Polisi wajib diisi!'), vehicle_number: Yup.string().required('No. Polisi wajib diisi!'),
kandang: Yup.object({ kandang: Yup.object({
@@ -47,16 +47,17 @@ export const MarketingProductSchema: Yup.ObjectSchema<MarketingProductSchemaType
qty: Yup.number() qty: Yup.number()
.min(1, 'Kuantitas wajib diisi!') .min(1, 'Kuantitas wajib diisi!')
.required('Kuantitas wajib diisi!'), .required('Kuantitas wajib diisi!'),
uom: Yup.string().nullable(),
avg_weight: Yup.number() avg_weight: Yup.number()
.min(1, 'Avg. Bobot wajib diisi!') .min(1, 'Avg. Bobot wajib diisi!')
.required('Avg. Bobot wajib diisi!'), .required('Avg. Bobot wajib diisi!'),
total_price: Yup.number() total_price: Yup.number()
.min(1, 'Total Penjualan wajib diisi!') .min(1, 'Total Penjualan wajib diisi!')
.required('Total Penjualan wajib diisi!'), .required('Total Penjualan wajib diisi!'),
delivery_date: Yup.string().required().nullable(),
}); });
export type MarketingProductFormValues = Yup.InferType< export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
typeof MarketingProductSchema MarketingProductSchema;
export type SalesOrderProductFormValues = Yup.InferType<
typeof SalesOrderProductSchema
>; >;
@@ -1,17 +1,15 @@
'use client'; 'use client';
import TextInput from '@/components/input/TextInput';
import { import {
CreateMarketingPayload, CreateSalesOrderProductPayload,
CreateMarketingProductPayload,
MarketingProduct, MarketingProduct,
} from '@/types/api/marketing/marketing'; } from '@/types/api/marketing/marketing';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { import {
MarketingProductFormValues, SalesOrderProductFormValues,
MarketingProductSchema, SalesOrderProductSchema,
} from './MarketingProduct.schema'; } from './MarketingProduct.schema';
import { RefObject, use, useEffect, useRef, useState } from 'react'; import { RefObject, useEffect, useState } from 'react';
import SelectInput, { import SelectInput, {
OptionType, OptionType,
useSelect, useSelect,
@@ -36,8 +34,8 @@ const MarketingProductForm = ({
data: MarketingProduct[]; data: MarketingProduct[];
modalRef?: RefObject<HTMLDialogElement | null>; modalRef?: RefObject<HTMLDialogElement | null>;
onSubmitForm?: ( onSubmitForm?: (
tableValues: CreateMarketingProductPayload, tableValues: CreateSalesOrderProductPayload,
fieldValues: MarketingProductFormValues fieldValues: SalesOrderProductFormValues
) => Promise<void>; ) => Promise<void>;
}) => { }) => {
// State // State
@@ -96,11 +94,11 @@ const MarketingProductForm = ({
}; };
// Formik // Formik
const formik = useFormik<MarketingProductFormValues>({ const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
vehicle_number: vehicle_number:
initialValues?.marketing_delivery_products?.vehicle_number || undefined, initialValues?.delivery_product?.vehicle_number || undefined,
kandang_id: initialValues?.product_warehouse.warehouse.id || undefined, kandang_id: initialValues?.product_warehouse.warehouse.id || undefined,
kandang: { kandang: {
value: initialValues?.product_warehouse.warehouse.id as number, value: initialValues?.product_warehouse.warehouse.id as number,
@@ -115,15 +113,10 @@ const MarketingProductForm = ({
unit_price: initialValues?.unit_price || undefined, unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined, total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined, qty: initialValues?.qty || undefined,
uom: initialValues?.product_warehouse?.product?.uom?.name || undefined,
avg_weight: initialValues?.avg_weight || undefined, avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || 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) => { onSubmit: async (values) => {
setFormErrorMessage(''); setFormErrorMessage('');
if ( if (
@@ -137,7 +130,7 @@ const MarketingProductForm = ({
(item: Kandang) => item.id === values.kandang_id (item: Kandang) => item.id === values.kandang_id
); );
const marketingProduct: CreateMarketingProductPayload = { const marketingProduct: CreateSalesOrderProductPayload = {
id: initialValues?.id || undefined, id: initialValues?.id || undefined,
vehicle_number: formatVechicleNumber(values.vehicle_number as string), vehicle_number: formatVechicleNumber(values.vehicle_number as string),
kandang_id: values.kandang_id as number, kandang_id: values.kandang_id as number,
@@ -147,10 +140,8 @@ const MarketingProductForm = ({
unit_price: values.unit_price as number, unit_price: values.unit_price as number,
total_weight: values.total_weight as number, total_weight: values.total_weight as number,
qty: values.qty as number, qty: values.qty as number,
uom: values.uom as string,
avg_weight: values.avg_weight as number, avg_weight: values.avg_weight as number,
total_price: values.total_price as number, total_price: values.total_price as number,
delivery_date: values.delivery_date as string,
}; };
onSubmitForm?.(marketingProduct, values); onSubmitForm?.(marketingProduct, values);
@@ -178,10 +169,8 @@ const MarketingProductForm = ({
unit_price: '', unit_price: '',
total_weight: '', total_weight: '',
qty: '', qty: '',
uom: '',
avg_weight: '', avg_weight: '',
total_price: '', total_price: '',
delivery_date: new Date().toDateString(),
}, },
}); });
}; };
+15
View File
@@ -32,3 +32,18 @@ export const TRANSFER_TO_LAYING_APPROVAL_LINE: ApprovalLine = [
step_name: 'Disetujui', step_name: 'Disetujui',
}, },
] as const; ] 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',
},
];
+10 -16
View File
@@ -176,8 +176,7 @@ export const dummyMarketings: Marketing[] = [
{ {
id: 1, id: 1,
status: 'APPROVED', status: 'APPROVED',
so_number: 'SO-001-2025', name: 'SO-001-2025',
so_docs: 'https://example.com/docs/so001.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'), so_date: format(new Date(), 'yyyy-MM-dd'),
customer: { customer: {
id: 1, id: 1,
@@ -193,9 +192,8 @@ export const dummyMarketings: Marketing[] = [
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_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.', notes: 'Pengiriman awal bulan.',
grand_total: 7500000,
approval: { approval: {
step_number: 1, step_number: 1,
step_name: 'Pengajuan Order', step_name: 'Pengajuan Order',
@@ -212,7 +210,7 @@ export const dummyMarketings: Marketing[] = [
total_weight: 250, total_weight: 250,
total_price: 7500000, total_price: 7500000,
product_warehouse: dummyProductWarehouses[0], product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: { delivery_product: {
id: 1, id: 1,
qty: 100, qty: 100,
unit_price: 75000, unit_price: 75000,
@@ -233,8 +231,7 @@ export const dummyMarketings: Marketing[] = [
{ {
id: 2, id: 2,
status: 'APPROVED', status: 'APPROVED',
so_number: 'SO-002-2025', name: 'SO-002-2025',
so_docs: 'https://example.com/docs/so002.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'), so_date: format(new Date(), 'yyyy-MM-dd'),
customer: { customer: {
id: 2, id: 2,
@@ -250,9 +247,8 @@ export const dummyMarketings: Marketing[] = [
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_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.', notes: 'Pesanan kedua untuk stok akhir tahun.',
grand_total: 3750000,
approval: { approval: {
step_number: 2, step_number: 2,
step_name: 'Sales Order', step_name: 'Sales Order',
@@ -269,7 +265,7 @@ export const dummyMarketings: Marketing[] = [
total_weight: 125, total_weight: 125,
total_price: 3750000, total_price: 3750000,
product_warehouse: dummyProductWarehouses[1], product_warehouse: dummyProductWarehouses[1],
marketing_delivery_products: { delivery_product: {
id: 2, id: 2,
qty: 50, qty: 50,
unit_price: 75000, unit_price: 75000,
@@ -290,8 +286,7 @@ export const dummyMarketings: Marketing[] = [
{ {
id: 3, id: 3,
status: 'APPROVED', status: 'APPROVED',
so_number: 'SO-003-2025', name: 'SO-003-2025',
so_docs: 'https://example.com/docs/so003.pdf',
so_date: format(new Date(), 'yyyy-MM-dd'), so_date: format(new Date(), 'yyyy-MM-dd'),
customer: { customer: {
id: 3, id: 3,
@@ -307,9 +302,8 @@ export const dummyMarketings: Marketing[] = [
created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), created_at: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
updated_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.', notes: 'Order untuk pengiriman ke luar kota.',
grand_total: 5600000,
approval: { approval: {
step_number: 3, step_number: 3,
step_name: 'Delivery Order', step_name: 'Delivery Order',
@@ -326,7 +320,7 @@ export const dummyMarketings: Marketing[] = [
total_weight: 192, total_weight: 192,
total_price: 5600000, total_price: 5600000,
product_warehouse: dummyProductWarehouses[0], product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: { delivery_product: {
id: 3, id: 3,
qty: 80, qty: 80,
unit_price: 70000, unit_price: 70000,
@@ -345,7 +339,7 @@ export const dummyMarketings: Marketing[] = [
total_weight: 192, total_weight: 192,
total_price: 5600000, total_price: 5600000,
product_warehouse: dummyProductWarehouses[0], product_warehouse: dummyProductWarehouses[0],
marketing_delivery_products: { delivery_product: {
id: 3, id: 3,
qty: 80, qty: 80,
unit_price: 70000, unit_price: 70000,
+1 -1
View File
@@ -32,7 +32,7 @@ export const formatNumber = (
export function formatVechicleNumber(value: string): string { export function formatVechicleNumber(value: string): string {
let result = ''; let result = '';
for (let i = 0; i < value.length; i++) { for (let i = 0; i < (value?.length ?? 0); i++) {
const curr = value[i]; const curr = value[i];
const prev = value[i - 1]; const prev = value[i - 1];
+9 -66
View File
@@ -5,82 +5,25 @@ import { httpClient } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import { BaseApiResponse } from '@/types/api/api-general';
import { import {
Marketing, Marketing,
CreateMarketingPayload, CreateSalesOrderPayload,
UpdateMarketingPayload, UpdateSalesOrderPayload,
} from '@/types/api/marketing/marketing'; } from '@/types/api/marketing/marketing';
export class MarketingService extends BaseApiService< export class MarketingService extends BaseApiService<
Marketing, Marketing,
CreateMarketingPayload, CreateSalesOrderPayload,
UpdateMarketingPayload UpdateSalesOrderPayload
> { > {
constructor(basePath: string = '/marketing') { constructor(basePath: string = '/marketing') {
super(basePath); 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 * Approve single marketing data
*/ */
async singleApproval( async singleApproval(
id: number, id: number,
action: 'approve' | 'reject' action: 'APPROVED' | 'REJECTED'
): Promise<BaseApiResponse<{ message: string }> | undefined> { ): Promise<BaseApiResponse<{ message: string }> | undefined> {
try { try {
const path = `${this.basePath}/approvals`; const path = `${this.basePath}/approvals`;
@@ -88,7 +31,7 @@ export class MarketingService extends BaseApiService<
method: 'POST', method: 'POST',
body: { body: {
action: action, action: action,
approval_ids: [id], approvable_ids: [id],
notes: `${action} marketing ${id}`, notes: `${action} marketing ${id}`,
}, },
}); });
@@ -103,7 +46,7 @@ export class MarketingService extends BaseApiService<
*/ */
async bulkApprovals( async bulkApprovals(
ids: number[], ids: number[],
action: 'approve' | 'reject' action: 'APPROVED' | 'REJECTED'
): Promise<BaseApiResponse<{ message: string }> | undefined> { ): Promise<BaseApiResponse<{ message: string }> | undefined> {
try { try {
const path = `${this.basePath}/approvals`; const path = `${this.basePath}/approvals`;
@@ -111,7 +54,7 @@ export class MarketingService extends BaseApiService<
method: 'POST', method: 'POST',
body: { body: {
action: action, action: action,
approval_ids: ids, approvable_ids: ids,
notes: `${action} marketing ${ids.join(', ')}`, 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
View File
@@ -7,16 +7,17 @@ import {
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Kandang } from '@/types/api/master-data/kandang'; import { Kandang } from '@/types/api/master-data/kandang';
/**
* Base Data Response
*/
export type BaseMarketing = { export type BaseMarketing = {
id: number; id: number;
status?: string; status?: string;
so_number: string; name: string;
customer: Customer; customer: Customer;
so_docs: string;
so_date: string; so_date: string;
sales_person: CreatedUser; sales_person_id: number;
notes: string; notes: string;
grand_total: number;
approval: BaseApproval; approval: BaseApproval;
marketing_products?: MarketingProduct[]; marketing_products?: MarketingProduct[];
}; };
@@ -29,7 +30,7 @@ export type MarketingProduct = {
total_weight: number; total_weight: number;
total_price: number; total_price: number;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
marketing_delivery_products?: MarketingDeliveryProducts; delivery_product?: MarketingDeliveryProducts;
}; };
export type MarketingDeliveryProducts = { export type MarketingDeliveryProducts = {
@@ -39,34 +40,49 @@ export type MarketingDeliveryProducts = {
avg_weight: number; avg_weight: number;
total_weight: number; total_weight: number;
total_price: number; total_price: number;
delivery_date: string; delivery_date: string | null;
vehicle_number: string; vehicle_number: string;
do_number?: string | undefined; do_number?: string | undefined; // Uncertain
}; };
export type Marketing = BaseMetadata & BaseMarketing; export type Marketing = BaseMetadata & BaseMarketing;
export type CreateMarketingPayload = { /**
* Base Data Payload
*/
export type BaseCreateMarketingPayload = {
customer_id: number; customer_id: number;
sales_person_id: number;
date: string; date: string;
notes: string; notes: string;
marketing_products: CreateMarketingProductPayload[];
}; };
export type UpdateMarketingPayload = CreateMarketingPayload;
export type CreateMarketingProductPayload = { export type BaseCreateMarketingProductPayload = {
id?: number;
vehicle_number: string; vehicle_number: string;
kandang_id: string | number | undefined; kandang_id: string | number | undefined;
kandang: Kandang | undefined;
product_warehouse_id: string | number | undefined; product_warehouse_id: string | number | undefined;
product_warehouse: ProductWarehouse | undefined;
unit_price: string | number | undefined; unit_price: string | number | undefined;
total_weight: string | number | undefined; total_weight: string | number | undefined;
qty: string | number | undefined; qty: string | number | undefined;
uom: string | undefined;
avg_weight: string | number | undefined; avg_weight: string | number | undefined;
total_price: 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;