/** * 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; } };