From dfd86a04e04c580fba1eb4ac9b676be8e9813aaa Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 5 Feb 2026 02:39:10 +0700 Subject: [PATCH] feat(FE): scenario egg sale with type conversion qty --- .../pages/marketing/SalesOrderFormModal.tsx | 27 +- .../marketing/form/MarketingForm.schema.ts | 10 +- .../pages/marketing/form/MarketingForm.tsx | 2 +- .../sales-order/SalesOrderProduct.schema.ts | 8 +- .../sales-order/SalesOrderProductForm.tsx | 144 ++---- .../table-view/SalesOrderProductTable.tsx | 28 +- src/lib/marketing-calculation.ts | 449 ++++++++++++++++++ src/types/api/marketing/marketing.d.ts | 18 + 8 files changed, 563 insertions(+), 123 deletions(-) create mode 100644 src/lib/marketing-calculation.ts diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index fd7d8b7b..5c08810e 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -186,15 +186,31 @@ const SalesOrderFormModal = ({ date: formatDate(values.so_date as string, 'yyyy-MM-DD'), notes: values.notes as string, marketing_products: values.sales_order.map((product) => { + // Workaround untuk TELUR + QTY: kirim "KG" karena BE tidak support "QTY" + const convertionUnitValue = + product.convertion_unit?.value?.toUpperCase(); + const normalizedConvertionUnit = + product.marketing_type?.value?.toLowerCase() === 'telur' + ? convertionUnitValue === 'PETI' + ? 'PETI' + : 'KG' // termasuk "QTY" dan "KG" + : undefined; + return { vehicle_number: product.vehicle_number as string, kandang_id: product.kandang_id as number, product_warehouse_id: product.product_warehouse_id as number, - unit_price: parseFloat(product.unit_price as string), - total_weight: parseFloat(product.total_weight as string), - qty: parseFloat(product.qty as string), - avg_weight: parseFloat(product.avg_weight as string), - total_price: parseFloat(product.total_price as string), + unit_price: parseFloat(String(product.unit_price || 0)), + total_weight: parseFloat(String(product.total_weight || 0)), + qty: parseFloat(String(product.qty || 0)), + avg_weight: parseFloat(String(product.avg_weight || 0)), + total_price: parseFloat(String(product.total_price || 0)), + marketing_type: + product.marketing_type?.value?.toUpperCase() || '', + convertion_unit: normalizedConvertionUnit, + weight_per_convertion: + product.weight_per_convertion ?? undefined, + week: product.weeks?.value ?? undefined, } as CreateSalesOrderProductPayload; }), } as CreateSalesOrderPayload) @@ -375,6 +391,7 @@ const SalesOrderFormModal = ({ } formik.setFieldValue('sales_order', updatedProducts); + console.log(formik.values); nextButtonHandler(); }, [memoSalesOrder, nextButtonHandler] diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 4a77ebd5..188fbf20 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -12,7 +12,7 @@ import { BaseSalesOrder, Marketing, } from '@/types/api/marketing/marketing'; -import { formatDate } from '@/lib/helper'; +import { formatDate, formatTitleCase } from '@/lib/helper'; type MarketingSchemaType = { customer_id: number | undefined; @@ -115,6 +115,14 @@ const SalesProductToFieldValues = ( qty: product.qty, avg_weight: product.avg_weight, total_price: product.total_price, + marketing_type: { + value: product.marketing_type, + label: formatTitleCase(product.marketing_type), + }, + convertion_unit: { + value: product.convertion_unit, + label: formatTitleCase(product.convertion_unit), + }, }; }; const DeliveryProductToFieldValues = ( diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 1f866350..d2d26bc2 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -9,7 +9,7 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; -import { formatCurrency, formatDate } from '@/lib/helper'; +import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { BaseDeliveryOrder, BaseSalesOrder, diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index d71beb42..33028e69 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -32,6 +32,8 @@ type SalesOrderProductSchemaType = { total_peti?: number | null | undefined; sisa_berat?: number | null | undefined; price_sisa_berat?: number | null | undefined; + /** Harga per butir telur untuk TELUR + QTY */ + price_per_qty?: number | null | undefined; weeks?: { value: number; label: string; @@ -89,10 +91,14 @@ export const SalesOrderProductSchema: Yup.ObjectSchema Number(value.toFixed(2)); -const roundPrice = (value: number) => Math.round(value); +import { handleMarketingCalculation } from '@/lib/marketing-calculation'; const SalesOrderProductForm = ({ initialValues, @@ -75,6 +73,8 @@ const SalesOrderProductForm = ({ 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, }, validationSchema: SalesOrderProductSchema, onSubmit: async (values) => { @@ -175,113 +175,11 @@ const SalesOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const qty = Number(formik.values.qty || 0); - const avgWeight = Number(formik.values.avg_weight || 0); - const totalWeight = Number(formik.values.total_weight || 0); - const unitPrice = Number(formik.values.unit_price || 0); - const totalPrice = Number(formik.values.total_price || 0); - - if (qty <= 0) return; - - // Cek apakah produk memiliki flag OVK atau PAKAN - const productFlags = selectedProductWarehouse?.product?.flags || []; - const isOvkOrPakan = - productFlags.includes('OVK') || productFlags.includes('PAKAN'); - - switch (field) { - // ===== SOURCE FIELDS ===== - case 'qty': { - if (avgWeight > 0) { - const tw = roundWeight(qty * avgWeight); - formik.setFieldValue('total_weight', tw); - - // Hitung total_price berdasarkan flag produk - if (unitPrice > 0) { - if (isOvkOrPakan) { - // Untuk OVK/PAKAN: total_price = qty × unit_price - formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); - } else { - // Untuk produk lain: total_price = unit_price × total_weight - formik.setFieldValue('total_price', roundPrice(unitPrice * tw)); - } - } - } - break; - } - - case 'avg_weight': { - if (avgWeight > 0) { - const tw = roundWeight(qty * avgWeight); - formik.setFieldValue('total_weight', tw); - - // Hitung total_price berdasarkan flag produk - if (unitPrice > 0) { - if (isOvkOrPakan) { - // Untuk OVK/PAKAN: total_price = qty × unit_price - formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); - } else { - // Untuk produk lain: total_price = unit_price × total_weight - formik.setFieldValue('total_price', roundPrice(unitPrice * tw)); - } - } - } - break; - } - - case 'unit_price': { - if (unitPrice > 0) { - if (isOvkOrPakan) { - // Untuk OVK/PAKAN: total_price = qty × unit_price - formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); - } else if (totalWeight > 0) { - // Untuk produk lain: total_price = unit_price × total_weight - formik.setFieldValue( - 'total_price', - roundPrice(unitPrice * totalWeight) - ); - } - } - break; - } - - // ===== TOTAL EDITABLE ===== - case 'total_weight': { - if (totalWeight > 0) { - formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); - - // Hitung ulang total_price berdasarkan flag produk - if (unitPrice > 0) { - if (isOvkOrPakan) { - // Untuk OVK/PAKAN: total_price = qty × unit_price - formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); - } else { - // Untuk produk lain: total_price = unit_price × total_weight - formik.setFieldValue( - 'total_price', - roundPrice(unitPrice * totalWeight) - ); - } - } - } - break; - } - - case 'total_price': { - if (totalPrice > 0) { - if (isOvkOrPakan && qty > 0) { - // Untuk OVK/PAKAN: unit_price = total_price / qty - formik.setFieldValue('unit_price', roundPrice(totalPrice / qty)); - } else if (totalWeight > 0) { - // Untuk produk lain: unit_price = total_price / total_weight - formik.setFieldValue( - 'unit_price', - roundPrice(totalPrice / totalWeight) - ); - } - } - break; - } - } + handleMarketingCalculation(field, { + values: formik.values, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); }; // ===== Formik Error List ===== @@ -617,7 +515,7 @@ const SalesOrderProductForm = ({ } /> - {/* Harga per convertion unit */} + {/* Harga per convertion unit (PETI / KG) */} {(formik.values.convertion_unit?.value.toLowerCase() === 'peti' || formik.values.convertion_unit?.value.toLowerCase() === 'kg') && ( )} + {/* Harga per butir untuk TELUR + QTY */} + {formik.values.marketing_type?.value.toLowerCase() === 'telur' && + formik.values.convertion_unit?.value.toLowerCase() === 'qty' && ( + { + formik.setFieldValue('price_per_qty', Number(e.target.value)); + setCurrentInput('price_per_qty'); + }} + onBlur={() => handleBlurField('price_per_qty')} + isError={ + formik.touched.price_per_qty && + Boolean(formik.errors.price_per_qty) + } + errorMessage={formik.errors.price_per_qty} + placeholder='Masukan Harga per Butir' + /> + )} + {/* Harga Satuan per Uom Produk Warehouse */} {formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( { diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 5ac9eede..57bfae3c 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -208,8 +208,12 @@ const SalesOrderProductTable = ({ Gudang + {item.kandang?.label} + + + Kategori - {item.product_warehouse?.label} + {item.marketing_type?.label} @@ -218,6 +222,20 @@ const SalesOrderProductTable = ({ {item.product_warehouse?.label} + + Tipe Konversi + + {item.convertion_unit?.label} + + + {item.convertion_unit?.value.toLowerCase() === 'peti' && ( + + Total Peti + + {item.convertion_unit?.label} + + + )} Total Bobot @@ -238,13 +256,17 @@ const SalesOrderProductTable = ({ - Qty + + {item.marketing_type?.value === 'telur' + ? 'Total Butir Telur' + : 'Qty'} + {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} - Total Harga Satuan + Harga Satuan {formatCurrency(parseFloat(item.unit_price as string))} diff --git a/src/lib/marketing-calculation.ts b/src/lib/marketing-calculation.ts new file mode 100644 index 00000000..8dd946ae --- /dev/null +++ b/src/lib/marketing-calculation.ts @@ -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; + } +}; diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 95444ecc..1ec2eed6 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -36,6 +36,8 @@ export type BaseSalesOrder = { total_price: number; product_warehouse: ProductWarehouse; vehicle_number: string; + marketing_type: string; + convertion_unit: string; }; export type BaseDeliveryOrder = { @@ -111,6 +113,22 @@ export type BaseCreateMarketingProductPayload = { avg_weight: string | number | undefined; total_price: string | number | undefined; marketing_type: string; + /** + * Tipe konversi untuk TELUR + * - "PETI": Penjualan telur dalam satuan peti + * - "KG": Penjualan telur dalam satuan kilogram + * + * Note: Untuk mode "QTY" di FE, tetap kirim "KG" ke BE dengan unit_price yang dinormalisasi + * karena BE tidak support convertion_unit "QTY". Workaround: + * - FE hitung: total_price = qty × price_per_qty + * - FE normalisasi: unit_price = total_price / total_weight + * - BE akan hitung: total_price = total_weight × unit_price (hasil sama) + */ + convertion_unit?: 'PETI' | 'KG'; + /** Berat per peti (kg), hanya untuk TELUR + PETI */ + weight_per_convertion?: number; + /** Umur minggu untuk AYAM_PULLET */ + week?: number; }; /**