feat(FE): scenario egg sale with type conversion qty

This commit is contained in:
randy-ar
2026-02-05 02:39:10 +07:00
parent 09cd6395e6
commit dfd86a04e0
8 changed files with 563 additions and 123 deletions
@@ -186,15 +186,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.weeks?.value ?? undefined,
} as CreateSalesOrderProductPayload;
}),
} as CreateSalesOrderPayload)
@@ -375,6 +391,7 @@ const SalesOrderFormModal = ({
}
formik.setFieldValue('sales_order', updatedProducts);
console.log(formik.values);
nextButtonHandler();
},
[memoSalesOrder, nextButtonHandler]
@@ -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;
@@ -115,6 +115,14 @@ const SalesProductToFieldValues = (
qty: product.qty,
avg_weight: product.avg_weight,
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 = (
@@ -9,7 +9,7 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import { formatCurrency, formatDate } from '@/lib/helper';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import {
BaseDeliveryOrder,
BaseSalesOrder,
@@ -32,6 +32,8 @@ type SalesOrderProductSchemaType = {
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;
weeks?: {
value: number;
label: string;
@@ -89,10 +91,14 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
total_peti: Yup.number().nullable().optional().notRequired(),
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({
value: Yup.number().required('Minggu wajib diisi!'),
label: Yup.string().required('Minggu wajib diisi!'),
}).nullable(),
})
.nullable()
.optional()
.notRequired(),
});
export type SalesOrderProductFormValues = Yup.InferType<
@@ -30,9 +30,7 @@ import {
} from '@/config/constant';
import { Icon } from '@iconify/react';
import Dropdown from '@/components/Dropdown';
const roundWeight = (value: number) => Number(value.toFixed(2));
const roundPrice = (value: number) => Math.round(value);
import { handleMarketingCalculation } from '@/lib/marketing-calculation';
const SalesOrderProductForm = ({
initialValues,
@@ -75,6 +73,8 @@ const SalesOrderProductForm = ({
convertion_unit: initialValues?.convertion_unit || null,
marketing_type: initialValues?.marketing_type || null,
total_peti: initialValues?.total_peti ?? null,
weeks: initialValues?.weeks || null,
price_per_qty: initialValues?.price_per_qty ?? null,
},
validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => {
@@ -175,113 +175,11 @@ const SalesOrderProductForm = ({
const handleBlurField = (field: string) => {
setCurrentInput(field);
const qty = Number(formik.values.qty || 0);
const avgWeight = Number(formik.values.avg_weight || 0);
const totalWeight = Number(formik.values.total_weight || 0);
const unitPrice = Number(formik.values.unit_price || 0);
const totalPrice = Number(formik.values.total_price || 0);
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;
}
}
handleMarketingCalculation(field, {
values: formik.values,
setFieldValue: formik.setFieldValue,
hasSisaBerat,
});
};
// ===== 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() === 'kg') && (
<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 */}
{formik.values.convertion_unit?.value.toLowerCase() !== 'peti' &&
formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && (
<NumberInput
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'
value={formik.values.unit_price}
onChange={(e) => {
@@ -208,8 +208,12 @@ const SalesOrderProductTable = ({
</tr>
<tr>
<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'>
{item.product_warehouse?.label}
{item.marketing_type?.label}
</td>
</tr>
<tr>
@@ -218,6 +222,20 @@ const SalesOrderProductTable = ({
{item.product_warehouse?.label}
</td>
</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>
<td className='text-sm px-4 py-3'>Total Bobot</td>
<td className='text-sm px-4 py-3'>
@@ -238,13 +256,17 @@ const SalesOrderProductTable = ({
</td>
</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'>
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
</td>
</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'>
{formatCurrency(parseFloat(item.unit_price as string))}
</td>