Files
lti-web-client/src/lib/marketing-calculation.ts
T
2026-04-10 15:07:39 +07:00

510 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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?: number | 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', unitPrice * qty);
}
break;
}
case 'total_price': {
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', 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 || 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', 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', 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', 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', totalWeight * unitPrice);
}
break;
}
case 'unit_price': {
// total_price = total_weight × unit_price
if (unitPrice > 0 && totalWeight > 0) {
setFieldValue('total_price', totalWeight * unitPrice);
}
break;
}
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', 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) {
const unitPrice = tp / tw;
setFieldValue('unit_price', unitPrice);
}
};
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', 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', totalPrice);
// Recalculate unit_price = total_price / totalWeight
updateUnitPrice(totalPrice, totalWeight);
}
break;
}
case 'price_sisa_berat': {
// Recalculate total_price
if (pricePerConvertion > 0 && totalPeti > 0) {
const totalPrice = pricePerConvertion * totalPeti + priceSisaBerat;
setFieldValue('total_price', 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',
(totalPrice - priceSisaBerat) / totalPeti
);
}
// Update unit_price = total_price / total_weight
const totalWeight = weightPerConvertion * totalPeti + sisaBerat;
updateUnitPrice(totalPrice, totalWeight);
break;
}
case 'qty':
// Recalculate avg_weight = total_weight / qty
if (qty > 0 && values.total_weight) {
setFieldValue(
'avg_weight',
preciseWeight(Number(values.total_weight) / 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', 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', totalWeight * pricePerConvertion);
setFieldValue('unit_price', pricePerConvertion);
}
break;
}
case 'total_price': {
// unit_price = total_price / total_weight
if (totalPrice > 0 && totalWeight > 0) {
setFieldValue('unit_price', totalPrice / totalWeight);
setFieldValue('price_per_convertion', totalPrice / totalWeight);
}
break;
}
}
};
/**
* TELUR + QTY Workaround:
* - User inputs: qty, avg_weight, unit_price (harga per butir)
* - FE calculates:
* - total_weight = avg_weight × qty
* - total_price = qty × unit_price
* - price_per_qty = total_price / total_weight (harga per kg)
*/
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 × unit_price
if (unitPrice > 0) {
const tp = qty * unitPrice;
setFieldValue('total_price', tp);
// price_per_qty = total_price / total_weight
if (tw > 0) {
setFieldValue('price_per_qty', 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 harga per butir
if (unitPrice > 0) {
setFieldValue('total_price', qty * unitPrice);
}
}
break;
}
case 'price_per_qty': {
// total_price = total_weight × price_per_qty
if (pricePerQty > 0 && totalWeight > 0) {
const tp = totalWeight * pricePerQty;
setFieldValue('total_price', tp);
// unit_price = total_price / qty
if (qty > 0) {
setFieldValue('unit_price', tp / qty);
}
}
break;
}
case 'total_price': {
// unit_price = total_price / qty
if (totalPrice > 0 && qty > 0) {
setFieldValue('unit_price', totalPrice / qty);
// price_per_qty = total_price / total_weight
if (totalWeight > 0) {
setFieldValue('price_per_qty', totalPrice / totalWeight);
}
}
break;
}
case 'unit_price': {
// total_price = qty × unit_price
const newTotalPrice = qty * unitPrice;
if (unitPrice > 0 && qty > 0) {
setFieldValue('total_price', newTotalPrice);
}
// price_per_qty = total_price / total_weight
if (newTotalPrice > 0 && totalWeight > 0) {
setFieldValue('price_per_qty', newTotalPrice / totalWeight);
}
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;
}
};