diff --git a/src/app/marketing/add/delivery-orders/layout.tsx b/src/app/marketing/add/delivery-orders/layout.tsx
deleted file mode 100644
index 7220dfa1..00000000
--- a/src/app/marketing/add/delivery-orders/layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import SuspenseHelper from '@/components/helper/SuspenseHelper';
-
-const Layout = ({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) => {
- return {children};
-};
-
-export default Layout;
diff --git a/src/app/marketing/add/delivery-orders/page.tsx b/src/app/marketing/add/delivery-orders/page.tsx
deleted file mode 100644
index 4d92acda..00000000
--- a/src/app/marketing/add/delivery-orders/page.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { MarketingApi } from '@/services/api/marketing/marketing';
-import { useRouter, useSearchParams } from 'next/navigation';
-import toast from 'react-hot-toast';
-import useSWR from 'swr';
-
-const EditMarketingDelivery = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const soId = searchParams.get('marketingId');
-
- const {
- data: marketing,
- isLoading: isLoading,
- mutate: refreshMarketing,
- } = useSWR(`get-so-${soId}`, () =>
- MarketingApi.getSingle(soId ? parseInt(soId) : 0)
- );
-
- if (!soId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (!isLoading && (!marketing || isResponseError(marketing))) {
- router.replace('/404');
- return;
- }
-
- return (
-
- {isLoading && }
- {!isLoading && isResponseSuccess(marketing) && (
- {
- refreshMarketing();
- }}
- />
- )}
-
- );
-};
-export default EditMarketingDelivery;
diff --git a/src/app/marketing/add/sales-orders/page.tsx b/src/app/marketing/add/sales-orders/page.tsx
deleted file mode 100644
index 9e33d304..00000000
--- a/src/app/marketing/add/sales-orders/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
-
-const AddSalesOrder = () => {
- return (
-
-
-
- );
-};
-
-export default AddSalesOrder;
diff --git a/src/app/marketing/detail/delivery-orders/edit/layout.tsx b/src/app/marketing/detail/delivery-orders/edit/layout.tsx
deleted file mode 100644
index 7220dfa1..00000000
--- a/src/app/marketing/detail/delivery-orders/edit/layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import SuspenseHelper from '@/components/helper/SuspenseHelper';
-
-const Layout = ({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) => {
- return {children};
-};
-
-export default Layout;
diff --git a/src/app/marketing/detail/delivery-orders/edit/page.tsx b/src/app/marketing/detail/delivery-orders/edit/page.tsx
deleted file mode 100644
index 32625026..00000000
--- a/src/app/marketing/detail/delivery-orders/edit/page.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-'use client';
-
-import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { MarketingApi } from '@/services/api/marketing/marketing';
-import { useRouter, useSearchParams } from 'next/navigation';
-import toast from 'react-hot-toast';
-import useSWR from 'swr';
-
-const EditMarketingDelivery = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const soId = searchParams.get('marketingId');
-
- const {
- data: marketing,
- isLoading: isLoading,
- mutate: refreshMarketing,
- } = useSWR(`get-so-${soId}`, () =>
- MarketingApi.getSingle(soId ? parseInt(soId) : 0)
- );
-
- if (!soId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (!isLoading && (!marketing || isResponseError(marketing))) {
- router.replace('/404');
- return;
- }
-
- if (
- isResponseSuccess(marketing) &&
- marketing.data.latest_approval.step_number != 3
- ) {
- toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!');
- router.back();
- }
-
- return (
-
- {isLoading && }
- {!isLoading && isResponseSuccess(marketing) && (
- {
- refreshMarketing();
- }}
- />
- )}
-
- );
-};
-export default EditMarketingDelivery;
diff --git a/src/app/marketing/detail/layout.tsx b/src/app/marketing/detail/layout.tsx
deleted file mode 100644
index 7220dfa1..00000000
--- a/src/app/marketing/detail/layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import SuspenseHelper from '@/components/helper/SuspenseHelper';
-
-const Layout = ({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) => {
- return {children};
-};
-
-export default Layout;
diff --git a/src/app/marketing/detail/page.tsx b/src/app/marketing/detail/page.tsx
deleted file mode 100644
index 902251e8..00000000
--- a/src/app/marketing/detail/page.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { MarketingApi } from '@/services/api/marketing/marketing';
-import { useRouter, useSearchParams } from 'next/navigation';
-import useSWR from 'swr';
-
-const DetailMarketing = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const soId = searchParams.get('marketingId');
-
- const {
- data: marketing,
- isLoading: isLoading,
- mutate: refreshMarketing,
- } = useSWR(soId, (id: number) => MarketingApi.getSingle(id));
-
- if (!soId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (!isLoading && (!marketing || isResponseError(marketing))) {
- router.replace('/404');
- return;
- }
-
- return (
-
- {isLoading && }
- {!isLoading && isResponseSuccess(marketing) && (
-
- )}
-
- );
-};
-
-export default DetailMarketing;
diff --git a/src/app/marketing/detail/sales-orders/edit/layout.tsx b/src/app/marketing/detail/sales-orders/edit/layout.tsx
deleted file mode 100644
index 7220dfa1..00000000
--- a/src/app/marketing/detail/sales-orders/edit/layout.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import SuspenseHelper from '@/components/helper/SuspenseHelper';
-
-const Layout = ({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) => {
- return {children};
-};
-
-export default Layout;
diff --git a/src/app/marketing/detail/sales-orders/edit/page.tsx b/src/app/marketing/detail/sales-orders/edit/page.tsx
deleted file mode 100644
index 19a098c5..00000000
--- a/src/app/marketing/detail/sales-orders/edit/page.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-'use client';
-
-import MarketingForm from '@/components/pages/marketing/form/MarketingForm';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import { MarketingApi } from '@/services/api/marketing/marketing';
-import { useRouter, useSearchParams } from 'next/navigation';
-import useSWR from 'swr';
-
-const EditSalesOrder = () => {
- const router = useRouter();
- const searchParams = useSearchParams();
-
- const soId = searchParams.get('marketingId');
-
- const {
- data: marketing,
- isLoading: isLoading,
- mutate: refreshMarketing,
- } = useSWR(`get-so-${soId}`, () =>
- MarketingApi.getSingle(soId ? parseInt(soId) : 0)
- );
-
- if (!soId) {
- router.back();
-
- return (
-
-
-
- );
- }
-
- if (!isLoading && (!marketing || isResponseError(marketing))) {
- router.replace('/404');
- return;
- }
- return (
-
- {isLoading && }
- {!isLoading && isResponseSuccess(marketing) && (
- {
- refreshMarketing();
- }}
- />
- )}
-
- );
-};
-export default EditSalesOrder;
diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx
index eee84224..854b4d34 100644
--- a/src/components/pages/expense/ExpenseStatusBadge.tsx
+++ b/src/components/pages/expense/ExpenseStatusBadge.tsx
@@ -49,7 +49,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => {
color={expenseStatusBadgeColor}
text={isLatestApprovalRejected ? 'Ditolak' : (approval?.step_name ?? '')}
className={{
- badge: 'w-fit',
+ badge: 'whitespace-nowrap max-w-max w-fit',
}}
/>
);
diff --git a/src/components/pages/expense/RealizationStatusBadge.tsx b/src/components/pages/expense/RealizationStatusBadge.tsx
index d04d35c3..eb429473 100644
--- a/src/components/pages/expense/RealizationStatusBadge.tsx
+++ b/src/components/pages/expense/RealizationStatusBadge.tsx
@@ -29,7 +29,7 @@ const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => {
color={realizationStatusBadgeColor}
text={isLatestApprovalRejected ? 'Ditolak' : realizationStatus}
className={{
- badge: 'w-fit',
+ badge: 'whitespace-nowrap max-w-max w-fit',
}}
/>
);
diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx
index c1c7f079..6f422753 100644
--- a/src/components/pages/finance/FinanceTable.tsx
+++ b/src/components/pages/finance/FinanceTable.tsx
@@ -1,13 +1,8 @@
-import {
- ChangeEventHandler,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
import { CellContext } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
+import { useFormik } from 'formik';
import Button from '@/components/Button';
import Card from '@/components/Card';
@@ -40,6 +35,10 @@ import { Icon } from '@iconify/react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { useUiStore } from '@/stores/ui/ui.store';
+import {
+ FinanceTableFilterSchema,
+ FinanceTableFilterValues,
+} from './FinanceTableFilter.schema';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -152,10 +151,10 @@ const FinanceTable = () => {
} = useTableFilter({
initial: {
search: searchValue,
- transactionType: '',
- bankId: '',
- customerId: '',
- supplierId: '',
+ transactionTypes: '',
+ bankIds: '',
+ customerIds: '',
+ supplierIds: '',
sortBy: '',
startDate: '',
endDate: '',
@@ -163,10 +162,10 @@ const FinanceTable = () => {
paramMap: {
page: 'page',
pageSize: 'limit',
- transactionType: 'transaction_type',
- bankId: 'bank_id',
- customerId: 'customer_id',
- supplierId: 'supplier_id',
+ transactionTypes: 'transaction_types',
+ bankIds: 'bank_ids',
+ customerIds: 'customer_ids',
+ supplierIds: 'supplier_ids',
sortBy: 'sort_date',
startDate: 'start_date',
endDate: 'end_date',
@@ -174,18 +173,7 @@ const FinanceTable = () => {
});
// ===== State =====
- const [searchParams, setSearchParams] = useSearchParams();
const deleteModal = useModal();
- const [pendingFilters, setPendingFilters] = useState({
- search: searchValue,
- transactionType: '',
- bankId: '',
- customerId: '',
- supplierId: '',
- sortBy: '',
- startDate: '',
- endDate: '',
- });
const [selectedTransactionType, setSelectedTransactionType] = useState<
OptionType | OptionType[] | null
>(null);
@@ -201,6 +189,34 @@ const FinanceTable = () => {
const [selectedSortBy, setSelectedSortBy] = useState(null);
const [selectedFinance, setSelectedFinance] = useState(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [dateErrorShown, setDateErrorShown] = useState(false);
+
+ // ===== Formik for Filter =====
+ const filterFormik = useFormik({
+ initialValues: {
+ search: searchValue,
+ transaction_types: '',
+ bank_ids: '',
+ customer_ids: '',
+ supplier_ids: '',
+ sort_by: '',
+ start_date: '',
+ end_date: '',
+ },
+ validationSchema: FinanceTableFilterSchema,
+ enableReinitialize: true,
+ onSubmit: (values) => {
+ updateFilter('search', values.search);
+ setSearchValue(values.search);
+ updateFilter('transactionTypes', values.transaction_types);
+ updateFilter('bankIds', values.bank_ids);
+ updateFilter('customerIds', values.customer_ids);
+ updateFilter('supplierIds', values.supplier_ids);
+ updateFilter('sortBy', values.sort_by);
+ updateFilter('startDate', values.start_date);
+ updateFilter('endDate', values.end_date);
+ },
+ });
// ===== Fetch Data =====
const {
@@ -237,84 +253,141 @@ const FinanceTable = () => {
loadMore: bankLoadMore,
} = useSelect(BankApi.basePath, 'id', 'alias');
+ const bankSelectOptions = useMemo(() => {
+ if (!isResponseSuccess(bankRawData)) return [];
+
+ return bankOptions.map((bank) => {
+ const bankData = bankRawData.data.find((data) => data.id === bank?.value);
+ return {
+ label: bankData
+ ? `${bankData.alias} - ${bankData.account_number} - ${bankData.owner}`
+ : '',
+ value: bank?.value,
+ };
+ });
+ }, [bankOptions, bankRawData]);
+
// ===== Handler =====
- const searchChangeHandler: ChangeEventHandler = (e) => {
- setPendingFilters((prev) => ({ ...prev, search: e.target.value }));
+ const searchChangeHandler = (e: React.ChangeEvent) => {
+ filterFormik.setFieldValue('search', e.target.value);
};
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedTransactionType(val);
- setPendingFilters((prev) => ({
- ...prev,
- transactionType: val
+ filterFormik.setFieldValue(
+ 'transaction_types',
+ val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
- : '',
- }));
+ : ''
+ );
};
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val);
- setPendingFilters((prev) => ({
- ...prev,
- bankId: val
+ filterFormik.setFieldValue(
+ 'bank_ids',
+ val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
- : '',
- }));
+ : ''
+ );
};
const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedCustomerId(val);
- setPendingFilters((prev) => ({
- ...prev,
- customerId: val
+ filterFormik.setFieldValue(
+ 'customer_ids',
+ val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
- : '',
- }));
+ : ''
+ );
};
const supplierIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSupplierId(val);
- setPendingFilters((prev) => ({
- ...prev,
- supplierId: val
+ filterFormik.setFieldValue(
+ 'supplier_ids',
+ val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
- : '',
- }));
+ : ''
+ );
};
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSortBy(val as OptionType);
- setPendingFilters((prev) => ({
- ...prev,
- sortBy: val ? ((val as OptionType).value as string) : '',
- }));
+ filterFormik.setFieldValue(
+ 'sort_by',
+ val ? ((val as OptionType).value as string) : ''
+ );
};
- const startDateChangeHandler: ChangeEventHandler = (e) => {
- setPendingFilters((prev) => ({ ...prev, startDate: e.target.value }));
+
+ const startDateChangeHandler = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ const endDate = filterFormik.values.end_date;
+
+ filterFormik.setFieldValue('start_date', value);
+
+ if (value && endDate) {
+ const startDate = new Date(value);
+ const endDateObj = new Date(endDate);
+
+ if (endDateObj < startDate) {
+ filterFormik.setFieldError(
+ 'end_date',
+ 'Tanggal akhir tidak boleh masa lampau'
+ );
+ if (!dateErrorShown) {
+ toast.error('Tanggal akhir tidak boleh masa lampau', {
+ duration: Infinity,
+ });
+ setDateErrorShown(true);
+ }
+ } else {
+ filterFormik.setFieldError('end_date', undefined);
+ if (dateErrorShown) {
+ toast.dismiss();
+ setDateErrorShown(false);
+ }
+ }
+ }
};
- const endDateChangeHandler: ChangeEventHandler = (e) => {
- setPendingFilters((prev) => ({ ...prev, endDate: e.target.value }));
- };
- const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
- const newVal = val as OptionType;
- setPageSize(newVal.value as number);
- };
- const submitFilterHandler = () => {
- updateFilter('search', pendingFilters.search);
- setSearchValue(pendingFilters.search);
- updateFilter('transactionType', pendingFilters.transactionType);
- updateFilter('bankId', pendingFilters.bankId);
- updateFilter('customerId', pendingFilters.customerId);
- updateFilter('supplierId', pendingFilters.supplierId);
- updateFilter('sortBy', pendingFilters.sortBy);
- updateFilter('startDate', pendingFilters.startDate);
- updateFilter('endDate', pendingFilters.endDate);
+
+ const endDateChangeHandler = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ const startDate = filterFormik.values.start_date;
+
+ filterFormik.setFieldValue('end_date', value);
+
+ if (value && startDate) {
+ const startDateObj = new Date(startDate);
+ const endDate = new Date(value);
+
+ if (endDate < startDateObj) {
+ filterFormik.setFieldError(
+ 'end_date',
+ 'Tanggal akhir tidak boleh masa lampau'
+ );
+ if (!dateErrorShown) {
+ toast.error('Tanggal akhir tidak boleh masa lampau', {
+ duration: Infinity,
+ });
+ setDateErrorShown(true);
+ }
+ return;
+ }
+ }
+
+ filterFormik.setFieldError('end_date', undefined);
+ if (dateErrorShown) {
+ toast.dismiss();
+ setDateErrorShown(false);
+ }
};
+
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
@@ -322,24 +395,14 @@ const FinanceTable = () => {
setSelectedSupplierId(null);
setSelectedSortBy(null);
- const emptyFilters = {
- search: '',
- transactionType: '',
- bankId: '',
- customerId: '',
- supplierId: '',
- sortBy: '',
- startDate: '',
- endDate: '',
- };
- setPendingFilters(emptyFilters);
+ filterFormik.resetForm();
updateFilter('search', '');
resetSearchValue();
- updateFilter('transactionType', '');
- updateFilter('bankId', '');
- updateFilter('customerId', '');
- updateFilter('supplierId', '');
+ updateFilter('transactionTypes', '');
+ updateFilter('bankIds', '');
+ updateFilter('customerIds', '');
+ updateFilter('supplierIds', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
@@ -464,26 +527,36 @@ const FinanceTable = () => {
}, []);
useEffect(() => {
- // Store current path on mount
+ return () => {
+ if (dateErrorShown) {
+ toast.dismiss();
+ }
+ };
+ }, [dateErrorShown]);
+
+ useEffect(() => {
previousPathRef.current = window.location.pathname;
return () => {
const currentPath = window.location.pathname;
- // if both paths are within /finance module
const isCurrentPathFinance = currentPath.includes('/finance');
const isPreviousPathFinance =
previousPathRef.current?.includes('/finance');
- // reset if we outside finance module entirely
if (isPreviousPathFinance && !isCurrentPathFinance) {
resetSearchValue();
}
+
+ if (dateErrorShown) {
+ toast.dismiss();
+ setDateErrorShown(false);
+ }
};
- }, [resetSearchValue]);
+ }, [resetSearchValue, dateErrorShown]);
return (
-
+
diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/FinanceTableFilter.schema.ts
new file mode 100644
index 00000000..fecfc35d
--- /dev/null
+++ b/src/components/pages/finance/FinanceTableFilter.schema.ts
@@ -0,0 +1,38 @@
+import * as yup from 'yup';
+
+export type FinanceTableFilterType = {
+ search: string;
+ transaction_types: string;
+ bank_ids: string;
+ customer_ids: string;
+ supplier_ids: string;
+ sort_by: string;
+ start_date: string;
+ end_date: string;
+};
+
+export const FinanceTableFilterSchema = yup.object({
+ search: yup.string().optional(),
+ transaction_types: yup.string().optional(),
+ bank_ids: yup.string().optional(),
+ customer_ids: yup.string().optional(),
+ supplier_ids: yup.string().optional(),
+ sort_by: yup.string().optional(),
+ start_date: yup.string().optional(),
+ end_date: yup
+ .string()
+ .optional()
+ .test(
+ 'is-greater-than-start',
+ 'Tanggal akhir tidak boleh masa lampau',
+ function (value) {
+ const { start_date } = this.parent;
+ if (!start_date || !value) return true;
+ return new Date(value) >= new Date(start_date);
+ }
+ ),
+}) as yup.ObjectSchema;
+
+export type FinanceTableFilterValues = yup.InferType<
+ typeof FinanceTableFilterSchema
+>;
diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx
index 2cf4ef5c..7c953fe8 100644
--- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx
+++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx
@@ -1,16 +1,10 @@
'use client';
import AlertErrorList from '@/components/helper/form/FormErrors';
-import { useSelect, OptionType } from '@/components/input/SelectInput';
+import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import {
- mergeSOwithDO,
- SalesProductToFieldValues,
- DeliveryProductToFieldValues,
-} from '@/components/pages/marketing/form/MarketingForm';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
-import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import {
@@ -18,16 +12,13 @@ import {
MarketingApi,
SalesOrderApi,
} from '@/services/api/marketing/marketing';
-import { CustomerApi } from '@/services/api/master-data';
-import { UserApi } from '@/services/api/user';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
-import { BaseApproval, CreatedUser } from '@/types/api/api-general';
+import { BaseApproval } from '@/types/api/api-general';
import {
CreateDeliveryOrderPayload,
Marketing,
UpdateDeliveryOrderPayload,
} from '@/types/api/marketing/marketing';
-import { Customer } from '@/types/api/master-data/customer';
import { useFormik } from 'formik';
import { Icon } from '@iconify/react';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -47,6 +38,9 @@ import {
DeliveryOrderSchema,
getFilledMarketingFormInitialValues,
SalesOrderFormValues,
+ mergeSOwithDO,
+ SalesProductToFieldValues,
+ DeliveryProductToFieldValues,
} from '@/components/pages/marketing/form/MarketingForm.schema';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import RequirePermission from '@/components/helper/RequirePermission';
@@ -116,13 +110,6 @@ const DeliveryOrderFormModal = ({
const formRef = useRef(null);
const textareaRef = useRef(null);
- const [grandTotal, setGrandTotal] = useState(
- isResponseSuccess(marketing) &&
- marketing?.data.sales_order
- ?.map((item) => item.total_price)
- .reduce((a, b) => a + b, 0)
- );
-
const [formErrorMessage, setFormErrorMessage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
@@ -505,6 +492,14 @@ const DeliveryOrderFormModal = ({
formik.setFieldValue('delivery_order', deliveryOrderValues);
}, [deliveryOrderValues]);
+ const grandTotal = useMemo(() => {
+ return deliveryOrderValues.reduce(
+ (total, product) =>
+ total + parseFloat((product.total_price as string) || '0'),
+ 0
+ );
+ }, [deliveryOrderValues]);
+
return (
<>
)}
-
-
+
+
{step == 2 && 'Ubah '} Informasi{' '}
{step == 2 ? 'Delivery' : 'Produk'}
{step === 1 && (
-
+
+
+
)}
{step === 2 && (
void;
-}) => {
- const router = useRouter();
- const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
- 'APPROVED'
- );
- const [grandTotal, setGrandTotal] = useState(
- initialValues?.sales_order
- ?.map((item) => item.total_price)
- .reduce((a, b) => a + b, 0)
- );
- const [isLoading, setIsLoading] = useState(false);
-
- const deleteModal = useModal();
- const confirmationModal = useModal();
- const deliveryModal = useModal();
- const {
- approvals,
- isLoading: isLoadingApproval,
- refresh: refreshApproval,
- } = useApprovalSteps({
- latestApproval: initialValues?.latest_approval,
- approvalLines: MARKETING_APPROVAL_LINE,
- moduleName: 'MARKETINGS',
- moduleId: initialValues?.id as number as unknown as string,
- });
-
- const approveClickHandler = () => {
- setApprovalAction('APPROVED');
- confirmationModal.openModal();
- };
-
- const rejectClickHandler = () => {
- setApprovalAction('REJECTED');
- confirmationModal.openModal();
- };
-
- const deleteClickHandler = () => {
- deleteModal.openModal();
- };
-
- const confirmationModalDeleteClickHandler = async () => {
- setIsLoading(true);
- const res = await MarketingApi.delete(initialValues?.id as number);
- deleteModal.closeModal();
- router.push('/marketing');
- toast.success(res?.message as string);
- setIsLoading(false);
- };
-
- const confirmationModalApproveClickHandler = async (notes: string) => {
- setIsLoading(true);
- const res = await SalesOrderApi.singleApproval(
- initialValues?.id as number,
- approvalAction,
- notes
- );
- setIsLoading(false);
- confirmationModal.closeModal();
- toast.success(res?.message as string);
- refresh?.();
- refreshApproval?.();
- };
-
- const confirmationModalDeliveryClickHandler = async (notes: string) => {
- setIsLoading(true);
- const res = await SalesOrderApi.delivery(
- initialValues?.id as number,
- notes
- );
- setIsLoading(false);
- deliveryModal.closeModal();
- toast.success(res?.message as string);
- refresh?.();
- refreshApproval?.();
- router.push(
- `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
- );
- };
-
- const approval = initialValues?.latest_approval;
- const isRejected = approval?.action == 'REJECTED';
- const isApproved = approval?.action == 'APPROVED';
-
- return (
- <>
-
-
2 ? 'Delivery Order' : 'Sales Order'}`}
- backUrl='/marketing'
- />
- {!isLoadingApproval && approvals && (
-
- )}
-
- {initialValues?.latest_approval?.step_number == 1 && (
- <>
-
-
-
- Approve
-
-
-
-
-
-
- Reject
-
-
- >
- )}
- {initialValues?.latest_approval?.step_number != 1 && (
- <>
-
-
-
- {initialValues?.latest_approval?.step_number == 3
- ? 'Edit '
- : 'Tambah '}
- Delivery Order
-
-
- >
- )}
-
-
-
-
-
-
-
- |
- No. Sales Order
- |
- : |
-
- {initialValues?.so_number}
- |
-
- {Number(initialValues?.latest_approval?.step_number) > 2 && (
-
- |
- No. Delivery Order
- |
- : |
-
- {initialValues?.delivery_order
- ?.map((item) => item.do_number)
- .join(', ')}
- |
-
- )}
-
- | Nama Pelanggan |
- : |
- {initialValues?.customer?.name} |
-
-
- | Status |
- : |
-
-
-
- {isRejected
- ? 'Ditolak'
- : formatTitleCase(approval?.step_name || '')}
-
- |
-
-
- | Tanggal Penjualan |
- : |
- {formatDate(initialValues?.so_date, 'DD MMM yyyy')} |
-
-
- | Total Penjualan |
- : |
- {formatCurrency(grandTotal as number)} |
-
-
- | Catatan |
- : |
- {initialValues?.notes ?? '-'} |
-
-
- | Dokumen Penjualan |
- : |
-
-
- |
-
- {Number(initialValues?.latest_approval?.step_number) > 2 && (
-
- | Dokumen Pengiriman |
- : |
-
- {initialValues?.delivery_order?.map((item, index) => (
-
- ))}
- |
-
- )}
-
-
-
-
- {initialValues?.sales_order && (
-
-
- data={initialValues?.sales_order}
- columns={[
- {
- header: 'Kandang',
- accessorFn(row) {
- return row.product_warehouse.warehouse.name;
- },
- },
- {
- header: 'Produk',
- accessorFn(row) {
- return row.product_warehouse.product.name;
- },
- },
- {
- header: 'Harga Satuan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.unit_price);
- },
- },
- {
- header: 'Total Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.total_weight);
- },
- },
- {
- header: 'Kuantitas',
- accessorFn(row) {
- return formatNumber(row.qty);
- },
- },
- {
- header: 'Avg. Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.avg_weight);
- },
- },
- {
- header: 'Total Penjualan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.total_price);
- },
- },
- ]}
- className={{
- containerClassName: cn({
- 'mb-20':
- initialValues?.sales_order &&
- initialValues?.sales_order?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
-
- )}
- {initialValues?.delivery_order && (
-
- {initialValues?.delivery_order.map((delivery, index) => {
- return (
-
-
-
-
- Nomor DO : {delivery.do_number}
-
-
-
- data={delivery.deliveries}
- columns={[
- {
- header: 'Tanggal Pengiriman',
- accessorFn() {
- return formatDate(
- delivery.delivery_date,
- 'DD MMM yyyy'
- );
- },
- },
- {
- header: 'No. Polisi',
- accessorFn(row) {
- return formatVechicleNumber(row.vehicle_number);
- },
- },
- {
- header: 'Kandang',
- accessorFn(row) {
- return row.product_warehouse.warehouse.name;
- },
- },
- {
- header: 'Produk',
- accessorFn(row) {
- return row.product_warehouse.product.name;
- },
- },
- {
- header: 'Harga Satuan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.unit_price);
- },
- },
- {
- header: 'Total Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.total_weight);
- },
- },
- {
- header: 'Kuantitas',
- accessorFn(row) {
- return formatNumber(row.qty);
- },
- },
- {
- header: 'Avg. Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.avg_weight);
- },
- },
- {
- header: 'Total Penjualan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.total_price);
- },
- },
- ]}
- className={{
- containerClassName: cn({
- 'mb-20':
- initialValues?.sales_order &&
- initialValues?.sales_order?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName:
- 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
-
-
-
-
-
- );
- })}
-
- )}
-
- {initialValues?.latest_approval?.step_number != 3 && (
- <>
-
-
-
- Edit
-
-
- >
- )}
-
-
-
- Hapus
-
-
-
-
-
-
-
- >
- );
-};
-
-export default MarketingDetail;
diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx
index e09617aa..0a35a8bc 100644
--- a/src/components/pages/marketing/MarketingTable.tsx
+++ b/src/components/pages/marketing/MarketingTable.tsx
@@ -623,7 +623,10 @@ const MarketingTable = () => {
data={allData}
columns={columns}
pageSize={tableFilterState.pageSize}
- page={tableFilterState.page}
+ page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1}
+ totalItems={
+ isResponseSuccess(marketing) ? marketing?.meta?.total_results : 0
+ }
isLoading={isLoadingMarketing}
className={{
containerClassName: cn('p-3', {
diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx
index 57f1e18c..66acc440 100644
--- a/src/components/pages/marketing/SalesOrderFormModal.tsx
+++ b/src/components/pages/marketing/SalesOrderFormModal.tsx
@@ -14,8 +14,6 @@ import {
DeliveryProductToFieldValues,
mergeSOwithDO,
SalesProductToFieldValues,
-} from '@/components/pages/marketing/form/MarketingForm';
-import {
DeliveryOrderFormValues,
DeliveryOrderSchema,
getFilledMarketingFormInitialValues,
@@ -186,15 +184,31 @@ const SalesOrderFormModal = ({
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
notes: values.notes as string,
marketing_products: values.sales_order.map((product) => {
+ // Workaround untuk TELUR + QTY: kirim "KG" karena BE tidak support "QTY"
+ const convertionUnitValue =
+ product.convertion_unit?.value?.toUpperCase();
+ const normalizedConvertionUnit =
+ product.marketing_type?.value?.toLowerCase() === 'telur'
+ ? convertionUnitValue === 'PETI'
+ ? 'PETI'
+ : 'KG' // termasuk "QTY" dan "KG"
+ : undefined;
+
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),
+ unit_price: parseFloat(String(product.unit_price || 0)),
+ total_weight: parseFloat(String(product.total_weight || 0)),
+ qty: parseFloat(String(product.qty || 0)),
+ avg_weight: parseFloat(String(product.avg_weight || 0)),
+ total_price: parseFloat(String(product.total_price || 0)),
+ marketing_type:
+ product.marketing_type?.value?.toUpperCase() || '',
+ convertion_unit: normalizedConvertionUnit,
+ weight_per_convertion:
+ product.weight_per_convertion ?? undefined,
+ week: product.week?.value ?? undefined,
} as CreateSalesOrderProductPayload;
}),
} as CreateSalesOrderPayload)
@@ -282,6 +296,7 @@ const SalesOrderFormModal = ({
// ================== HANDLER ==================
const nextButtonHandler = () => {
+ setSelectedMarketingProduct(null);
setStep(step + 1);
};
const prevButtonHandler = () => {
@@ -375,6 +390,7 @@ const SalesOrderFormModal = ({
}
formik.setFieldValue('sales_order', updatedProducts);
+ console.log(formik.values);
nextButtonHandler();
},
[memoSalesOrder, nextButtonHandler]
@@ -650,8 +666,9 @@ const SalesOrderFormModal = ({
-
+
void;
-}) => {
- const router = useRouter();
- const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
- 'APPROVED'
- );
- const [grandTotal, setGrandTotal] = useState(
- initialValues?.sales_order
- ?.map((item) => item.total_price)
- .reduce((a, b) => a + b, 0)
- );
- const [isLoading, setIsLoading] = useState(false);
-
- const deleteModal = useModal();
- const confirmationModal = useModal();
- const deliveryModal = useModal();
- const {
- approvals,
- isLoading: isLoadingApproval,
- refresh: refreshApproval,
- } = useApprovalSteps({
- latestApproval: initialValues?.latest_approval,
- approvalLines: MARKETING_APPROVAL_LINE,
- moduleName: 'MARKETINGS',
- moduleId: initialValues?.id as number as unknown as string,
- });
-
- const approveClickHandler = () => {
- setApprovalAction('APPROVED');
- confirmationModal.openModal();
- };
-
- const rejectClickHandler = () => {
- setApprovalAction('REJECTED');
- confirmationModal.openModal();
- };
-
- const deleteClickHandler = () => {
- deleteModal.openModal();
- };
-
- const confirmationModalDeleteClickHandler = async () => {
- setIsLoading(true);
- const res = await MarketingApi.delete(initialValues?.id as number);
- deleteModal.closeModal();
- router.push('/marketing');
- toast.success(res?.message as string);
- setIsLoading(false);
- };
-
- const confirmationModalApproveClickHandler = async (notes: string) => {
- setIsLoading(true);
- const res = await SalesOrderApi.singleApproval(
- initialValues?.id as number,
- approvalAction,
- notes
- );
- setIsLoading(false);
- confirmationModal.closeModal();
- toast.success(res?.message as string);
- refresh?.();
- refreshApproval?.();
- };
-
- const confirmationModalDeliveryClickHandler = async (notes: string) => {
- setIsLoading(true);
- const res = await SalesOrderApi.delivery(
- initialValues?.id as number,
- notes
- );
- setIsLoading(false);
- deliveryModal.closeModal();
- toast.success(res?.message as string);
- refresh?.();
- refreshApproval?.();
- router.push(
- `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
- );
- };
-
- const approval = initialValues?.latest_approval;
- const isRejected = approval?.action == 'REJECTED';
- const isApproved = approval?.action == 'APPROVED';
-
- return (
- <>
-
-
2 ? 'Delivery Order' : 'Sales Order'}`}
- backUrl='/marketing'
- />
- {!isLoadingApproval && approvals && (
-
- )}
-
- {initialValues?.latest_approval?.step_number == 1 && (
- <>
-
-
-
- Approve
-
-
-
-
-
-
- Reject
-
-
- >
- )}
- {initialValues?.latest_approval?.step_number != 1 && (
- <>
-
-
-
- {initialValues?.latest_approval?.step_number == 3
- ? 'Edit '
- : 'Tambah '}
- Delivery Order
-
-
- >
- )}
-
-
-
-
-
-
-
- |
- No. Sales Order
- |
- : |
-
- {initialValues?.so_number}
- |
-
- {Number(initialValues?.latest_approval?.step_number) > 2 && (
-
- |
- No. Delivery Order
- |
- : |
-
- {initialValues?.delivery_order
- ?.map((item) => item.do_number)
- .join(', ')}
- |
-
- )}
-
- | Nama Pelanggan |
- : |
- {initialValues?.customer?.name} |
-
-
- | Status |
- : |
-
-
-
- {isRejected
- ? 'Ditolak'
- : formatTitleCase(approval?.step_name || '')}
-
- |
-
-
- | Tanggal Penjualan |
- : |
- {formatDate(initialValues?.so_date, 'DD MMM yyyy')} |
-
-
- | Total Penjualan |
- : |
- {formatCurrency(grandTotal as number)} |
-
-
- | Catatan |
- : |
- {initialValues?.notes ?? '-'} |
-
-
- | Dokumen Penjualan |
- : |
-
-
- |
-
- {Number(initialValues?.latest_approval?.step_number) > 2 && (
-
- | Dokumen Pengiriman |
- : |
-
- {initialValues?.delivery_order?.map((item, index) => (
-
- ))}
- |
-
- )}
-
-
-
-
- {initialValues?.sales_order && (
-
-
- data={initialValues?.sales_order}
- columns={[
- {
- header: 'Kandang',
- accessorFn(row) {
- return row.product_warehouse.warehouse.name;
- },
- },
- {
- header: 'Produk',
- accessorFn(row) {
- return row.product_warehouse.product.name;
- },
- },
- {
- header: 'Harga Satuan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.unit_price);
- },
- },
- {
- header: 'Total Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.total_weight);
- },
- },
- {
- header: 'Kuantitas',
- accessorFn(row) {
- return formatNumber(row.qty);
- },
- },
- {
- header: 'Avg. Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.avg_weight);
- },
- },
- {
- header: 'Total Penjualan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.total_price);
- },
- },
- ]}
- className={{
- containerClassName: cn({
- 'mb-20':
- initialValues?.sales_order &&
- initialValues?.sales_order?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName: 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
-
- )}
- {initialValues?.delivery_order && (
-
- {initialValues?.delivery_order.map((delivery, index) => {
- return (
-
-
-
-
- Nomor DO : {delivery.do_number}
-
-
-
- data={delivery.deliveries}
- columns={[
- {
- header: 'Tanggal Pengiriman',
- accessorFn() {
- return formatDate(
- delivery.delivery_date,
- 'DD MMM yyyy'
- );
- },
- },
- {
- header: 'No. Polisi',
- accessorFn(row) {
- return formatVechicleNumber(row.vehicle_number);
- },
- },
- {
- header: 'Kandang',
- accessorFn(row) {
- return row.product_warehouse.warehouse.name;
- },
- },
- {
- header: 'Produk',
- accessorFn(row) {
- return row.product_warehouse.product.name;
- },
- },
- {
- header: 'Harga Satuan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.unit_price);
- },
- },
- {
- header: 'Total Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.total_weight);
- },
- },
- {
- header: 'Kuantitas',
- accessorFn(row) {
- return formatNumber(row.qty);
- },
- },
- {
- header: 'Avg. Bobot (Kg)',
- accessorFn(row) {
- return formatNumber(row.avg_weight);
- },
- },
- {
- header: 'Total Penjualan (Rp)',
- accessorFn(row) {
- return formatCurrency(row.total_price);
- },
- },
- ]}
- className={{
- containerClassName: cn({
- 'mb-20':
- initialValues?.sales_order &&
- initialValues?.sales_order?.length === 0,
- }),
- tableWrapperClassName: 'overflow-x-auto min-h-full!',
- tableClassName:
- 'font-inter w-full table-auto min-h-full!',
- headerRowClassName: 'border-b border-b-gray-200',
- headerColumnClassName:
- 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
- bodyRowClassName: 'border-b border-b-gray-200',
- bodyColumnClassName:
- 'px-6 py-3 last:flex last:flex-row last:justify-end',
- paginationClassName: 'hidden',
- }}
- />
-
-
-
-
-
- );
- })}
-
- )}
-
- {initialValues?.latest_approval?.step_number != 3 && (
- <>
-
-
-
- Edit
-
-
- >
- )}
-
-
-
- Hapus
-
-
-
-
-
-
-
- >
- );
-};
-
-export default MarketingDetail;
diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts
index 4a77ebd5..0215217f 100644
--- a/src/components/pages/marketing/form/MarketingForm.schema.ts
+++ b/src/components/pages/marketing/form/MarketingForm.schema.ts
@@ -12,7 +12,7 @@ import {
BaseSalesOrder,
Marketing,
} from '@/types/api/marketing/marketing';
-import { formatDate } from '@/lib/helper';
+import { formatDate, formatTitleCase } from '@/lib/helper';
type MarketingSchemaType = {
customer_id: number | undefined;
@@ -94,7 +94,7 @@ export type SalesOrderFormValues = Yup.InferType;
export type DeliveryOrderFormValues = Yup.InferType;
// ================ Helper Function ================
-const SalesProductToFieldValues = (
+export const SalesProductToFieldValues = (
product: BaseSalesOrder
): SalesOrderProductFormValues => {
return {
@@ -109,15 +109,37 @@ const SalesProductToFieldValues = (
value: product.product_warehouse.id,
label: product.product_warehouse.product.name,
},
+ product_warehouse_data: product.product_warehouse,
product_warehouse_id: product.product_warehouse.id,
unit_price: product.unit_price,
total_weight: product.total_weight,
qty: product.qty,
avg_weight: product.avg_weight,
total_price: product.total_price,
+ marketing_type: product.marketing_type
+ ? {
+ value: product.marketing_type,
+ label: formatTitleCase(product.marketing_type),
+ }
+ : null,
+ convertion_unit: product.convertion_unit
+ ? {
+ value: product.convertion_unit,
+ label: formatTitleCase(product.convertion_unit),
+ }
+ : null,
+ week: product.week
+ ? {
+ value: product.week,
+ label: `Week ${product.week}`,
+ }
+ : null,
+ total_peti: product.total_peti,
+ weight_per_convertion: product.weight_per_convertion,
+ uom: product.product_warehouse.product.uom.name,
};
};
-const DeliveryProductToFieldValues = (
+export const DeliveryProductToFieldValues = (
salesOrders: BaseSalesOrder[],
delivery: BaseDeliveryOrder
): DeliveryOrderProductFormValues[] => {
@@ -181,6 +203,24 @@ export const mergeSOwithDO = (
avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
total_price: autofill ? so.total_price : delivery?.total_price,
marketing_product: so, // jika ada, override
+ uom: autofill ? so.uom : delivery?.uom,
+ weight_per_convertion: autofill
+ ? so.weight_per_convertion
+ : delivery?.weight_per_convertion,
+ price_per_convertion: autofill
+ ? so.price_per_convertion
+ : delivery?.price_per_convertion,
+ convertion_unit: autofill
+ ? so.convertion_unit
+ : delivery?.convertion_unit,
+ marketing_type: autofill ? so.marketing_type : delivery?.marketing_type,
+ total_peti: autofill ? so.total_peti : delivery?.total_peti,
+ price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty,
+ sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat,
+ price_sisa_berat: autofill
+ ? so.price_sisa_berat
+ : delivery?.price_sisa_berat,
+ week: autofill ? so.week : delivery?.week,
} as DeliveryOrderProductFormValues;
});
};
@@ -213,3 +253,11 @@ export const getFilledMarketingFormInitialValues = (
),
};
};
+
+export const getPricePerConvertion = (
+ totalPrice: number,
+ weightPerConvertion: number,
+ totalPeti: number
+) => {
+ return totalPrice / (weightPerConvertion * totalPeti);
+};
diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx
deleted file mode 100644
index 1f866350..00000000
--- a/src/components/pages/marketing/form/MarketingForm.tsx
+++ /dev/null
@@ -1,872 +0,0 @@
-'use client';
-
-import Button from '@/components/Button';
-import Card from '@/components/Card';
-import { FormHeader } from '@/components/helper/form/FormHeader';
-import DateInput from '@/components/input/DateInput';
-import SelectInput, {
- OptionType,
- useSelect,
-} from '@/components/input/SelectInput';
-import Modal, { useModal } from '@/components/Modal';
-import { formatCurrency, formatDate } from '@/lib/helper';
-import {
- BaseDeliveryOrder,
- BaseSalesOrder,
- CreateDeliveryOrderPayload,
- CreateSalesOrderPayload,
- CreateSalesOrderProductPayload,
- Marketing,
- UpdateDeliveryOrderPayload,
- UpdateSalesOrderPayload,
-} from '@/types/api/marketing/marketing';
-import { Icon } from '@iconify/react';
-import { memo, useCallback, useEffect, useMemo, useState } from 'react';
-import { Customer } from '@/types/api/master-data/customer';
-import { CustomerApi } from '@/services/api/master-data';
-import { useFormik } from 'formik';
-import {
- DeliveryOrderFormValues,
- DeliveryOrderSchema,
- SalesOrderFormValues,
- SalesOrderSchema,
-} from '@/components/pages/marketing/form/MarketingForm.schema';
-import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
-import {
- DeliveryOrderApi,
- MarketingApi,
- SalesOrderApi,
-} from '@/services/api/marketing/marketing';
-import ConfirmationModal from '@/components/modal/ConfirmationModal';
-import toast from 'react-hot-toast';
-import { useRouter } from 'next/navigation';
-import DebouncedTextArea from '@/components/input/DebouncedTextArea';
-import SalesOrderProductForm from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm';
-import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-view/DeliveryOrderProductTable';
-import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
-import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
-import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
-import RequirePermission from '@/components/helper/RequirePermission';
-import AlertErrorList from '@/components/helper/form/FormErrors';
-import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
-import { CreatedUser } from '@/types/api/api-general';
-import { UserApi } from '@/services/api/user';
-
-const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
-const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
-const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
-
-// ================== EXTERNAL HELPER FUNCTION ==================
-export interface ProductCalculationFields {
- qty: string | number | undefined;
- unit_price: string | number | undefined;
- total_price: string | number | undefined;
- avg_weight: string | number | undefined;
- total_weight: string | number | undefined;
-}
-
-export const SalesProductToFieldValues = (
- product: BaseSalesOrder
-): SalesOrderProductFormValues => {
- return {
- id: product.id,
- vehicle_number: product.vehicle_number,
- kandang_id: product.product_warehouse.warehouse.id,
- kandang: {
- value: product.product_warehouse.warehouse.id,
- label: product.product_warehouse.warehouse.name,
- },
- product_warehouse: {
- value: product.product_warehouse.id,
- label: product.product_warehouse.product.name,
- },
- product_warehouse_id: product.product_warehouse.id,
- unit_price: product.unit_price,
- total_weight: product.total_weight,
- qty: product.qty,
- avg_weight: product.avg_weight,
- total_price: product.total_price,
- };
-};
-export const DeliveryProductToFieldValues = (
- salesOrders: BaseSalesOrder[],
- delivery: BaseDeliveryOrder
-): DeliveryOrderProductFormValues[] => {
- const data = delivery.deliveries.map((item) => {
- const soId = salesOrders.find(
- (so) => so.product_warehouse.id === item.product_warehouse.id
- )?.id;
- return {
- id: soId,
- unit_price: item.unit_price,
- total_weight: item.total_weight,
- qty: item.qty,
- avg_weight: item.avg_weight,
- total_price: item.total_price,
- vehicle_number: item.vehicle_number,
- delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'),
- do_number: delivery.do_number,
- marketing_product_id: soId,
- marketing_product: {
- id: soId,
- vehicle_number: item.vehicle_number,
- kandang_id: item.product_warehouse.warehouse.id,
- kandang: {
- value: item.product_warehouse.warehouse.id,
- label: item.product_warehouse.warehouse.name,
- },
- product_warehouse: {
- value: item.product_warehouse.id,
- label: item.product_warehouse.product.name,
- },
- product_warehouse_id: item.product_warehouse.id,
- unit_price: item.unit_price,
- total_weight: item.total_weight,
- qty: item.qty,
- avg_weight: item.avg_weight,
- total_price: item.total_price,
- },
- } as DeliveryOrderProductFormValues;
- });
- return data;
-};
-export const mergeSOwithDO = (
- salesOrders: SalesOrderProductFormValues[],
- deliveryOrders: DeliveryOrderProductFormValues[],
- autofill?: boolean
-): DeliveryOrderProductFormValues[] => {
- return salesOrders.map((so) => {
- const delivery = deliveryOrders.find(
- (d) => d?.marketing_product_id === so.id
- );
-
- return {
- ...so, // nilai dasar dari sales order
- marketing_product_id: so.id,
- delivery_date: delivery?.delivery_date || undefined,
- do_number: delivery?.do_number || undefined,
- vehicle_number: delivery?.vehicle_number || so.vehicle_number,
- unit_price: autofill ? so.unit_price : delivery?.unit_price,
- total_weight: autofill ? so.total_weight : delivery?.total_weight,
- qty: autofill ? so.qty : delivery?.qty,
- avg_weight: autofill ? so.avg_weight : delivery?.avg_weight,
- total_price: autofill ? so.total_price : delivery?.total_price,
- marketing_product: so, // jika ada, override
- } as DeliveryOrderProductFormValues;
- });
-};
-export const recalculate = (
- field: string,
- values: ProductCalculationFields
-) => {
- const { qty, unit_price, total_price, avg_weight, total_weight } = values;
- const result: Partial = {};
- if (field == 'unit_price' || field == 'total_price' || field == 'qty') {
- if (qty && unit_price && (field == 'unit_price' || field == 'qty')) {
- result.total_price = Number(qty) * Number(unit_price);
- } else if (qty && total_price && field == 'total_price') {
- result.unit_price = Number(total_price) / Number(qty);
- }
- }
- if (field == 'avg_weight' || field == 'total_weight' || field == 'qty') {
- if (qty && avg_weight && (field == 'avg_weight' || field == 'qty')) {
- result.total_weight = Number(qty) * Number(avg_weight);
- } else if (qty && total_weight && field == 'total_weight') {
- result.avg_weight = Number(total_weight) / Number(qty);
- }
- }
- return result;
-};
-export const getSubmitField = (values: ProductCalculationFields) => {
- const { qty, unit_price, total_price, avg_weight, total_weight } = values;
-
- // Harga logic
- if (qty && unit_price && !total_price) {
- return 'unit_price';
- }
- if (qty && total_price && !unit_price) {
- return 'total_price';
- }
-
- // Bobot logic
- if (qty && avg_weight && !total_weight) {
- return 'avg_weight';
- }
- if (qty && total_weight && !avg_weight) {
- return 'total_weight';
- }
-
- // Tidak ada yang perlu dihitung
- return '';
-};
-
-const MarketingForm = ({
- formType = 'add',
- initialValues,
- afterSubmit,
-}: {
- formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
- initialValues?: Marketing;
- afterSubmit?: () => void;
-}) => {
- const router = useRouter();
- const deleteModal = useModal();
-
- const [isLoading, setIsLoading] = useState(false);
- const [selectedMarketingProduct, setSelectedMarketingProduct] =
- useState(null);
- const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
- useState(null);
- const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>(
- 'add'
- );
- const [deliveryOrderValues, setDeliveryOrderValues] = useState<
- DeliveryOrderProductFormValues[]
- >(
- mergeSOwithDO(
- initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
- initialValues?.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(initialValues.sales_order, delivery)
- ) ?? []
- )
- );
-
- // ================== REPEATER ==================
- const addSOModal = useModal();
- const addDOModal = useModal();
- const [rowSOSelection, setRowSOSelection] = useState>(
- {}
- );
- const selectedRowSOIds = Object.keys(rowSOSelection).map((item) =>
- parseInt(item)
- );
-
- // ================== FETCH OPTIONS ==================
- const {
- options: customerOptions,
- isLoadingOptions: isLoadingCustomerOptions,
- setInputValue: setInputCustomerValue,
- loadMore: loadMoreCustomer,
- } = useSelect(CustomerApi.basePath, 'id', 'name');
- const {
- options: salesOptions,
- isLoadingOptions: isLoadingSalesOptions,
- setInputValue: setInputSalesValue,
- loadMore: loadMoreSales,
- } = useSelect(UserApi.basePath, 'id', 'name');
-
- // ================== SETUP FORMIK ==================
- const formikInitialValues = useMemo<
- SalesOrderFormValues & DeliveryOrderFormValues
- >(() => {
- return {
- so_date: initialValues?.so_date || undefined,
- notes: initialValues?.notes || undefined,
- customer_id: initialValues?.customer?.id || undefined,
- sales_person_id: initialValues?.sales_person?.id || 1,
- sales_person: initialValues?.sales_person
- ? {
- value: initialValues.sales_person.id,
- label: initialValues.sales_person.name,
- }
- : null,
- customer: initialValues?.customer
- ? {
- value: initialValues.customer.id,
- label: initialValues.customer.name,
- }
- : null,
- sales_order:
- initialValues?.sales_order?.map((product) =>
- SalesProductToFieldValues(product)
- ) ?? [],
- delivery_order: mergeSOwithDO(
- initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
- initialValues?.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(initialValues.sales_order, delivery)
- ) ?? []
- ),
- };
- }, [initialValues]);
- const formik = useFormik({
- enableReinitialize: true,
- initialValues: formikInitialValues,
- validationSchema:
- formType == 'add_deliver' || formType == 'edit_deliver'
- ? DeliveryOrderSchema
- : SalesOrderSchema,
- validateOnMount: true,
- onSubmit: async (values) => {
- const payload =
- formType != 'add_deliver' && formType != 'edit_deliver'
- ? ({
- customer_id: values.customer_id as number,
- sales_person_id: values.sales_person_id as number,
- date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
- notes: values.notes as string,
- marketing_products: values.sales_order.map((product) => {
- return {
- vehicle_number: product.vehicle_number as string,
- kandang_id: product.kandang_id as number,
- product_warehouse_id: product.product_warehouse_id as number,
- unit_price: parseFloat(product.unit_price as string),
- total_weight: parseFloat(product.total_weight as string),
- qty: parseFloat(product.qty as string),
- avg_weight: parseFloat(product.avg_weight as string),
- total_price: parseFloat(product.total_price as string),
- } as CreateSalesOrderProductPayload;
- }),
- } as CreateSalesOrderPayload)
- : ({
- marketing_id: initialValues?.id as number,
- delivery_products: values.delivery_order
- .map((product) => {
- if (Boolean(product.delivery_date)) {
- return {
- marketing_product_id:
- product.marketing_product_id as number,
- unit_price: parseFloat(product.unit_price as string),
- total_weight: parseFloat(product.total_weight as string),
- qty: parseFloat(product.qty as string),
- avg_weight: parseFloat(product.avg_weight as string),
- total_price: parseFloat(product.total_price as string),
- delivery_date: formatDate(
- product.delivery_date as string,
- 'yyyy-MM-DD'
- ),
- vehicle_number: product.vehicle_number,
- };
- }
- })
- .filter((item) => Boolean(item)),
- } as UpdateDeliveryOrderPayload);
- switch (formType) {
- case 'add':
- await createMarketingHandler(payload as CreateSalesOrderPayload);
- break;
- case 'edit':
- await updateMarketingHandler(payload as UpdateSalesOrderPayload);
- break;
- case 'add_deliver':
- await createDeliveryHandler(payload as CreateDeliveryOrderPayload);
- break;
- case 'edit_deliver':
- await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload);
- break;
- default:
- break;
- }
- afterSubmit?.();
- },
- });
-
- const memoSalesOrder = formik.values.sales_order;
-
- // ================== FORM REPEATER HANDLER ==================
- const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
- setIsLoading(true);
- const createMarketingRes = await SalesOrderApi.create(values);
- if (isResponseSuccess(createMarketingRes)) {
- toast.success(createMarketingRes?.message as string);
- router.push('/marketing');
- }
- if (isResponseError(createMarketingRes)) {
- toast.error(createMarketingRes?.message as string);
- }
- setIsLoading(false);
- };
- const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => {
- setIsLoading(true);
- const updateMarketingRes = await SalesOrderApi.update(
- initialValues?.id as number,
- values
- );
- if (isResponseSuccess(updateMarketingRes)) {
- toast.success(updateMarketingRes?.message as string);
- router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
- }
- if (isResponseError(updateMarketingRes)) {
- toast.error(updateMarketingRes?.message as string);
- }
- setIsLoading(false);
- };
- const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
- setIsLoading(true);
- const createDeliveryRes = await DeliveryOrderApi.create(values);
- if (isResponseSuccess(createDeliveryRes)) {
- toast.success(createDeliveryRes?.message as string);
- setDeliveryOrderValues(
- createDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(
- createDeliveryRes.data?.sales_order,
- delivery
- )
- ) ?? []
- );
- router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
- }
- if (isResponseError(createDeliveryRes)) {
- toast.error(createDeliveryRes?.message as string);
- }
- setIsLoading(false);
- };
- const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
- setIsLoading(true);
- const updateDeliveryRes = await DeliveryOrderApi.update(
- initialValues?.id as number,
- values
- );
- if (isResponseSuccess(updateDeliveryRes)) {
- toast.success(updateDeliveryRes?.message as string);
- setDeliveryOrderValues(
- mergeSOwithDO(
- formik.values.sales_order,
- updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
- DeliveryProductToFieldValues(
- updateDeliveryRes.data?.sales_order,
- delivery
- )
- ) ?? []
- )
- );
- router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
- }
- if (isResponseError(updateDeliveryRes)) {
- toast.error(updateDeliveryRes?.message as string);
- }
- setIsLoading(false);
- };
-
- // ================== MARKETING HANDLER ==================
- const deleteMarketingHandler = async () => {
- setIsLoading(true);
- const deleteMarketingRes = await MarketingApi.delete(
- initialValues?.id as number
- );
- if (isResponseSuccess(deleteMarketingRes)) {
- toast.success(deleteMarketingRes?.message as string);
- }
- if (isResponseError(deleteMarketingRes)) {
- toast.error(deleteMarketingRes?.message as string);
- }
- setIsLoading(false);
- deleteModal.closeModal();
- router.push('/marketing');
- };
- const handleChangeCustomer = useCallback(
- (val: OptionType | OptionType[] | null) => {
- formik.setFieldValue('customer_id', (val as OptionType)?.value);
- formik.setFieldValue('customer', val as OptionType);
- },
- []
- );
- const handleChangeSalesPerson = useCallback(
- (val: OptionType | OptionType[] | null) => {
- formik.setFieldValue('sales_person_id', (val as OptionType)?.value);
- formik.setFieldValue('sales_person', val as OptionType);
- },
- []
- );
- const handleDelete = useCallback(() => {
- deleteModal.openModal();
- }, [deleteModal]);
-
- // ================== SALES ORDER HANDLER ==================
- const handleDeleteSO = useCallback(
- (id: number) => {
- const currentProducts = formik.values.sales_order;
- formik.setFieldValue(
- 'sales_order',
- currentProducts.filter((p) => p.id != id)
- );
- },
- [memoSalesOrder]
- );
- const handleEditSO = useCallback(
- (id: number) => {
- const currentProducts = formik.values.sales_order;
- const selectedProduct = currentProducts.find((p) => p.id == id);
- setSelectedMarketingProduct(selectedProduct ?? null);
- addSOModal.openModal();
- },
- [memoSalesOrder]
- );
- const handleBulkDeleteSO = useCallback(() => {
- const currentProducts = formik.values.sales_order;
- formik.setFieldValue(
- 'sales_order',
- currentProducts.filter(
- (product) => !selectedRowSOIds.includes(product.id ?? -1)
- )
- );
- setRowSOSelection({});
- }, [selectedRowSOIds, memoSalesOrder]);
- const handleAddSOClick = useCallback(() => {
- setSelectedMarketingProduct(null);
- addSOModal.openModal();
- }, [addSOModal]);
- const handleAddSubmitSO = useCallback(
- async (values: SalesOrderProductFormValues, id?: number) => {
- const currentProducts = formik.values.sales_order;
-
- const newValues = {
- ...values,
- id: values.id ?? Date.now(),
- };
-
- let updatedProducts = [];
-
- if (id) {
- // Overwrite
- updatedProducts = currentProducts.map((item) =>
- item.id === id ? newValues : item
- );
- } else {
- // Add new item
- updatedProducts = [...currentProducts, newValues];
- }
-
- formik.setFieldValue('sales_order', updatedProducts);
-
- addSOModal.closeModal();
- },
- [addSOModal, memoSalesOrder]
- );
-
- // ================== DELIVERY ORDER HANDLER ==================
- const handleEditDO = useCallback(
- (id: number, values?: DeliveryOrderProductFormValues) => {
- setDeliveryFormState('edit');
- const currentProducts = formik.values.delivery_order.find(
- (product) => product.id == id
- );
- setSelectedDeliveryProduct(values ?? currentProducts ?? null);
- addDOModal.openModal();
- },
- [addDOModal]
- );
- const handleAddDOClick = useCallback(() => {
- setDeliveryFormState('add');
- setSelectedDeliveryProduct(null);
- addDOModal.openModal();
- }, [addDOModal]);
- const handleAddSubmitDO = useCallback(
- async (values: DeliveryOrderProductFormValues) => {
- const newValues = {
- ...values,
- id: values.id ?? Date.now(),
- };
-
- setDeliveryOrderValues((prev) => [...prev, newValues]);
- addDOModal.closeModal();
- setSelectedDeliveryProduct(null);
- },
- [addDOModal]
- );
- const handleUpdateDO = useCallback(
- async (id: number, values: DeliveryOrderProductFormValues) => {
- setDeliveryOrderValues((prev) =>
- prev.map((product) =>
- product.id === id ? { ...product, ...values } : product
- )
- );
- addDOModal.closeModal();
- setSelectedDeliveryProduct(null);
- },
- [addDOModal]
- );
- const handleDeleteDO = useCallback(
- async (id: number) => {
- setDeliveryOrderValues((prev) =>
- prev.map((product) =>
- product.id === id
- ? {
- ...product,
- ...{
- unit_price: '',
- total_weight: '',
- qty: '',
- avg_weight: '',
- total_price: '',
- delivery_date: '',
- },
- }
- : product
- )
- );
- addDOModal.closeModal();
- setSelectedDeliveryProduct(null);
- },
- [addDOModal]
- );
-
- useEffect(() => {
- formik.setFieldValue('delivery_order', deliveryOrderValues);
- }, [deliveryOrderValues, initialValues]);
-
- const grandTotal = useMemo(() => {
- return memoSalesOrder.reduce(
- (total, product) =>
- total + parseFloat((product.total_price as string) || '0'),
- 0
- );
- }, [memoSalesOrder]);
-
- // ===== Formik Error List =====
- const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
-
- return (
- <>
-
-
- {/* Actions button */}
- {formType == 'edit' && (
-
-
-
-
- Hapus
-
-
-
- )}
-
- {/* Modals */}
-
-
-
-
Tambah Produk
-
-
-
-
-
-
-
-
-
-
-
-
-
- {selectedDeliveryProduct ? 'Edit' : 'Tambah'} Pengiriman
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default MarketingForm;
diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts
index 1fc4c7c0..4c20f05b 100644
--- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts
+++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts
@@ -13,6 +13,30 @@ type DeliveryOrderProductSchemaType = {
vehicle_number: string | undefined;
delivery_date: string | undefined | null;
do_number?: string | undefined | null; // Uncertain
+ uom?: string | null | undefined;
+ convertion_unit?: {
+ value: string;
+ label: string;
+ } | null;
+ weight_per_convertion?: number | null | undefined;
+ price_per_convertion?: number | null | undefined;
+ marketing_type?: {
+ value: string;
+ label: string;
+ } | null;
+ total_peti?: number | null | undefined;
+ sisa_berat?: number | null | undefined;
+ price_sisa_berat?: number | null | undefined;
+ /** Harga per butir telur untuk TELUR + QTY */
+ price_per_qty?: number | null | undefined;
+ /** Week untuk ayam pullet */
+ week?:
+ | {
+ value?: number;
+ label?: string;
+ }
+ | null
+ | undefined;
};
export const DeliveryOrderProductSchema: Yup.ObjectSchema =
@@ -40,6 +64,43 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema
+ marketingType?.value?.toLowerCase() === 'ayam_pullet',
+ then: (schema) =>
+ schema
+ .shape({
+ value: Yup.number().required(
+ 'Week wajib diisi untuk Ayam Pullet!'
+ ),
+ label: Yup.string().required(
+ 'Week wajib diisi untuk Ayam Pullet!'
+ ),
+ })
+ .required('Week wajib diisi untuk Ayam Pullet!'),
+ otherwise: (schema) => schema.optional().notRequired(),
+ }),
});
export type DeliveryOrderProductFormValues = Yup.InferType<
diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
index 9e735a95..850d88d2 100644
--- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
+++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import {
DeliveryOrderProductFormValues,
DeliveryOrderProductSchema,
@@ -8,10 +8,10 @@ import Alert from '@/components/Alert';
import Button from '@/components/Button';
import NumberInput from '@/components/input/NumberInput';
import PatternInput from '@/components/input/PatternInput';
-import { formatVechicleNumber } from '@/lib/helper';
+import { formatTitleCase, formatVechicleNumber } from '@/lib/helper';
import DateInput from '@/components/input/DateInput';
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
-import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm';
+import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema';
import * as Yup from 'yup';
import { isResponseSuccess } from '@/lib/api-helper';
import AlertErrorList from '@/components/helper/form/FormErrors';
@@ -21,9 +21,13 @@ import { ProductApi } from '@/services/api/master-data';
import StatusBadge from '@/components/helper/StatusBadge';
import SelectInputRadio from '@/components/input/SelectInputRadio';
import { OptionType } from '@/components/input/SelectInput';
-
-const roundWeight = (value: number) => Number(value.toFixed(2));
-const roundPrice = (value: number) => Math.round(value);
+import {
+ MARKETING_CONVERTION_UNIT_OPTIONS,
+ MARKETING_TYPE_OPTIONS,
+} from '@/config/constant';
+import Dropdown from '@/components/Dropdown';
+import { Icon } from '@iconify/react';
+import { handleMarketingCalculation } from '@/lib/marketing-calculation';
const DeliveryOrderProductForm = ({
formState,
@@ -49,6 +53,35 @@ const DeliveryOrderProductForm = ({
);
const [currentInput, setCurrentInput] = useState('');
+ // Check jika ada sisa berat = total_weight - (weight_per_convertion * total_peti)
+ const initialSisaBerat =
+ initialValues?.total_weight &&
+ initialValues?.weight_per_convertion &&
+ initialValues?.total_peti
+ ? Number(initialValues.total_weight) -
+ Number(initialValues.weight_per_convertion) *
+ Number(initialValues.total_peti)
+ : 0;
+
+ const initialPricePerConvertion =
+ initialValues?.total_price &&
+ initialValues?.total_peti &&
+ Number(initialValues.total_peti) !== 0
+ ? (Number(initialValues.total_price) -
+ initialSisaBerat * Number(initialValues.unit_price || 0)) /
+ Number(initialValues.total_peti)
+ : 0;
+
+ const initialPriceSisaBerat =
+ initialValues?.total_price && initialValues?.total_peti
+ ? Number(initialValues.total_price) -
+ initialPricePerConvertion * Number(initialValues.total_peti)
+ : 0;
+
+ const [hasSisaBerat, setHasSisaBerat] = useState(
+ initialSisaBerat > 0
+ );
+
// ============ Fetch Data ============
const { data: productData } = useSWR(
selectedProduct?.value
@@ -60,6 +93,27 @@ const DeliveryOrderProductForm = ({
: undefined
);
+ // Options Week dari minggu 1 - 22
+ const optionsWeek = useMemo(() => {
+ return Array.from({ length: 22 }, (_, i) => ({
+ value: i + 1,
+ label: `Week ${i + 1}`,
+ }));
+ }, []);
+
+ const options = exisitingValues
+ ?.map((item) => {
+ if (!Boolean(item.qty)) {
+ return {
+ value: item.id,
+ label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
+ } as OptionType;
+ } else {
+ return null;
+ }
+ })
+ ?.filter((item) => item != null) as OptionType[];
+
const salesOrder = salesOrders.find(
(item) => item.id === initialValues?.marketing_product_id
);
@@ -77,6 +131,19 @@ const DeliveryOrderProductForm = ({
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
marketing_product: initialValues?.marketing_product || undefined,
+ uom: initialValues?.uom || '',
+ weight_per_convertion:
+ initialValues?.weight_per_convertion != null
+ ? Number(initialValues.weight_per_convertion)
+ : null,
+ price_per_convertion: initialPricePerConvertion,
+ convertion_unit: initialValues?.convertion_unit || null,
+ marketing_type: initialValues?.marketing_type || null,
+ total_peti: initialValues?.total_peti ?? null,
+ price_per_qty: initialValues?.price_per_qty ?? null,
+ sisa_berat: initialSisaBerat,
+ price_sisa_berat: initialPriceSisaBerat,
+ week: initialValues?.week ?? null,
},
isInitialValid: false,
validationSchema: Yup.object().shape({
@@ -124,6 +191,16 @@ const DeliveryOrderProductForm = ({
avg_weight: '',
total_price: '',
marketing_product: undefined,
+ total_peti: null,
+ price_per_qty: null,
+ price_sisa_berat: null,
+ sisa_berat: null,
+ convertion_unit: null,
+ marketing_type: null,
+ weight_per_convertion: null,
+ price_per_convertion: null,
+ uom: '',
+ week: null,
},
});
// setSelectedProduct(null);
@@ -132,94 +209,34 @@ const DeliveryOrderProductForm = ({
const handleBlurField = (field: string) => {
setCurrentInput(field);
- const qty = Number(formik.values.qty || 0);
- const avgWeight = Number(formik.values.avg_weight || 0);
- const totalWeight = Number(formik.values.total_weight || 0);
- const unitPrice = Number(formik.values.unit_price || 0);
- const totalPrice = Number(formik.values.total_price || 0);
-
- if (qty <= 0) return;
-
- switch (field) {
- // ===== SOURCE FIELDS =====
- case 'qty': {
- if (avgWeight > 0) {
- const tw = roundWeight(qty * avgWeight);
- formik.setFieldValue('total_weight', tw);
-
- // Hitung total_price berdasarkan unit_price × total_weight
- if (unitPrice > 0) {
- formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
- }
- }
- break;
- }
-
- case 'avg_weight': {
- if (avgWeight > 0) {
- const tw = roundWeight(qty * avgWeight);
- formik.setFieldValue('total_weight', tw);
-
- // Hitung total_price berdasarkan unit_price × total_weight
- if (unitPrice > 0) {
- formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
- }
- }
- break;
- }
-
- case 'unit_price': {
- if (unitPrice > 0 && totalWeight > 0) {
- // Hitung total_price berdasarkan unit_price × total_weight
- formik.setFieldValue(
- 'total_price',
- roundPrice(unitPrice * totalWeight)
- );
- }
- break;
- }
-
- // ===== TOTAL EDITABLE =====
- case 'total_weight': {
- if (totalWeight > 0) {
- formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
-
- // Hitung ulang total_price berdasarkan unit_price × total_weight
- if (unitPrice > 0) {
- formik.setFieldValue(
- 'total_price',
- roundPrice(unitPrice * totalWeight)
- );
- }
- }
- break;
- }
-
- case 'total_price': {
- if (totalPrice > 0 && totalWeight > 0) {
- // Hitung unit_price berdasarkan total_price / total_weight
- formik.setFieldValue(
- 'unit_price',
- roundPrice(totalPrice / totalWeight)
- );
- }
- break;
- }
- }
+ handleMarketingCalculation(field, {
+ values: formik.values,
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat,
+ });
};
- const options = exisitingValues
- ?.map((item) => {
- if (!Boolean(item.qty)) {
- return {
- value: item.id,
- label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
- } as OptionType;
- } else {
- return null;
- }
- })
- ?.filter((item) => item != null) as OptionType[];
+ // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru
+ const handleSisaBeratToggle = (newHasSisaBerat: boolean) => {
+ setHasSisaBerat(newHasSisaBerat);
+
+ if (!newHasSisaBerat) {
+ // Ketika OFF - set nilai ke 0 dan recalculate tanpa sisa
+ formik.setFieldValue('sisa_berat', 0);
+ formik.setFieldValue('price_sisa_berat', 0);
+ }
+
+ // Langsung trigger recalculation dengan hasSisaBerat yang baru
+ handleMarketingCalculation('total_peti', {
+ values: {
+ ...formik.values,
+ sisa_berat: newHasSisaBerat ? formik.values.sisa_berat : 0,
+ price_sisa_berat: newHasSisaBerat ? formik.values.price_sisa_berat : 0,
+ },
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat: newHasSisaBerat,
+ });
+ };
const { setValues: setFormikValues } = formik;
@@ -229,9 +246,6 @@ const DeliveryOrderProductForm = ({
handleResetForm();
} else {
setFormikValues(initialValues);
- // const value = exisitingValues?.find(
- // (item) => item.id === initialValues?.id
- // );
if (initialValues?.marketing_product_id) {
setSelectedProduct({
value: initialValues?.id,
@@ -243,7 +257,23 @@ const DeliveryOrderProductForm = ({
}, [initialValues]);
// ===== Formik Error List =====
- const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
+ const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
+ formik,
+ {
+ onBeforeSubmit(e) {
+ e.preventDefault();
+ handleBlurField(currentInput);
+ formik.setFieldValue(
+ 'uom',
+ isResponseSuccess(productData) ? productData?.data?.uom?.name : ''
+ );
+ },
+ }
+ );
+
+ useEffect(() => {
+ handleBlurField('week');
+ }, [formik.values.week]);
return (
<>
@@ -252,214 +282,514 @@ const DeliveryOrderProductForm = ({
onSubmit={handleFormSubmit}
onReset={handleResetForm}
>
- {formikErrorMessage && (
- setFormErrorMessage('')} className='my-3 w-full'>
-
{formikErrorMessage}
-
- )}
-
- item.id === selectedProduct?.value
- )?.marketing_product?.product_warehouse?.label,
- } as OptionType)
- : null
- }
- onChange={(value) => {
- const selected = value as OptionType;
- setSelectedProduct(selected);
+
+ {formikErrorMessage && (
+
setFormErrorMessage('')}
+ className='my-3 w-full'
+ >
+
{formikErrorMessage}
+
+ )}
+
+ {/* Tanggal Pengiriman */}
+
+
+ {/* No. Polisi */}
+
+
+ {/* Produk */}
+
item.id === selectedProduct?.value
+ )?.marketing_product?.product_warehouse?.label,
+ } as OptionType)
+ : null
+ }
+ onChange={(value) => {
+ const selected = value as OptionType;
+ setSelectedProduct(selected);
+
+ const so = salesOrders?.find(
+ (item) => item.id === selected?.value
+ );
+ if (!so) {
+ formik.setValues({
+ ...formik.values,
+ marketing_product_id: undefined,
+ marketing_product: null,
+ qty: '',
+ unit_price: '',
+ total_price: '',
+ avg_weight: '',
+ total_weight: '',
+ vehicle_number: '',
+ });
+ return;
+ }
- const so = salesOrders?.find((item) => item.id === selected?.value);
- if (!so) {
formik.setValues({
...formik.values,
- marketing_product_id: undefined,
- marketing_product: null,
- qty: '',
- unit_price: '',
- total_price: '',
- avg_weight: '',
- total_weight: '',
- vehicle_number: '',
+ marketing_product_id: selected.value as number,
+ marketing_product: SalesProductToFieldValues(so),
+ qty: so.qty,
+ unit_price: so.unit_price,
+ total_price: so.total_price,
+ avg_weight: so.avg_weight,
+ total_weight: so.total_weight,
+ vehicle_number: so.vehicle_number,
});
- return;
+ }}
+ startAdornment={
+ selectedProduct && (
+ item.id === selectedProduct?.value
+ )?.marketing_product?.kandang?.label ?? ''
+ }
+ color='success'
+ className={{
+ badge: 'whitespace-nowrap w-fit font-semibold',
+ }}
+ />
+ )
}
+ isClearable
+ isError={Boolean(formik.errors.marketing_product_id)}
+ errorMessage={formik.errors.marketing_product_id}
+ required
+ />
- formik.setValues({
- ...formik.values,
- marketing_product_id: selected.value as number,
- marketing_product: SalesProductToFieldValues(so),
- qty: so.qty,
- unit_price: so.unit_price,
- total_price: so.total_price,
- avg_weight: so.avg_weight,
- total_weight: so.total_weight,
- vehicle_number: so.vehicle_number,
- });
- }}
- startAdornment={
- selectedProduct && (
- item.id === selectedProduct?.value
- )?.marketing_product?.kandang?.label ?? ''
- }
- color='success'
- className={{
- badge: 'whitespace-nowrap w-fit font-semibold',
- }}
+ {/* Kategori */}
+ {
+ formik.setFieldValue('marketing_type', val);
+ }}
+ isClearable
+ placeholder='Pilih Kategori'
+ isDisabled
+ />
+
+ {/* Konversi Satuan Telur */}
+ {formik.values.marketing_type &&
+ formik.values.marketing_type.value.toLowerCase() === 'telur' &&
+ (!formik.values.convertion_unit ||
+ formik.values.convertion_unit.value.toLowerCase() !== 'peti') && (
+ formik.setFieldValue('convertion_unit', val)}
+ isClearable
+ placeholder='Pilih Konversi Satuan'
/>
- )
- }
- isClearable
- isError={Boolean(formik.errors.marketing_product_id)}
- errorMessage={formik.errors.marketing_product_id}
- required
- />
+ )}
+ {formik.values.convertion_unit &&
+ formik.values.convertion_unit.value.toLowerCase() === 'peti' && (
+
+
+
+
+
+
+ {formatTitleCase(
+ formik.values.convertion_unit.value
+ )}
+
+
+
+
+ }
+ className={{
+ wrapper: 'relative',
+ content:
+ 'rounded-xl mt-1 border border-base-content/5 shadow-sm overflow-hidden min-w-68.5 sm:min-w-103.25 w-full',
+ }}
+ >
+
+
+
+
{
+ formik.setFieldValue(
+ 'weight_per_convertion',
+ Number(e.target.value)
+ );
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('weight_per_convertion')}
+ />
+
+
+ )}
-
- {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
- }}
- onBlur={() => handleBlurField('qty')}
- isError={Boolean(formik.errors.qty)}
- errorMessage={formik.errors.qty}
- placeholder='Masukan Kuantitas'
- endAdornment={
-
-
- {isResponseSuccess(productData)
- ? productData?.data?.uom.name
- : ''}
+ {/* Konversi Satuan Week Pullet */}
+ {formik.values.marketing_type?.value.toLowerCase() ===
+ 'ayam_pullet' && (
+ {
+ formik.setFieldValue('week', val);
+ }}
+ placeholder='Pilih Week'
+ />
+ )}
+
+ {/* Total Peti */}
+ {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && (
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('total_peti')}
+ isError={
+ formik.touched.total_peti && Boolean(formik.errors.total_peti)
+ }
+ errorMessage={formik.errors.total_peti}
+ placeholder='Masukan Total Peti'
+ endAdornment={
+
+ Kg
+
+ }
+ bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
+ />
+ )}
+
+ {/* Avg. Bobot */}
+ {formik.values.marketing_type?.value.toLowerCase() === 'trading' ||
+ (formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
+ formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('avg_weight')}
+ isError={
+ formik.touched.avg_weight &&
+ Boolean(formik.errors.avg_weight)
+ }
+ errorMessage={formik.errors.avg_weight}
+ placeholder='Masukan Bobot Rata-rata'
+ />
+ ))}
+
+ {/* Total Bobot */}
+ {formik.values.marketing_type?.value.toLowerCase() !== 'trading' && (
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('total_weight')}
+ isError={
+ formik.touched.total_weight &&
+ Boolean(formik.errors.total_weight)
+ }
+ errorMessage={formik.errors.total_weight}
+ placeholder='Masukan Total Bobot'
+ />
+ )}
+
+ {/* Kuantitas */}
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('qty')}
+ isError={Boolean(formik.errors.qty)}
+ errorMessage={formik.errors.qty}
+ placeholder='Masukan Kuantitas'
+ endAdornment={
+
+
+ {isResponseSuccess(productData)
+ ? productData?.data?.uom.name
+ : ''}
+
+
+ }
+ bottomLabel={
+ formik.values.marketing_product_id
+ ? 'Stok dijual: ' +
+ salesOrders?.find(
+ (item) => item.id === formik.values.marketing_product_id
+ )?.qty +
+ ' ' +
+ (isResponseSuccess(productData)
+ ? productData?.data?.uom.name
+ : '')
+ : ''
+ }
+ />
+
+ {/* Harga per convertion unit (PETI / KG) */}
+ {(formik.values.convertion_unit?.value.toLowerCase() === 'peti' ||
+ formik.values.convertion_unit?.value.toLowerCase() === 'kg') && (
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('price_per_convertion')}
+ isError={
+ formik.touched.price_per_convertion &&
+ Boolean(formik.errors.price_per_convertion)
+ }
+ errorMessage={formik.errors.price_per_convertion}
+ placeholder='Masukan Harga Satuan'
+ />
+ )}
+
+ {/* Harga per butir untuk TELUR + QTY */}
+ {formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
+ formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
+ {
+ formik.setFieldValue('price_per_qty', Number(e.target.value));
+ setCurrentInput('price_per_qty');
+ }}
+ onBlur={() => handleBlurField('price_per_qty')}
+ isError={
+ formik.touched.price_per_qty &&
+ Boolean(formik.errors.price_per_qty)
+ }
+ errorMessage={formik.errors.price_per_qty}
+ placeholder='Masukan Harga per Butir'
+ />
+ )}
+
+ {/* Harga Satuan */}
+ {formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
+ formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('unit_price')}
+ isError={Boolean(formik.errors.unit_price)}
+ errorMessage={formik.errors.unit_price}
+ placeholder='Masukan Harga Satuan'
+ />
+ )}
+
+ {/* Sisa kg diluar peti */}
+ {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && (
+
+
+ handleSisaBeratToggle(!hasSisaBerat)}
+ className='toggle toggle-primary rounded-full before:rounded-full before:bg-base-content/50 border-base-content/50 checked:border-primary checked:bg-primary checked:before:bg-base-100'
+ />
+
+
+
+ Jika ada, masukan berat di luar peti
- }
- bottomLabel={
- formik.values.marketing_product_id
- ? 'Stok dijual: ' +
- salesOrders?.find(
- (item) => item.id === formik.values.marketing_product_id
- )?.qty +
- ' ' +
- (isResponseSuccess(productData)
- ? productData?.data?.uom.name
- : '')
- : ''
- }
- />
- {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
- }}
- onBlur={() => handleBlurField('unit_price')}
- isError={Boolean(formik.errors.unit_price)}
- errorMessage={formik.errors.unit_price}
- placeholder='Masukan Harga Satuan'
- />
- {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
- }}
- onBlur={() => handleBlurField('avg_weight')}
- isError={Boolean(formik.errors.avg_weight)}
- errorMessage={formik.errors.avg_weight}
- placeholder='Masukan Bobot Rata-rata'
- />
- {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
- }}
- onBlur={() => handleBlurField('total_weight')}
- isError={Boolean(formik.errors.total_weight)}
- errorMessage={formik.errors.total_weight}
- placeholder='Masukan Total Bobot'
- />
+ )}
- {
- formik.handleChange(e);
- setCurrentInput(e.target.name);
- }}
- onBlur={() => handleBlurField('total_price')}
- isError={Boolean(formik.errors.total_price)}
- errorMessage={formik.errors.total_price}
- placeholder='Masukan Total Penjualan'
- />
+ {hasSisaBerat && (
+ <>
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('sisa_berat')}
+ isError={
+ formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat)
+ }
+ errorMessage={formik.errors.sisa_berat}
+ placeholder='Masukan Sisa Berat'
+ />
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('price_sisa_berat')}
+ isError={
+ formik.touched.price_sisa_berat &&
+ Boolean(formik.errors.price_sisa_berat)
+ }
+ errorMessage={formik.errors.price_sisa_berat}
+ placeholder='Masukan Harga Sisa Berat'
+ />
+ >
+ )}
-
+ {/* Total Penjualan */}
+ {
+ formik.handleChange(e);
+ setCurrentInput(e.target.name);
+ }}
+ onBlur={() => handleBlurField('total_price')}
+ isError={
+ formik.touched.total_price && Boolean(formik.errors.total_price)
+ }
+ errorMessage={formik.errors.total_price}
+ placeholder='Masukan Total Penjualan'
+ />
+
+
-
+
=
@@ -40,6 +65,10 @@ export const SalesOrderProductSchema: Yup.ObjectSchema()
+ .nullable()
+ .optional()
+ .notRequired(),
product_warehouse_id: Yup.number()
.min(1, 'Produk wajib diisi!')
.required('Produk wajib diisi!'),
@@ -59,6 +88,42 @@ export const SalesOrderProductSchema: Yup.ObjectSchema
+ marketingType?.value?.toLowerCase() === 'ayam_pullet',
+ then: (schema) =>
+ schema
+ .shape({
+ value: Yup.number().required(
+ 'Week wajib diisi untuk Ayam Pullet!'
+ ),
+ label: Yup.string().required(
+ 'Week wajib diisi untuk Ayam Pullet!'
+ ),
+ })
+ .required('Week wajib diisi untuk Ayam Pullet!'),
+ otherwise: (schema) => schema.optional().notRequired(),
+ }),
});
export type SalesOrderProductFormValues = Yup.InferType<
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
index 3161054c..c718c40c 100644
--- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
+++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
@@ -5,7 +5,7 @@ import {
SalesOrderProductFormValues,
SalesOrderProductSchema,
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
-import { RefObject, useMemo, useState } from 'react';
+import { RefObject, useEffect, useMemo, useState } from 'react';
import { OptionType, useSelect } from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang';
import { WarehouseApi } from '@/services/api/master-data';
@@ -14,15 +14,23 @@ import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput';
import Button from '@/components/Button';
import { isResponseSuccess } from '@/lib/api-helper';
-import { formatNumber, formatVechicleNumber } from '@/lib/helper';
+import {
+ formatNumber,
+ formatTitleCase,
+ formatVechicleNumber,
+} from '@/lib/helper';
import PatternInput from '@/components/input/PatternInput';
import Alert from '@/components/Alert';
import AlertErrorList from '@/components/helper/form/FormErrors';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import SelectInputRadio from '@/components/input/SelectInputRadio';
-
-const roundWeight = (value: number) => Number(value.toFixed(2));
-const roundPrice = (value: number) => Math.round(value);
+import {
+ MARKETING_CONVERTION_UNIT_OPTIONS,
+ MARKETING_TYPE_OPTIONS,
+} from '@/config/constant';
+import { Icon } from '@iconify/react';
+import Dropdown from '@/components/Dropdown';
+import { handleMarketingCalculation } from '@/lib/marketing-calculation';
const SalesOrderProductForm = ({
initialValues,
@@ -42,6 +50,35 @@ const SalesOrderProductForm = ({
const [selectedProductWarehouse, setSelectedProductWarehouse] =
useState(null);
+ // Check jika ada sisa berat = total_weight - (weight_per_convertion * total_peti)
+ const initialSisaBerat =
+ initialValues?.total_weight &&
+ initialValues?.weight_per_convertion &&
+ initialValues?.total_peti
+ ? Number(initialValues.total_weight) -
+ Number(initialValues.weight_per_convertion) *
+ Number(initialValues.total_peti)
+ : 0;
+
+ const initialPricePerConvertion =
+ initialValues?.total_price &&
+ initialValues?.total_peti &&
+ Number(initialValues.total_peti) !== 0
+ ? (Number(initialValues.total_price) -
+ initialSisaBerat * Number(initialValues.unit_price || 0)) /
+ Number(initialValues.total_peti)
+ : 0;
+
+ const initialPriceSisaBerat =
+ initialValues?.total_price && initialValues?.total_peti
+ ? Number(initialValues.total_price) -
+ initialPricePerConvertion * Number(initialValues.total_peti)
+ : 0;
+
+ const [hasSisaBerat, setHasSisaBerat] = useState(
+ initialSisaBerat > 0
+ );
+
// ============ Formik ============
const formik = useFormik({
enableReinitialize: true,
@@ -57,6 +94,18 @@ const SalesOrderProductForm = ({
avg_weight: initialValues?.avg_weight || '',
total_price: initialValues?.total_price || '',
uom: initialValues?.uom || '',
+ weight_per_convertion:
+ initialValues?.weight_per_convertion != null
+ ? Number(initialValues.weight_per_convertion)
+ : null,
+ price_per_convertion: initialPricePerConvertion,
+ convertion_unit: initialValues?.convertion_unit || null,
+ marketing_type: initialValues?.marketing_type || null,
+ total_peti: initialValues?.total_peti ?? null,
+ price_per_qty: initialValues?.price_per_qty ?? null,
+ sisa_berat: initialSisaBerat,
+ price_sisa_berat: initialPriceSisaBerat,
+ week: initialValues?.week ?? null,
},
validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => {
@@ -76,6 +125,14 @@ const SalesOrderProductForm = ({
loadMore: loadMoreKandang,
} = useSelect(WarehouseApi.basePath, 'id', 'name');
+ // Options Week dari minggu 1 - 22
+ const optionsWeek = useMemo(() => {
+ return Array.from({ length: 22 }, (_, i) => ({
+ value: i + 1,
+ label: `Week ${i + 1}`,
+ }));
+ }, []);
+
const {
options: warehouseSourceOptions,
rawData: warehouseSourceRawData,
@@ -89,6 +146,7 @@ const SalesOrderProductForm = ({
'',
{
warehouse_id: formik.values.kandang_id?.toString() ?? '',
+ type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '',
}
);
@@ -121,14 +179,18 @@ const SalesOrderProductForm = ({
);
setSelectedProductWarehouse(productWarehouse || null);
formik.setFieldValue('qty', productWarehouse?.quantity);
+ formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
handleBlurField('qty');
} else {
formik.setFieldValue('qty', '');
+ formik.setFieldValue('uom', '');
}
};
const handleResetForm = () => {
setFormErrorMessage('');
+ setHasSisaBerat(false);
+ setSelectedProductWarehouse(null);
formik.resetForm({
values: {
vehicle_number: '',
@@ -141,6 +203,16 @@ const SalesOrderProductForm = ({
qty: '',
avg_weight: '',
total_price: '',
+ total_peti: null,
+ price_per_qty: null,
+ price_sisa_berat: null,
+ sisa_berat: null,
+ convertion_unit: null,
+ marketing_type: null,
+ weight_per_convertion: null,
+ price_per_convertion: null,
+ uom: '',
+ week: null,
},
});
};
@@ -148,113 +220,33 @@ const SalesOrderProductForm = ({
const handleBlurField = (field: string) => {
setCurrentInput(field);
- const qty = Number(formik.values.qty || 0);
- const avgWeight = Number(formik.values.avg_weight || 0);
- const totalWeight = Number(formik.values.total_weight || 0);
- const unitPrice = Number(formik.values.unit_price || 0);
- const totalPrice = Number(formik.values.total_price || 0);
+ handleMarketingCalculation(field, {
+ values: formik.values,
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat,
+ });
+ };
- if (qty <= 0) return;
+ // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru
+ const handleSisaBeratToggle = (newHasSisaBerat: boolean) => {
+ setHasSisaBerat(newHasSisaBerat);
- // Cek apakah produk memiliki flag OVK atau PAKAN
- const productFlags = selectedProductWarehouse?.product?.flags || [];
- const isOvkOrPakan =
- productFlags.includes('OVK') || productFlags.includes('PAKAN');
-
- switch (field) {
- // ===== SOURCE FIELDS =====
- case 'qty': {
- if (avgWeight > 0) {
- const tw = roundWeight(qty * avgWeight);
- formik.setFieldValue('total_weight', tw);
-
- // Hitung total_price berdasarkan flag produk
- if (unitPrice > 0) {
- if (isOvkOrPakan) {
- // Untuk OVK/PAKAN: total_price = qty × unit_price
- formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
- } else {
- // Untuk produk lain: total_price = unit_price × total_weight
- formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
- }
- }
- }
- break;
- }
-
- case 'avg_weight': {
- if (avgWeight > 0) {
- const tw = roundWeight(qty * avgWeight);
- formik.setFieldValue('total_weight', tw);
-
- // Hitung total_price berdasarkan flag produk
- if (unitPrice > 0) {
- if (isOvkOrPakan) {
- // Untuk OVK/PAKAN: total_price = qty × unit_price
- formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
- } else {
- // Untuk produk lain: total_price = unit_price × total_weight
- formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
- }
- }
- }
- break;
- }
-
- case 'unit_price': {
- if (unitPrice > 0) {
- if (isOvkOrPakan) {
- // Untuk OVK/PAKAN: total_price = qty × unit_price
- formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
- } else if (totalWeight > 0) {
- // Untuk produk lain: total_price = unit_price × total_weight
- formik.setFieldValue(
- 'total_price',
- roundPrice(unitPrice * totalWeight)
- );
- }
- }
- break;
- }
-
- // ===== TOTAL EDITABLE =====
- case 'total_weight': {
- if (totalWeight > 0) {
- formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
-
- // Hitung ulang total_price berdasarkan flag produk
- if (unitPrice > 0) {
- if (isOvkOrPakan) {
- // Untuk OVK/PAKAN: total_price = qty × unit_price
- formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
- } else {
- // Untuk produk lain: total_price = unit_price × total_weight
- formik.setFieldValue(
- 'total_price',
- roundPrice(unitPrice * totalWeight)
- );
- }
- }
- }
- break;
- }
-
- case 'total_price': {
- if (totalPrice > 0) {
- if (isOvkOrPakan && qty > 0) {
- // Untuk OVK/PAKAN: unit_price = total_price / qty
- formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
- } else if (totalWeight > 0) {
- // Untuk produk lain: unit_price = total_price / total_weight
- formik.setFieldValue(
- 'unit_price',
- roundPrice(totalPrice / totalWeight)
- );
- }
- }
- break;
- }
+ if (!newHasSisaBerat) {
+ // Ketika OFF - set nilai ke 0 dan recalculate tanpa sisa
+ formik.setFieldValue('sisa_berat', 0);
+ formik.setFieldValue('price_sisa_berat', 0);
}
+
+ // Langsung trigger recalculation dengan hasSisaBerat yang baru
+ handleMarketingCalculation('total_peti', {
+ values: {
+ ...formik.values,
+ sisa_berat: newHasSisaBerat ? formik.values.sisa_berat : 0,
+ price_sisa_berat: newHasSisaBerat ? formik.values.price_sisa_berat : 0,
+ },
+ setFieldValue: formik.setFieldValue,
+ hasSisaBerat: newHasSisaBerat,
+ });
};
// ===== Formik Error List =====
@@ -272,6 +264,10 @@ const SalesOrderProductForm = ({
}
);
+ useEffect(() => {
+ handleBlurField('week');
+ }, [formik.values.week]);
+
return (
<>