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 { 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,23 +17,28 @@ type MarketingSchema = {
| null;
so_date: string | undefined;
notes: string | undefined;
marketing_products: MarketingProductFormValues[];
};
export const MarketingSchema: Yup.ObjectSchema<MarketingSchema> = Yup.object({
customer_id: Yup.number().required('Customer wajib diisi!'),
customer: Yup.object({
value: Yup.number().required(),
label: Yup.string().required(),
}).nullable(),
so_date: Yup.string().required('Tanggal wajib diisi!'),
notes: Yup.string().required('Catatan wajib diisi!'),
marketing_products: Yup.array()
.of(MarketingProductSchema)
.min(1, 'Minimal harus ada 1 produk!')
.required('Produk wajib diisi!'),
});
type SalesOrderSchemaType = MarketingSchemaType & {
marketing_products: SalesOrderProductFormValues[];
};
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 * 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 (isResponseSuccess(updateMarketingRes)) {
toast.success(updateMarketingRes?.message as string);
router.push('/marketing/sales-orders');
}
if (isResponseError(createMarketingRes)) {
console.log(createMarketingRes);
if (isResponseError(updateMarketingRes)) {
toast.error(updateMarketingRes?.message as string);
}
toast.success('Successfully updated Sales Order!');
router.push('/marketing/sales-orders');
};
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!',
@@ -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
>;
@@ -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(),
},
});
};
+15
View File
@@ -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',
},
];
+10 -16
View File
@@ -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
View File
@@ -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];
+9 -66
View File
@@ -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
View File
@@ -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;