mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
feat(FE): calculation penjualan telur + peti
This commit is contained in:
@@ -14,8 +14,6 @@ import {
|
||||
DeliveryProductToFieldValues,
|
||||
mergeSOwithDO,
|
||||
SalesProductToFieldValues,
|
||||
} from '@/components/pages/marketing/form/MarketingForm';
|
||||
import {
|
||||
DeliveryOrderFormValues,
|
||||
DeliveryOrderSchema,
|
||||
getFilledMarketingFormInitialValues,
|
||||
@@ -210,7 +208,6 @@ const SalesOrderFormModal = ({
|
||||
convertion_unit: normalizedConvertionUnit,
|
||||
weight_per_convertion:
|
||||
product.weight_per_convertion ?? undefined,
|
||||
week: product.weeks?.value ?? undefined,
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
|
||||
@@ -94,7 +94,7 @@ export type SalesOrderFormValues = Yup.InferType<typeof SalesOrderSchema>;
|
||||
export type DeliveryOrderFormValues = Yup.InferType<typeof DeliveryOrderSchema>;
|
||||
|
||||
// ================ Helper Function ================
|
||||
const SalesProductToFieldValues = (
|
||||
export const SalesProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
@@ -123,9 +123,11 @@ const SalesProductToFieldValues = (
|
||||
value: product.convertion_unit,
|
||||
label: formatTitleCase(product.convertion_unit),
|
||||
},
|
||||
total_peti: product.total_peti,
|
||||
weight_per_convertion: product.weight_per_convertion,
|
||||
};
|
||||
};
|
||||
const DeliveryProductToFieldValues = (
|
||||
export const DeliveryProductToFieldValues = (
|
||||
salesOrders: BaseSalesOrder[],
|
||||
delivery: BaseDeliveryOrder
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
@@ -221,3 +223,11 @@ export const getFilledMarketingFormInitialValues = (
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPricePerConvertion = (
|
||||
totalPrice: number,
|
||||
weightPerConvertion: number,
|
||||
totalPeti: number
|
||||
) => {
|
||||
return totalPrice / (weightPerConvertion * totalPeti);
|
||||
};
|
||||
|
||||
@@ -34,10 +34,6 @@ type SalesOrderProductSchemaType = {
|
||||
price_sisa_berat?: number | null | undefined;
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
weeks?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
@@ -92,13 +88,6 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
||||
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()
|
||||
.optional()
|
||||
.notRequired(),
|
||||
});
|
||||
|
||||
export type SalesOrderProductFormValues = Yup.InferType<
|
||||
|
||||
+72
-15
@@ -5,7 +5,7 @@ import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import { RefObject, useMemo, useState } from 'react';
|
||||
import { RefObject, useEffect, useMemo, useState } from 'react';
|
||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { WarehouseApi } from '@/services/api/master-data';
|
||||
@@ -49,7 +49,33 @@ const SalesOrderProductForm = ({
|
||||
const [currentInput, setCurrentInput] = useState<string>('');
|
||||
const [selectedProductWarehouse, setSelectedProductWarehouse] =
|
||||
useState<ProductWarehouse | null>(null);
|
||||
const [hasSisaBerat, setHasSisaBerat] = useState<boolean>(false);
|
||||
|
||||
// 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 =
|
||||
Number(initialValues?.total_price) -
|
||||
initialPricePerConvertion * Number(initialValues?.total_peti);
|
||||
|
||||
const [hasSisaBerat, setHasSisaBerat] = useState<boolean>(
|
||||
initialSisaBerat > 0
|
||||
);
|
||||
|
||||
// ============ Formik ============
|
||||
const formik = useFormik<SalesOrderProductFormValues>({
|
||||
@@ -70,11 +96,13 @@ const SalesOrderProductForm = ({
|
||||
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,
|
||||
weeks: initialValues?.weeks || null,
|
||||
price_per_qty: initialValues?.price_per_qty ?? null,
|
||||
sisa_berat: initialSisaBerat,
|
||||
price_sisa_berat: initialPriceSisaBerat,
|
||||
},
|
||||
validationSchema: SalesOrderProductSchema,
|
||||
onSubmit: async (values) => {
|
||||
@@ -168,6 +196,15 @@ const SalesOrderProductForm = ({
|
||||
qty: '',
|
||||
avg_weight: '',
|
||||
total_price: '',
|
||||
total_peti: null,
|
||||
price_per_qty: null,
|
||||
price_sisa_berat: null,
|
||||
sisa_berat: null,
|
||||
convertion_unit: null,
|
||||
marketing_type: null,
|
||||
weight_per_convertion: null,
|
||||
price_per_convertion: null,
|
||||
uom: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -182,6 +219,28 @@ const SalesOrderProductForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
};
|
||||
|
||||
// ===== Formik Error List =====
|
||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||
formik,
|
||||
@@ -314,7 +373,7 @@ const SalesOrderProductForm = ({
|
||||
/>
|
||||
)}
|
||||
{formik.values.convertion_unit &&
|
||||
formik.values.convertion_unit.value === 'peti' && (
|
||||
formik.values.convertion_unit.value.toLowerCase() === 'peti' && (
|
||||
<div className='flex flex-col'>
|
||||
<label className='font-semibold text-xs py-2 leading-5'>
|
||||
Tipe Konversi <span className='text-error'>*</span>
|
||||
@@ -387,32 +446,33 @@ const SalesOrderProductForm = ({
|
||||
);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('weight_per_convertion')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Konversi Satuan Weeks Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
{/* {formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeeks}
|
||||
value={formik.values.weeks}
|
||||
value={formik.values.weeks || undefined}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('weeks', val);
|
||||
}}
|
||||
placeholder='Pilih Weeks'
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* Total Peti */}
|
||||
{formik.values.convertion_unit?.value === 'peti' && (
|
||||
{formik.values.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
<NumberInput
|
||||
required
|
||||
label='Total Peti'
|
||||
name='total_pet'
|
||||
name='total_peti'
|
||||
value={formik.values.total_peti ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
@@ -429,7 +489,7 @@ const SalesOrderProductForm = ({
|
||||
<span className='text-sm text-base-content/50'>Kg</span>
|
||||
</div>
|
||||
}
|
||||
bottomLabel={`1 ${formik.values.convertion_unit?.value} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||
bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -586,12 +646,9 @@ const SalesOrderProductForm = ({
|
||||
<div className='py-2 gap-3 flex items-center'>
|
||||
<input
|
||||
type='checkbox'
|
||||
name='sisa_berat'
|
||||
name='sisa_berat_toggle'
|
||||
checked={hasSisaBerat}
|
||||
onChange={() => {
|
||||
setHasSisaBerat(!hasSisaBerat);
|
||||
}}
|
||||
onBlur={() => handleBlurField('sisa_berat')}
|
||||
onChange={() => 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'
|
||||
/>
|
||||
<label className='text-sm text-base-content/50'>
|
||||
|
||||
@@ -232,7 +232,7 @@ const SalesOrderProductTable = ({
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.convertion_unit?.label}
|
||||
{item.total_peti} {item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -199,9 +199,12 @@ export const calculateAyam = (field: string, ctx: CalculationContext): void => {
|
||||
|
||||
/**
|
||||
* TELUR + PETI: Penjualan telur dalam satuan peti
|
||||
* - Formula: total_price = (price_per_convertion × total_peti) + price_sisa_berat
|
||||
*
|
||||
* Formulas:
|
||||
* - total_weight = (weight_per_convertion × total_peti) + sisa_berat
|
||||
* - Payload: unit_price = total_price / qty (normalisasi untuk BE)
|
||||
* - 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,
|
||||
@@ -217,27 +220,83 @@ export const calculateTelurPeti = (
|
||||
: 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':
|
||||
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));
|
||||
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 '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));
|
||||
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;
|
||||
}
|
||||
@@ -250,10 +309,9 @@ export const calculateTelurPeti = (
|
||||
roundPrice((totalPrice - priceSisaBerat) / totalPeti)
|
||||
);
|
||||
}
|
||||
// Update unit_price (normalized for BE)
|
||||
if (qty > 0) {
|
||||
setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||
}
|
||||
// Update unit_price = total_price / total_weight
|
||||
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
|
||||
updateUnitPrice(totalPrice, totalWeight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -38,6 +38,8 @@ export type BaseSalesOrder = {
|
||||
vehicle_number: string;
|
||||
marketing_type: string;
|
||||
convertion_unit: string;
|
||||
total_peti: number;
|
||||
weight_per_convertion: number;
|
||||
};
|
||||
|
||||
export type BaseDeliveryOrder = {
|
||||
|
||||
Reference in New Issue
Block a user