mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE): scenario egg sale with type conversion qty
This commit is contained in:
@@ -186,15 +186,31 @@ const SalesOrderFormModal = ({
|
|||||||
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
|
date: formatDate(values.so_date as string, 'yyyy-MM-DD'),
|
||||||
notes: values.notes as string,
|
notes: values.notes as string,
|
||||||
marketing_products: values.sales_order.map((product) => {
|
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 {
|
return {
|
||||||
vehicle_number: product.vehicle_number as string,
|
vehicle_number: product.vehicle_number as string,
|
||||||
kandang_id: product.kandang_id as number,
|
kandang_id: product.kandang_id as number,
|
||||||
product_warehouse_id: product.product_warehouse_id as number,
|
product_warehouse_id: product.product_warehouse_id as number,
|
||||||
unit_price: parseFloat(product.unit_price as string),
|
unit_price: parseFloat(String(product.unit_price || 0)),
|
||||||
total_weight: parseFloat(product.total_weight as string),
|
total_weight: parseFloat(String(product.total_weight || 0)),
|
||||||
qty: parseFloat(product.qty as string),
|
qty: parseFloat(String(product.qty || 0)),
|
||||||
avg_weight: parseFloat(product.avg_weight as string),
|
avg_weight: parseFloat(String(product.avg_weight || 0)),
|
||||||
total_price: parseFloat(product.total_price as string),
|
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.weeks?.value ?? undefined,
|
||||||
} as CreateSalesOrderProductPayload;
|
} as CreateSalesOrderProductPayload;
|
||||||
}),
|
}),
|
||||||
} as CreateSalesOrderPayload)
|
} as CreateSalesOrderPayload)
|
||||||
@@ -375,6 +391,7 @@ const SalesOrderFormModal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
formik.setFieldValue('sales_order', updatedProducts);
|
formik.setFieldValue('sales_order', updatedProducts);
|
||||||
|
console.log(formik.values);
|
||||||
nextButtonHandler();
|
nextButtonHandler();
|
||||||
},
|
},
|
||||||
[memoSalesOrder, nextButtonHandler]
|
[memoSalesOrder, nextButtonHandler]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
Marketing,
|
Marketing,
|
||||||
} from '@/types/api/marketing/marketing';
|
} from '@/types/api/marketing/marketing';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
|
|
||||||
type MarketingSchemaType = {
|
type MarketingSchemaType = {
|
||||||
customer_id: number | undefined;
|
customer_id: number | undefined;
|
||||||
@@ -115,6 +115,14 @@ const SalesProductToFieldValues = (
|
|||||||
qty: product.qty,
|
qty: product.qty,
|
||||||
avg_weight: product.avg_weight,
|
avg_weight: product.avg_weight,
|
||||||
total_price: product.total_price,
|
total_price: product.total_price,
|
||||||
|
marketing_type: {
|
||||||
|
value: product.marketing_type,
|
||||||
|
label: formatTitleCase(product.marketing_type),
|
||||||
|
},
|
||||||
|
convertion_unit: {
|
||||||
|
value: product.convertion_unit,
|
||||||
|
label: formatTitleCase(product.convertion_unit),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const DeliveryProductToFieldValues = (
|
const DeliveryProductToFieldValues = (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import SelectInput, {
|
|||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
BaseDeliveryOrder,
|
BaseDeliveryOrder,
|
||||||
BaseSalesOrder,
|
BaseSalesOrder,
|
||||||
|
|||||||
+7
-1
@@ -32,6 +32,8 @@ type SalesOrderProductSchemaType = {
|
|||||||
total_peti?: number | null | undefined;
|
total_peti?: number | null | undefined;
|
||||||
sisa_berat?: number | null | undefined;
|
sisa_berat?: number | null | undefined;
|
||||||
price_sisa_berat?: number | null | undefined;
|
price_sisa_berat?: number | null | undefined;
|
||||||
|
/** Harga per butir telur untuk TELUR + QTY */
|
||||||
|
price_per_qty?: number | null | undefined;
|
||||||
weeks?: {
|
weeks?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -89,10 +91,14 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
|||||||
total_peti: Yup.number().nullable().optional().notRequired(),
|
total_peti: Yup.number().nullable().optional().notRequired(),
|
||||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||||
|
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||||
weeks: Yup.object({
|
weeks: Yup.object({
|
||||||
value: Yup.number().required('Minggu wajib diisi!'),
|
value: Yup.number().required('Minggu wajib diisi!'),
|
||||||
label: Yup.string().required('Minggu wajib diisi!'),
|
label: Yup.string().required('Minggu wajib diisi!'),
|
||||||
}).nullable(),
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
.notRequired(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SalesOrderProductFormValues = Yup.InferType<
|
export type SalesOrderProductFormValues = Yup.InferType<
|
||||||
|
|||||||
+32
-112
@@ -30,9 +30,7 @@ import {
|
|||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
|
||||||
const roundWeight = (value: number) => Number(value.toFixed(2));
|
|
||||||
const roundPrice = (value: number) => Math.round(value);
|
|
||||||
|
|
||||||
const SalesOrderProductForm = ({
|
const SalesOrderProductForm = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -75,6 +73,8 @@ const SalesOrderProductForm = ({
|
|||||||
convertion_unit: initialValues?.convertion_unit || null,
|
convertion_unit: initialValues?.convertion_unit || null,
|
||||||
marketing_type: initialValues?.marketing_type || null,
|
marketing_type: initialValues?.marketing_type || null,
|
||||||
total_peti: initialValues?.total_peti ?? null,
|
total_peti: initialValues?.total_peti ?? null,
|
||||||
|
weeks: initialValues?.weeks || null,
|
||||||
|
price_per_qty: initialValues?.price_per_qty ?? null,
|
||||||
},
|
},
|
||||||
validationSchema: SalesOrderProductSchema,
|
validationSchema: SalesOrderProductSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
@@ -175,113 +175,11 @@ const SalesOrderProductForm = ({
|
|||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
|
|
||||||
const qty = Number(formik.values.qty || 0);
|
handleMarketingCalculation(field, {
|
||||||
const avgWeight = Number(formik.values.avg_weight || 0);
|
values: formik.values,
|
||||||
const totalWeight = Number(formik.values.total_weight || 0);
|
setFieldValue: formik.setFieldValue,
|
||||||
const unitPrice = Number(formik.values.unit_price || 0);
|
hasSisaBerat,
|
||||||
const totalPrice = Number(formik.values.total_price || 0);
|
});
|
||||||
|
|
||||||
if (qty <= 0) return;
|
|
||||||
|
|
||||||
// Cek apakah produk memiliki flag OVK atau PAKAN
|
|
||||||
const productFlags = selectedProductWarehouse?.product?.flags || [];
|
|
||||||
const isOvkOrPakan =
|
|
||||||
productFlags.includes('OVK') || productFlags.includes('PAKAN');
|
|
||||||
|
|
||||||
switch (field) {
|
|
||||||
// ===== SOURCE FIELDS =====
|
|
||||||
case 'qty': {
|
|
||||||
if (avgWeight > 0) {
|
|
||||||
const tw = roundWeight(qty * avgWeight);
|
|
||||||
formik.setFieldValue('total_weight', tw);
|
|
||||||
|
|
||||||
// Hitung total_price berdasarkan flag produk
|
|
||||||
if (unitPrice > 0) {
|
|
||||||
if (isOvkOrPakan) {
|
|
||||||
// Untuk OVK/PAKAN: total_price = qty × unit_price
|
|
||||||
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
|
||||||
} else {
|
|
||||||
// Untuk produk lain: total_price = unit_price × total_weight
|
|
||||||
formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'avg_weight': {
|
|
||||||
if (avgWeight > 0) {
|
|
||||||
const tw = roundWeight(qty * avgWeight);
|
|
||||||
formik.setFieldValue('total_weight', tw);
|
|
||||||
|
|
||||||
// Hitung total_price berdasarkan flag produk
|
|
||||||
if (unitPrice > 0) {
|
|
||||||
if (isOvkOrPakan) {
|
|
||||||
// Untuk OVK/PAKAN: total_price = qty × unit_price
|
|
||||||
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
|
||||||
} else {
|
|
||||||
// Untuk produk lain: total_price = unit_price × total_weight
|
|
||||||
formik.setFieldValue('total_price', roundPrice(unitPrice * tw));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'unit_price': {
|
|
||||||
if (unitPrice > 0) {
|
|
||||||
if (isOvkOrPakan) {
|
|
||||||
// Untuk OVK/PAKAN: total_price = qty × unit_price
|
|
||||||
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
|
||||||
} else if (totalWeight > 0) {
|
|
||||||
// Untuk produk lain: total_price = unit_price × total_weight
|
|
||||||
formik.setFieldValue(
|
|
||||||
'total_price',
|
|
||||||
roundPrice(unitPrice * totalWeight)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== TOTAL EDITABLE =====
|
|
||||||
case 'total_weight': {
|
|
||||||
if (totalWeight > 0) {
|
|
||||||
formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
|
|
||||||
|
|
||||||
// Hitung ulang total_price berdasarkan flag produk
|
|
||||||
if (unitPrice > 0) {
|
|
||||||
if (isOvkOrPakan) {
|
|
||||||
// Untuk OVK/PAKAN: total_price = qty × unit_price
|
|
||||||
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
|
||||||
} else {
|
|
||||||
// Untuk produk lain: total_price = unit_price × total_weight
|
|
||||||
formik.setFieldValue(
|
|
||||||
'total_price',
|
|
||||||
roundPrice(unitPrice * totalWeight)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'total_price': {
|
|
||||||
if (totalPrice > 0) {
|
|
||||||
if (isOvkOrPakan && qty > 0) {
|
|
||||||
// Untuk OVK/PAKAN: unit_price = total_price / qty
|
|
||||||
formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
|
||||||
} else if (totalWeight > 0) {
|
|
||||||
// Untuk produk lain: unit_price = total_price / total_weight
|
|
||||||
formik.setFieldValue(
|
|
||||||
'unit_price',
|
|
||||||
roundPrice(totalPrice / totalWeight)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
@@ -617,7 +515,7 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Harga per convertion unit */}
|
{/* Harga per convertion unit (PETI / KG) */}
|
||||||
{(formik.values.convertion_unit?.value.toLowerCase() === 'peti' ||
|
{(formik.values.convertion_unit?.value.toLowerCase() === 'peti' ||
|
||||||
formik.values.convertion_unit?.value.toLowerCase() === 'kg') && (
|
formik.values.convertion_unit?.value.toLowerCase() === 'kg') && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -639,12 +537,34 @@ const SalesOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Harga per butir untuk TELUR + QTY */}
|
||||||
|
{formik.values.marketing_type?.value.toLowerCase() === 'telur' &&
|
||||||
|
formik.values.convertion_unit?.value.toLowerCase() === 'qty' && (
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label='Harga / Butir (Rp)'
|
||||||
|
name='price_per_qty'
|
||||||
|
value={formik.values.price_per_qty ?? undefined}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 */}
|
{/* Harga Satuan per Uom Produk Warehouse */}
|
||||||
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
|
||||||
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label={`Harga / ${selectedProductWarehouse?.product?.uom?.name ?? 'Produk'} (Rp)`}
|
label={`Harga / ${formik.values.convertion_unit?.label !== 'qty' ? 'Kg' : (selectedProductWarehouse?.product?.uom?.name ?? 'Produk')} (Rp)`}
|
||||||
name='unit_price'
|
name='unit_price'
|
||||||
value={formik.values.unit_price}
|
value={formik.values.unit_price}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -208,8 +208,12 @@ const SalesOrderProductTable = ({
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||||
|
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{item.product_warehouse?.label}
|
{item.marketing_type?.label}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -218,6 +222,20 @@ const SalesOrderProductTable = ({
|
|||||||
{item.product_warehouse?.label}
|
{item.product_warehouse?.label}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.convertion_unit?.label}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||||
|
<tr>
|
||||||
|
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||||
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.convertion_unit?.label}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
@@ -238,13 +256,17 @@ const SalesOrderProductTable = ({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Qty</td>
|
<td className='text-sm px-4 py-3'>
|
||||||
|
{item.marketing_type?.value === 'telur'
|
||||||
|
? 'Total Butir Telur'
|
||||||
|
: 'Qty'}
|
||||||
|
</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||||
<td className='text-sm px-4 py-3'>
|
<td className='text-sm px-4 py-3'>
|
||||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
weeks?: { 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 weeks = Number(values.weeks?.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 'weeks':
|
||||||
|
case 'qty': {
|
||||||
|
// total_price = unit_price × weeks × qty
|
||||||
|
if (unitPrice > 0 && weeks > 0 && qty > 0) {
|
||||||
|
setFieldValue('total_price', roundPrice(unitPrice * weeks * 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 / (weeks × qty)
|
||||||
|
if (totalPrice > 0 && weeks > 0 && qty > 0) {
|
||||||
|
setFieldValue('unit_price', roundPrice(totalPrice / (weeks * 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
|
||||||
|
* - Formula: total_price = (price_per_convertion × total_peti) + price_sisa_berat
|
||||||
|
* - total_weight = (weight_per_convertion × total_peti) + sisa_berat
|
||||||
|
* - Payload: unit_price = total_price / qty (normalisasi untuk BE)
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
case 'price_per_convertion':
|
||||||
|
case 'total_peti':
|
||||||
|
case 'price_sisa_berat': {
|
||||||
|
// Recalculate total_price
|
||||||
|
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
|
||||||
|
setFieldValue('total_price', roundPrice(totalPrice));
|
||||||
|
// Recalculate unit_price (normalized for BE)
|
||||||
|
if (qty > 0) {
|
||||||
|
setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'weight_per_convertion':
|
||||||
|
case 'sisa_berat': {
|
||||||
|
// Recalculate total_weight
|
||||||
|
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
|
||||||
|
setFieldValue('total_weight', roundWeight(totalWeight));
|
||||||
|
// Recalculate avg_weight
|
||||||
|
if (qty > 0) {
|
||||||
|
setFieldValue('avg_weight', preciseWeight(totalWeight / qty));
|
||||||
|
}
|
||||||
|
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 (normalized for BE)
|
||||||
|
if (qty > 0) {
|
||||||
|
setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
+18
@@ -36,6 +36,8 @@ export type BaseSalesOrder = {
|
|||||||
total_price: number;
|
total_price: number;
|
||||||
product_warehouse: ProductWarehouse;
|
product_warehouse: ProductWarehouse;
|
||||||
vehicle_number: string;
|
vehicle_number: string;
|
||||||
|
marketing_type: string;
|
||||||
|
convertion_unit: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BaseDeliveryOrder = {
|
export type BaseDeliveryOrder = {
|
||||||
@@ -111,6 +113,22 @@ export type BaseCreateMarketingProductPayload = {
|
|||||||
avg_weight: string | number | undefined;
|
avg_weight: string | number | undefined;
|
||||||
total_price: string | number | undefined;
|
total_price: string | number | undefined;
|
||||||
marketing_type: string;
|
marketing_type: string;
|
||||||
|
/**
|
||||||
|
* Tipe konversi untuk TELUR
|
||||||
|
* - "PETI": Penjualan telur dalam satuan peti
|
||||||
|
* - "KG": Penjualan telur dalam satuan kilogram
|
||||||
|
*
|
||||||
|
* Note: Untuk mode "QTY" di FE, tetap kirim "KG" ke BE dengan unit_price yang dinormalisasi
|
||||||
|
* karena BE tidak support convertion_unit "QTY". Workaround:
|
||||||
|
* - FE hitung: total_price = qty × price_per_qty
|
||||||
|
* - FE normalisasi: unit_price = total_price / total_weight
|
||||||
|
* - BE akan hitung: total_price = total_weight × unit_price (hasil sama)
|
||||||
|
*/
|
||||||
|
convertion_unit?: 'PETI' | 'KG';
|
||||||
|
/** Berat per peti (kg), hanya untuk TELUR + PETI */
|
||||||
|
weight_per_convertion?: number;
|
||||||
|
/** Umur minggu untuk AYAM_PULLET */
|
||||||
|
week?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user