Product Conversion

This commit is contained in:
2026-10-22 10:36:43 +03:30
parent 2048680d7a
commit 819e007e60
16 changed files with 1044 additions and 168 deletions
@@ -0,0 +1,175 @@
import {
computed,
inject,
provide,
ref,
type ComputedRef,
type InjectionKey,
type Ref,
} from 'vue'
import { getProducts } from '@/api/products'
import type { Product } from '@/models/products'
import {
COPPER_CONVERT_WIRE_FALLBACK,
calcCopperWireConvert,
type CopperConvertPrices,
type CopperConvertResult,
type CopperConvertSource,
} from '@/utils/copperWireConvert'
import { LEGACY_GROUP_KATOD, LEGACY_GROUP_MAFTOL, normalizeProductGroupType } from '@/utils/productCategories'
import { getProductBuyPrice } from '@/utils/productPrice'
import { getLastPrices, startLastPricePolling, stopLastPricePolling } from '@/utils/price'
import { persianToEnglishDigits } from '@/utils/persianDigits'
export const COPPER_CONVERT_PAGE_KEY: InjectionKey<CopperConvertPageContext> =
Symbol('copperConvertPageContext')
export interface CopperConvertPageContext {
loading: Ref<boolean>
source: Ref<CopperConvertSource>
inputKg: Ref<string>
prices: ComputedRef<CopperConvertPrices>
result: ComputedRef<CopperConvertResult | null>
priceRows: ComputedRef<{ label: string; value: string; missing?: boolean }[]>
}
function parseKg(raw: string): number {
const s = persianToEnglishDigits(String(raw ?? ''))
.replace(/,/g, '')
.replace(/[^\d.]/g, '')
const n = Number.parseFloat(s)
return Number.isFinite(n) ? n : 0
}
function titleIncludes(product: Product, part: string) {
return (product.title ?? '').includes(part)
}
function isKatodProduct(product: Product) {
const group = normalizeProductGroupType(product.group_type)
return group === LEGACY_GROUP_KATOD || titleIncludes(product, 'کاتد')
}
function isMaftolProduct(product: Product) {
const group = normalizeProductGroupType(product.group_type)
return group === LEGACY_GROUP_MAFTOL || titleIncludes(product, 'مفتول')
}
function isShamshProduct(product: Product) {
const group = normalizeProductGroupType(product.group_type)
return group.includes('شمش') || titleIncludes(product, 'شمش')
}
function wireCrossSection(product: Product): '1.5' | '2.5' | null {
const t = product.title ?? ''
if (/1\.5|۱\.۵/.test(t)) return '1.5'
if (/2\.5|۲\.۵/.test(t)) return '2.5'
return null
}
function isWireProduct(product: Product) {
const t = `${product.group_type ?? ''} ${product.title ?? ''}`
return t.includes('سیم') || wireCrossSection(product) != null
}
function pickProduct(
products: Product[],
matcher: (p: Product) => boolean,
): Product | undefined {
return products.find((p) => p.active && matcher(p))
}
function resolveWireRingPrice(product: Product | undefined, fallback: number): number {
if (!product) return fallback
const price = getProductBuyPrice(product)
return price > 0 ? price : fallback
}
export function useCopperConvertPage() {
const loading = ref(true)
const source = ref<CopperConvertSource>('katod')
const inputKg = ref('')
const catalog = ref<Product[]>([])
const katodProduct = computed(() => pickProduct(catalog.value, isKatodProduct))
const maftolProduct = computed(() => pickProduct(catalog.value, isMaftolProduct))
const shamshProduct = computed(() => pickProduct(catalog.value, isShamshProduct))
const wire15Product = computed(() =>
pickProduct(catalog.value, (p) => isWireProduct(p) && wireCrossSection(p) === '1.5'),
)
const wire25Product = computed(() =>
pickProduct(catalog.value, (p) => isWireProduct(p) && wireCrossSection(p) === '2.5'),
)
const prices = computed<CopperConvertPrices>(() => ({
katodPerKg: katodProduct.value ? getProductBuyPrice(katodProduct.value) : 0,
maftoulPerKg: maftolProduct.value ? getProductBuyPrice(maftolProduct.value) : 0,
shamshPerKg: shamshProduct.value ? getProductBuyPrice(shamshProduct.value) : 0,
wireRing15: resolveWireRingPrice(wire15Product.value, COPPER_CONVERT_WIRE_FALLBACK.wireRing15),
wireRing25: resolveWireRingPrice(wire25Product.value, COPPER_CONVERT_WIRE_FALLBACK.wireRing25),
wireWeightGrams15: COPPER_CONVERT_WIRE_FALLBACK.wireWeightGrams15,
wireWeightGrams25: COPPER_CONVERT_WIRE_FALLBACK.wireWeightGrams25,
}))
const result = computed(() =>
calcCopperWireConvert(parseKg(inputKg.value), source.value, prices.value),
)
const priceRows = computed(() => {
const p = prices.value
const row = (label: string, amount: number) => ({
label,
value: amount > 0 ? `${Math.round(amount).toLocaleString('en-US')} ریال` : '—',
missing: amount <= 0,
})
return [
row('قیمت کاتد (هر کیلو)', p.katodPerKg),
row('قیمت مفتول (هر کیلو)', p.maftoulPerKg),
row('قیمت شمش (هر کیلو)', p.shamshPerKg),
row('قیمت حلقه سیم ۱.۵', p.wireRing15),
row('قیمت حلقه سیم ۲.۵', p.wireRing25),
]
})
async function loadCatalog() {
loading.value = true
try {
await getLastPrices()
const list = await getProducts()
catalog.value = Array.isArray(list) ? list : []
} catch {
catalog.value = []
} finally {
loading.value = false
}
}
async function refreshLivePrices() {
startLastPricePolling()
await loadCatalog()
}
function stopLivePrices() {
stopLastPricePolling()
}
const ctx: CopperConvertPageContext = {
loading,
source,
inputKg,
prices,
result,
priceRows,
}
provide(COPPER_CONVERT_PAGE_KEY, ctx)
return { ...ctx, refreshLivePrices, stopLivePrices, loadCatalog }
}
export function useCopperConvertPageContext() {
const ctx = inject(COPPER_CONVERT_PAGE_KEY)
if (!ctx) {
throw new Error('useCopperConvertPageContext requires useCopperConvertPage')
}
return ctx
}