mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
510 lines
16 KiB
TypeScript
510 lines
16 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|
||
};
|