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
+449
View File
@@ -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;
}
};