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 ( -
+
@@ -539,6 +612,7 @@ const FinanceTable = () => { label='Jenis Transaksi' value={selectedTransactionType} onChange={transactionTypeChangeHandler} + closeMenuOnSelect={false} isClearable isMulti /> @@ -550,6 +624,7 @@ const FinanceTable = () => { onInputChange={customerInputValue} onMenuScrollToBottom={customerLoadMore} isLoading={customerIsLoadingOptions} + closeMenuOnSelect={false} isClearable isMulti /> @@ -561,31 +636,18 @@ const FinanceTable = () => { onInputChange={supplierInputValue} onMenuScrollToBottom={supplierLoadMore} isLoading={supplierIsLoadingOptions} + closeMenuOnSelect={false} isClearable isMulti /> ({ - label: - bankRawData.data.find((data) => data.id === bank?.value) - ?.alias + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.account_number + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.owner, - value: bank?.value, - })) - : [] - } + options={bankSelectOptions} label='Bank' value={selectedBank} onChange={bankChangeHandler} onInputChange={bankInputValue} onMenuScrollToBottom={bankLoadMore} + closeMenuOnSelect={false} isClearable isMulti /> @@ -597,22 +659,32 @@ const FinanceTable = () => { isClearable />
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 && ( - <> - - - - - - - - - )} - {initialValues?.latest_approval?.step_number != 1 && ( - <> - - - - - )} -
- - -
- - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - -
- No. Sales Order - : - {initialValues?.so_number} -
- 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: - -
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 && ( - <> - - - - - )} - - - -
-
- - - - - ); -}; - -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 && ( - <> - - - - - - - - - )} - {initialValues?.latest_approval?.step_number != 1 && ( - <> - - - - - )} -
- - -
- - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - -
- No. Sales Order - : - {initialValues?.so_number} -
- 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: - -
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 && ( - <> - - - - - )} - - - -
-
- - - - - ); -}; - -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 ( - <> -
- - {/* Input Cutomer And Date */} - -
- - -
-
- - {/* Input Table Repeater Sales Order */} - - {/* */} - - - {/* Input Table Repeater Delivery Order */} - {(formType == 'add_deliver' || formType == 'edit_deliver') && - initialValues?.sales_order && - initialValues?.sales_order.length > 0 && ( - - - - )} - - {/* Input Notes */} -
-
- - -
-
- Total Penjualan - - {formatCurrency(grandTotal)}{' '} - -
-
- - - - {/* Form Actions */} -
- - -
- - - {/* Actions button */} - {formType == 'edit' && ( -
- - - -
- )} - - {/* 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', + }} + > +
    + {MARKETING_CONVERTION_UNIT_OPTIONS.map((option) => ( +
  • + +
  • + ))} +
+ +
+ { + 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' + /> + +
-
+
+ + ))} + + +
+ { + formik.setFieldValue( + 'weight_per_convertion', + Number(e.target.value) + ); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('weight_per_convertion')} + /> +
+
+ )} + + {/* 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={formik.touched.qty && Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty} + placeholder='Masukan Kuantitas' + endAdornment={ + formik.values.uom ? ( +
+ + {formik.values.uom} + +
+ ) : undefined + } + bottomLabel={ + isResponseSuccess(warehouseSourceRawData) && + formik.values.product_warehouse_id + ? `Stok tersedia: ${formatNumber( + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.quantity ?? 0 + )} ${formik.values.uom}` + : '' + } + /> + + {/* 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 per Uom Produk Warehouse */} + {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={ + formik.touched.unit_price && 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={ - isResponseSuccess(warehouseSourceRawData) && - formik.values.product_warehouse_id - ? `Stok tersedia: ${formatNumber( - warehouseSourceRawData?.data?.find( - (item) => item.id === formik.values.product_warehouse_id - )?.quantity ?? 0 - )} ${selectedProductWarehouse?.product?.uom?.name}` - : '' - } - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('unit_price')} - isError={ - formik.touched.unit_price && 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={ - formik.touched.avg_weight && 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={ - formik.touched.total_weight && 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={ - formik.touched.total_price && 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' + /> + + {formErrorList.length > 0 && ( +
+ +
+ )}
-
+
+ ); + }, + }, + { + accessorKey: 'supplier.name', header: 'Vendor', cell: (props) => props.row.original.supplier.name, }, + { + accessorKey: 'requester_name', + header: 'Nama Pengaju', + cell: (props) => props.row.original.requester_name || '-', + }, + { + accessorKey: 'products.name', + header: 'Produk', + cell: (props) => { + const products = props.row.original.products; + if (!products || products.length === 0) return '-'; + return ( +
    + {products.map((product, index) => ( +
  • {product.name}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'location.name', + header: 'Lokasi', + cell: (props) => props.row.original.location?.name || '-', + }, { accessorKey: 'po_date', header: 'Tgl. PO', @@ -180,6 +258,14 @@ const PurchaseTable = () => { ? formatDate(props.row.original.po_date, 'DD MMM YYYY') : '-', }, + { + accessorKey: 'due_date', + header: 'Jatuh Tempo', + cell: (props) => + props.row.original.due_date + ? formatDate(props.row.original.due_date, 'DD MMM YYYY') + : '-', + }, { header: 'Aging', cell: (props) => { @@ -231,7 +317,7 @@ const PurchaseTable = () => { color={statusColor} text={statusText} className={{ - badge: 'whitespace-nowrap', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); @@ -330,11 +416,21 @@ const PurchaseTable = () => { + } className={{ - wrapper: 'sm:max-w-3xs', + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} />
@@ -409,6 +505,15 @@ const PurchaseTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {invoicePurchaseData && ( +
+ +
+ )} ); }; diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index aed154d0..4ad093e1 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { Page, Text, @@ -235,11 +235,16 @@ const pdfStyles = StyleSheet.create({ interface PurchaseOrderInvoiceProps { data?: Purchase; className?: string; + triggerDownloadOnMount?: boolean; } -const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { +const PurchaseOrderInvoice = ({ + data, + triggerDownloadOnMount, +}: PurchaseOrderInvoiceProps) => { const [, setIsGeneratingPDF] = useState(false); const purchaseData = data; + const hasDownloadedRef = useRef(false); const grandTotal = useMemo(() => { return ( @@ -250,7 +255,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { ); }, [purchaseData?.items]); - const handleDownloadPDF = async () => { + const handleDownloadPDF = useCallback(async () => { if (!purchaseData) { toast.error('No purchase order data available'); return; @@ -510,7 +515,20 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { } finally { setIsGeneratingPDF(false); } - }; + }, [purchaseData]); + + useEffect(() => { + if (triggerDownloadOnMount && purchaseData && !hasDownloadedRef.current) { + hasDownloadedRef.current = true; + handleDownloadPDF(); + } + }, [triggerDownloadOnMount, purchaseData]); + + useEffect(() => { + if (!triggerDownloadOnMount) { + hasDownloadedRef.current = false; + } + }, [triggerDownloadOnMount]); if (!purchaseData) { return ( @@ -520,6 +538,10 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { ); } + if (triggerDownloadOnMount) { + return null; + } + return purchaseData?.po_number && purchaseData.po_number !== 'Belum dibuat' ? (
*/} - {/* TODO: Uncomment when BE is ready */} - {/*
- -
*/} + { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> {/* Action Buttons */}
@@ -889,6 +1015,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { diff --git a/src/config/constant.ts b/src/config/constant.ts index fb5214a6..170a27bf 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -513,16 +513,35 @@ export const FILTER_TYPE_OPTIONS = [ export const MARKETING_TYPE_OPTIONS = [ { - label: 'Ayam', - value: 'ayam', + label: 'Ayam Pullet', + value: 'AYAM_PULLET', }, { - label: 'Telur', - value: 'telur', + label: 'Ayam', + value: 'AYAM', }, { label: 'Trading', - value: 'trading', + value: 'TRADING', + }, + { + label: 'Telur', + value: 'TELUR', + }, +]; + +export const MARKETING_CONVERTION_UNIT_OPTIONS = [ + { + label: 'Kg', + value: 'kg', + }, + { + label: 'Qty', + value: 'qty', + }, + { + label: 'Peti', + value: 'peti', }, ]; diff --git a/src/lib/marketing-calculation.ts b/src/lib/marketing-calculation.ts new file mode 100644 index 00000000..5715ec88 --- /dev/null +++ b/src/lib/marketing-calculation.ts @@ -0,0 +1,507 @@ +/** + * Marketing Product Calculation Hook + * + * Reusable calculation logic for Sales Order and Delivery Order forms. + * Handles 6 scenarios: TRADING, AYAM_PULLET, AYAM, TELUR+KG, TELUR+PETI, TELUR+QTY + */ + +// ============ Types ============ + +export type MarketingFormValues = { + qty?: string | number; + avg_weight?: string | number; + total_weight?: string | number; + unit_price?: string | number; + total_price?: string | number; + marketing_type?: { value: string; label: string } | null; + convertion_unit?: { value: string; label: string } | null; + week?: { value?: number; label?: string } | null; + weight_per_convertion?: number | null; + price_per_convertion?: number | null; + total_peti?: number | null; + sisa_berat?: number | null; + price_sisa_berat?: number | null; + /** Harga per butir telur untuk TELUR + QTY */ + price_per_qty?: number | null; +}; + +export type SetFieldValueFn = ( + field: string, + value: string | number | null +) => void; + +export type CalculationContext = { + values: MarketingFormValues; + setFieldValue: SetFieldValueFn; + hasSisaBerat: boolean; +}; + +// ============ Helper Functions ============ + +/** + * Round weight untuk operasi perkalian (total_weight = avg_weight × qty) + * Precision: 2 decimal places + */ +export const roundWeight = (value: number): number => Number(value.toFixed(2)); + +/** + * Precise weight untuk operasi pembagian (avg_weight = total_weight / qty) + * Tidak di-round untuk menjaga akurasi maksimal + */ +export const preciseWeight = (value: number): number => value; + +export const roundPrice = (value: number): number => Math.round(value); + +// ============ Calculation Handlers ============ + +/** + * TRADING: Penjualan non-livestock (obat-obatan, pakan, dll) + * - Formula: total_price = qty × unit_price + * - Weight fields: always 0 + */ +export const calculateTrading = ( + field: string, + ctx: CalculationContext +): void => { + const { values, setFieldValue } = ctx; + const unitPrice = Number(values.unit_price || 0); + const qty = Number(values.qty || 0); + const totalPrice = Number(values.total_price || 0); + + // Trading: avg_weight = 0, total_weight = 0 + setFieldValue('avg_weight', 0); + setFieldValue('total_weight', 0); + + switch (field) { + case 'unit_price': + case 'qty': { + if (unitPrice > 0 && qty > 0) { + setFieldValue('total_price', roundPrice(unitPrice * qty)); + } + break; + } + case 'total_price': { + if (totalPrice > 0 && qty > 0) { + setFieldValue('unit_price', roundPrice(totalPrice / qty)); + } + break; + } + } +}; + +/** + * AYAM_PULLET: Penjualan pullet dengan harga berdasarkan umur minggu + * - Formula: total_price = unit_price × week × qty + * - total_weight = avg_weight × qty + */ +export const calculateAyamPullet = ( + field: string, + ctx: CalculationContext +): void => { + const { values, setFieldValue } = ctx; + const unitPrice = Number(values.unit_price || 0); + const week = Number(values.week?.value || 0); + const qty = Number(values.qty || 0); + const avgWeight = Number(values.avg_weight || 0); + const totalWeight = Number(values.total_weight || 0); + const totalPrice = Number(values.total_price || 0); + + switch (field) { + case 'unit_price': + case 'week': + case 'qty': { + // total_price = unit_price × week × qty + if (unitPrice > 0 && week > 0 && qty > 0) { + setFieldValue('total_price', roundPrice(unitPrice * week * qty)); + } + // total_weight = avg_weight × qty + if (avgWeight > 0 && qty > 0) { + setFieldValue('total_weight', roundWeight(avgWeight * qty)); + } + break; + } + case 'avg_weight': { + if (avgWeight > 0 && qty > 0) { + setFieldValue('total_weight', roundWeight(avgWeight * qty)); + } + break; + } + case 'total_weight': { + if (totalWeight > 0 && qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + break; + } + case 'total_price': { + // Reverse: unit_price = total_price / (week × qty) + if (totalPrice > 0 && week > 0 && qty > 0) { + setFieldValue('unit_price', roundPrice(totalPrice / (week * qty))); + } + break; + } + } +}; + +/** + * AYAM: Penjualan ayam hidup/potong dengan harga per kg + * - Formula: total_price = total_weight × unit_price + * - total_weight = qty × avg_weight + */ +export const calculateAyam = (field: string, ctx: CalculationContext): void => { + const { values, setFieldValue } = ctx; + const unitPrice = Number(values.unit_price || 0); + const qty = Number(values.qty || 0); + const avgWeight = Number(values.avg_weight || 0); + const totalWeight = Number(values.total_weight || 0); + const totalPrice = Number(values.total_price || 0); + + switch (field) { + case 'qty': + case 'avg_weight': { + // total_weight = qty × avg_weight + if (qty > 0 && avgWeight > 0) { + const tw = roundWeight(qty * avgWeight); + setFieldValue('total_weight', tw); + // total_price = total_weight × unit_price + if (unitPrice > 0) { + setFieldValue('total_price', roundPrice(tw * unitPrice)); + } + } + break; + } + case 'total_weight': { + // avg_weight = total_weight / qty + if (totalWeight > 0 && qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + // total_price = total_weight × unit_price + if (unitPrice > 0 && totalWeight > 0) { + setFieldValue('total_price', roundPrice(totalWeight * unitPrice)); + } + break; + } + case 'unit_price': { + // total_price = total_weight × unit_price + if (unitPrice > 0 && totalWeight > 0) { + setFieldValue('total_price', roundPrice(totalWeight * unitPrice)); + } + break; + } + case 'total_price': { + // unit_price = total_price / total_weight + if (totalPrice > 0 && totalWeight > 0) { + setFieldValue('unit_price', roundPrice(totalPrice / totalWeight)); + } + break; + } + } +}; + +/** + * TELUR + PETI: Penjualan telur dalam satuan peti + * + * Formulas: + * - total_weight = (weight_per_convertion × total_peti) + sisa_berat + * - total_price = (price_per_convertion × total_peti) + price_sisa_berat + * - unit_price = total_price / total_weight (untuk BE) + * - avg_weight = total_weight / qty + */ +export const calculateTelurPeti = ( + field: string, + ctx: CalculationContext +): void => { + const { values, setFieldValue, hasSisaBerat } = ctx; + const pricePerConvertion = Number(values.price_per_convertion || 0); + const totalPeti = Number(values.total_peti || 0); + const weightPerConvertion = Number(values.weight_per_convertion || 0); + const sisaBerat = hasSisaBerat ? Number(values.sisa_berat || 0) : 0; + const priceSisaBerat = hasSisaBerat + ? Number(values.price_sisa_berat || 0) + : 0; + const qty = Number(values.qty || 0); + + // Helper untuk menghitung dan set unit_price = total_price / total_weight + const updateUnitPrice = (tp: number, tw: number) => { + if (tw > 0 && tp > 0) { + setFieldValue('unit_price', roundPrice(tp / tw)); + } + }; + + switch (field) { + case 'price_per_convertion': { + // Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat + if (pricePerConvertion > 0 && totalPeti > 0) { + const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat; + setFieldValue('total_price', roundPrice(totalPrice)); + // Recalculate unit_price = total_price / total_weight + const totalWeight = weightPerConvertion * totalPeti + sisaBerat; + updateUnitPrice(totalPrice, totalWeight); + } + break; + } + case 'total_peti': { + // Recalculate total_weight = (weight_per_convertion × total_peti) + sisa_berat + let totalWeight = 0; + if (weightPerConvertion > 0 && totalPeti > 0) { + totalWeight = weightPerConvertion * totalPeti + sisaBerat; + setFieldValue('total_weight', roundWeight(totalWeight)); + // Recalculate avg_weight = total_weight / qty + if (qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + } + // Recalculate total_price = (price_per_convertion × total_peti) + price_sisa_berat + if (pricePerConvertion > 0 && totalPeti > 0) { + const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat; + setFieldValue('total_price', roundPrice(totalPrice)); + // Recalculate unit_price = total_price / total_weight + updateUnitPrice(totalPrice, totalWeight); + } + break; + } + case 'price_sisa_berat': { + // Recalculate total_price + if (pricePerConvertion > 0 && totalPeti > 0) { + const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat; + setFieldValue('total_price', roundPrice(totalPrice)); + // Recalculate unit_price = total_price / total_weight + const totalWeight = weightPerConvertion * totalPeti + sisaBerat; + updateUnitPrice(totalPrice, totalWeight); + } + break; + } + case 'weight_per_convertion': { + // Recalculate total_weight = (weight_per_convertion × total_peti) + sisa_berat + if (weightPerConvertion > 0 && totalPeti > 0) { + const totalWeight = weightPerConvertion * totalPeti + sisaBerat; + setFieldValue('total_weight', roundWeight(totalWeight)); + // Recalculate avg_weight = total_weight / qty + if (qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + // Recalculate unit_price = total_price / total_weight + const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat; + updateUnitPrice(totalPrice, totalWeight); + } + break; + } + case 'sisa_berat': { + // Recalculate total_weight + if (weightPerConvertion > 0 && totalPeti > 0) { + const totalWeight = weightPerConvertion * totalPeti + sisaBerat; + setFieldValue('total_weight', roundWeight(totalWeight)); + // Recalculate avg_weight = total_weight / qty + if (qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + // Recalculate unit_price = total_price / total_weight + const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat; + updateUnitPrice(totalPrice, totalWeight); + } + break; + } + case 'total_price': { + const totalPrice = Number(values.total_price || 0); + // Reverse calculate price_per_convertion + if (totalPeti > 0 && totalPrice > priceSisaBerat) { + setFieldValue( + 'price_per_convertion', + roundPrice((totalPrice - priceSisaBerat) / totalPeti) + ); + } + // Update unit_price = total_price / total_weight + const totalWeight = weightPerConvertion * totalPeti + sisaBerat; + updateUnitPrice(totalPrice, totalWeight); + break; + } + } +}; + +/** + * TELUR + KG: Penjualan telur dalam satuan kilogram + * - Formula: total_price = total_weight × unit_price + * - avg_weight = total_weight / qty (calculated) + */ +export const calculateTelurKg = ( + field: string, + ctx: CalculationContext +): void => { + const { values, setFieldValue } = ctx; + const qty = Number(values.qty || 0); + const totalWeight = Number(values.total_weight || 0); + const totalPrice = Number(values.total_price || 0); + const pricePerConvertion = Number(values.price_per_convertion || 0); + + switch (field) { + case 'total_weight': + case 'qty': { + // avg_weight = total_weight / qty + if (totalWeight > 0 && qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + } + // total_price = total_weight × unit_price + if (pricePerConvertion > 0 && totalWeight > 0) { + setFieldValue( + 'total_price', + roundPrice(totalWeight * pricePerConvertion) + ); + setFieldValue('unit_price', pricePerConvertion); + } + break; + } + case 'price_per_convertion': { + // total_price = total_weight × price_per_convertion + if (pricePerConvertion > 0 && totalWeight > 0) { + setFieldValue( + 'total_price', + roundPrice(totalWeight * pricePerConvertion) + ); + setFieldValue('unit_price', pricePerConvertion); + } + break; + } + case 'total_price': { + // unit_price = total_price / total_weight + if (totalPrice > 0 && totalWeight > 0) { + setFieldValue('unit_price', roundPrice(totalPrice / totalWeight)); + setFieldValue( + 'price_per_convertion', + roundPrice(totalPrice / totalWeight) + ); + } + break; + } + } +}; + +/** + * TELUR + QTY Workaround: + * - User inputs: qty, avg_weight, price_per_qty (harga per butir) + * - FE calculates: + * - total_weight = avg_weight × qty + * - total_price = qty × price_per_qty + * - unit_price = total_price / total_weight (normalisasi untuk BE) + * - Kirim convertion_unit: "KG" karena BE tidak support "QTY" + * - BE akan hitung: total_price = total_weight × unit_price (hasil sama) + */ +export const calculateTelurQty = ( + field: string, + ctx: CalculationContext +): void => { + const { values, setFieldValue } = ctx; + const qty = Number(values.qty || 0); + const avgWeight = Number(values.avg_weight || 0); + const totalWeight = Number(values.total_weight || 0); + const pricePerQty = Number(values.price_per_qty || 0); + const totalPrice = Number(values.total_price || 0); + const unitPrice = Number(values.unit_price || 0); + + switch (field) { + case 'qty': + case 'avg_weight': { + // total_weight = avg_weight × qty + if (avgWeight > 0 && qty > 0) { + const tw = roundWeight(avgWeight * qty); + setFieldValue('total_weight', tw); + // total_price = qty × price_per_qty + if (pricePerQty > 0) { + const tp = roundPrice(qty * pricePerQty); + setFieldValue('total_price', tp); + // unit_price = total_price / total_weight (untuk BE) + if (tw > 0) { + setFieldValue('unit_price', roundPrice(tp / tw)); + } + } + } + break; + } + case 'total_weight': { + // avg_weight = total_weight / qty + if (totalWeight > 0 && qty > 0) { + setFieldValue('avg_weight', preciseWeight(totalWeight / qty)); + // Recalculate total_price jika ada unit_price + if (unitPrice > 0) { + setFieldValue('total_price', roundPrice(totalWeight * unitPrice)); + } + } + break; + } + case 'price_per_qty': { + // total_price = qty × price_per_qty + if (pricePerQty > 0 && qty > 0) { + const tp = roundPrice(qty * pricePerQty); + setFieldValue('total_price', tp); + // unit_price = total_price / total_weight (untuk BE) + if (totalWeight > 0) { + setFieldValue('unit_price', roundPrice(tp / totalWeight)); + } + } + break; + } + case 'total_price': { + // price_per_qty = total_price / qty + if (totalPrice > 0 && qty > 0) { + setFieldValue('price_per_qty', roundPrice(totalPrice / qty)); + // unit_price = total_price / total_weight (untuk BE) + if (totalWeight > 0) { + setFieldValue('unit_price', roundPrice(totalPrice / totalWeight)); + } + } + break; + } + case 'unit_price': { + // total_price = total_weight × unit_price + if (unitPrice > 0 && totalWeight > 0) { + setFieldValue('total_price', roundPrice(totalWeight * unitPrice)); + } + // price_per_qty = total_price / qty + if (totalPrice > 0 && qty > 0) { + setFieldValue('price_per_qty', roundPrice(totalPrice / qty)); + } + break; + } + } +}; + +// ============ Main Dispatcher ============ + +/** + * Handle field blur and dispatch to appropriate calculation handler + * based on marketing_type and convertion_unit + */ +export const handleMarketingCalculation = ( + field: string, + ctx: CalculationContext +): void => { + const { values } = ctx; + const marketingType = values.marketing_type?.value?.toLowerCase(); + const convertionUnit = values.convertion_unit?.value?.toLowerCase(); + + if (!marketingType) return; + + const qty = Number(values.qty || 0); + if (qty <= 0) return; + + switch (marketingType) { + case 'trading': + calculateTrading(field, ctx); + break; + case 'ayam_pullet': + calculateAyamPullet(field, ctx); + break; + case 'telur': + if (convertionUnit === 'peti') { + calculateTelurPeti(field, ctx); + } else if (convertionUnit === 'kg') { + calculateTelurKg(field, ctx); + } else { + // QTY mode - workaround dengan kirim KG ke BE + calculateTelurQty(field, ctx); + } + break; + case 'ayam': + default: + calculateAyam(field, ctx); + break; + } +}; diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 1102f99c..95a85b85 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -15,9 +15,7 @@ export class FinanceApiService extends BaseApiService< customer_ids?: string, // TODO: Uncomment when BE is ready // sales_id?: string, - // filter_by?: 'do_date', - sales_id?: string, - filter_by?: 'do_date' | undefined, + filter_by?: 'trans_date' | 'realization_date', start_date?: string, end_date?: string, page?: number, @@ -31,7 +29,7 @@ export class FinanceApiService extends BaseApiService< customer_ids: customer_ids, // TODO: Uncomment when BE is ready // sales_id: sales_id, - // filter_by: filter_by, + filter_by: filter_by, start_date: start_date, end_date: end_date, page: page, diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 7d0e390c..80a0b90b 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -36,6 +36,12 @@ export type BaseSalesOrder = { total_price: number; product_warehouse: ProductWarehouse; vehicle_number: string; + marketing_type: string; + convertion_unit: string; + total_peti: number; + weight_per_convertion: number; + /** Umur minggu untuk AYAM_PULLET */ + week?: number; }; export type BaseDeliveryOrder = { @@ -110,6 +116,12 @@ export type BaseCreateMarketingProductPayload = { qty: string | number | undefined; avg_weight: string | number | undefined; total_price: string | number | undefined; + marketing_type: string; + convertion_unit?: 'PETI' | 'KG'; + /** Berat per peti (kg), hanya untuk TELUR + PETI */ + weight_per_convertion?: number; + /** Umur minggu untuk AYAM_PULLET */ + week?: number; }; /** diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 9ad59f8b..60004ae0 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -1,10 +1,15 @@ -import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; +import { + BaseApproval, + BaseMetadata, + CreatedUser, +} from '@/types/api/api-general'; import { Supplier } from '@/types/api/master-data/supplier'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { Product } from '@/types/api/master-data/product'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { Area } from '@/types/api/master-data/area'; import { Location } from '@/types/api/master-data/location'; +import { Uom } from '@/types/api/master-data/uom'; export type PurchaseItemProduct = { id: number; @@ -12,14 +17,15 @@ export type PurchaseItemProduct = { flags?: string[]; ProductPrice?: number; SellingPrice?: number; - uom?: { - name: string; - }; + uom: Uom; product_category?: | { + id: number; name: string; + code: string; } | string; + suppliers?: Supplier[]; }; export type PurchaseItem = { @@ -69,6 +75,10 @@ export type BasePurchase = { warehouse?: Warehouse; items?: PurchaseItem[]; latest_approval?: BaseApproval; + requester_name?: string; + po_expedition?: string[]; + created_user?: CreatedUser; + products?: PurchaseItemProduct[]; }; export type Purchase = BaseMetadata & BasePurchase;