import type { Product } from '@/models/products' import { formatWeightAmount, usePriceWithoutType } from '@/utils/usePrice' import type { UserWalletLine } from '@/models/wallet' import type { ProductFile } from '@/utils/file' import type { WalletInventoryCard, WalletMetalTotal } from '@/models/walletAssets' import { getFileUrl } from '@/utils/file' import { isCountBasedProduct, getProductQuantityUnit, type ProductUnitSource } from '@/utils/productDealMethod' import { isMeltedTradeGroup, normalizeProductGroupType, LEGACY_GROUP_KATOD, LEGACY_GROUP_MAFTOL } from '@/utils/productCategories' import defaultImg from '@/assets/img/defaultAbshodeh.svg' import goyImg from '@/assets/img/goyImg.svg' import mokaabImg from '@/assets/img/mokaabImg.svg' import sekkeImg from '@/assets/img/sekkeImg.svg' import mesImg from '@/assets/img/mesImg.jpg' import noghrehImg from '@/assets/img/noghrehImg.webp' import parsianImg from '@/assets/img/parsianImg.svg' export const METAL_GOLD = 1 export const METAL_SILVER = 2 export const METAL_COPPER = 3 const GROUP_DEFAULT_IMAGES: Record = { گوی: goyImg, مکعب: mokaabImg, سکه: sekkeImg, مس: mesImg, [LEGACY_GROUP_KATOD]: defaultImg, [LEGACY_GROUP_MAFTOL]: defaultImg, نقره: noghrehImg, 'سکه پارسیان': parsianImg, طلا: defaultImg, آبشده: defaultImg, } export function getMetalLabel(metalId: number): string { switch (metalId) { case METAL_GOLD: return 'طلا' case METAL_SILVER: return 'نقره' case METAL_COPPER: return 'مس' default: return 'فلز' } } export function getTotalMetalLabel(metalId: number): string { return `کل دارایی ${getMetalLabel(metalId)}` } /** boursecu home — ترتیب نمایش: مس، طلا، سایر فلزها */ export function resolveBoursecuAssetMetalTotals( metalTotals: WalletMetalTotal[] ): WalletMetalTotal[] { if (!metalTotals.length) { return [] } const ordered: WalletMetalTotal[] = [] const copper = metalTotals.find((metal) => metal.metal_id === METAL_COPPER) const gold = metalTotals.find((metal) => metal.metal_id === METAL_GOLD) if (copper) { ordered.push(copper) } if (gold) { ordered.push(gold) } for (const metal of metalTotals) { if (!ordered.some((item) => item.metal_id === metal.metal_id)) { ordered.push(metal) } } return ordered } export function resolveMetalAssetsDisplayLabel( metal: WalletMetalTotal, isBoursecuLayout: boolean ): string { if (!isBoursecuLayout) { return metal.label } switch (metal.metal_id) { case METAL_COPPER: return 'دارایی مس' case METAL_GOLD: return 'دارایی شمش مس' case METAL_SILVER: return 'دارایی نقره' default: return metal.label } } export type BoursecuHomeAssetRowId = 'copper-total' | 'rial-total' export interface BoursecuAssetPopupLine { label: string value: string tone?: 'profit' | 'loss' | 'default' } export interface BoursecuHomeAssetRow { id: BoursecuHomeAssetRowId label: string value: string popupLines: BoursecuAssetPopupLine[] } /** boursecu — متن مودال برای ردیف‌های metal_totals */ export function formatBoursecuMetalModalText(metal: WalletMetalTotal): string { switch (metal.metal_id) { case METAL_COPPER: return getTotalMetalLabel(METAL_COPPER) case METAL_GOLD: return 'دارایی شمش مس = موجودی شمش مس' case METAL_SILVER: return 'دارایی نقره = موجودی نقره' default: return `${resolveMetalAssetsDisplayLabel(metal, true)} = موجودی ${metal.label}` } } function isBoursecuKatodCard(card: WalletInventoryCard) { const group = normalizeProductGroupType(card.group_type) return group === LEGACY_GROUP_KATOD || (card.title ?? '').includes('کاتد') } function isBoursecuMaftolCard(card: WalletInventoryCard) { const group = normalizeProductGroupType(card.group_type) return group === LEGACY_GROUP_MAFTOL || (card.title ?? '').includes('مفتول') } function isBoursecuShamshCard(card: WalletInventoryCard) { return (card.title ?? '').includes('شمش') } function findBoursecuProductCard( inventoryCards: WalletInventoryCard[], countProducts: WalletInventoryCard[], matcher: (card: WalletInventoryCard) => boolean ) { return inventoryCards.find(matcher) ?? countProducts.find(matcher) } function resolveBoursecuCardQuantityValue( card: WalletInventoryCard | undefined, metal: WalletMetalTotal | undefined, defaultUnit: string ): string { if (card) { return formatInventoryCardValue(card) } if (metal) { return formatBoursecuMetalRowValue(metal) } return `0 ${defaultUnit}` } /** boursecu — جمع موجودی کاتد، مفتول و شمش از کارت‌های دارایی صفحه اصلی */ function sumBoursecuHomeProductInventories( inventoryCards: WalletInventoryCard[], countProducts: WalletInventoryCard[] ): number { let sum = 0 for (const card of inventoryCards) { if (card.display_type === 'count') { continue } sum += Number(card.quantity ?? 0) } for (const card of countProducts) { const quantity = Number(card.quantity ?? 0) const unitWeight = Number(card.unit_weight ?? 0) if (unitWeight > 0) { sum += quantity * unitWeight continue } if (card.unit?.includes('کیلو')) { sum += quantity } } return sum } function findMetalTotalById(metalTotals: WalletMetalTotal[], metalId: number) { return metalTotals.find((metal) => metal.metal_id === metalId) } function formatBoursecuMetalRowValue(metal: WalletMetalTotal): string { const amount = formatWeightAmount(Number(metal.total_weight ?? 0)) if (metal.metal_id === METAL_GOLD) { return `${amount} عدد` } return `${amount}${metal.unit ?? ''}` } function formatRialNumber(value: number): string { return usePriceWithoutType(Math.round(value)) } function formatBoursecuProfitLossLine(profitLoss?: number): BoursecuAssetPopupLine { const amount = Number(profitLoss ?? 0) if (amount > 0) { return { label: 'میزان سود یا زیان', value: `${formatRialNumber(amount)} ریال (سود)`, tone: 'profit', } } if (amount < 0) { return { label: 'میزان سود یا زیان', value: `${formatRialNumber(Math.abs(amount))} ریال (زیان)`, tone: 'loss', } } return { label: 'میزان سود یا زیان', value: `${formatRialNumber(0)} ریال`, } } function buildBoursecuCopperPopupLines(summary: { inventory_cards?: WalletInventoryCard[] count_products?: WalletInventoryCard[] metal_totals?: WalletMetalTotal[] }): BoursecuAssetPopupLine[] { const inventoryCards = summary.inventory_cards ?? [] const countProducts = summary.count_products ?? [] const metalTotals = summary.metal_totals ?? [] const copperMetal = findMetalTotalById(metalTotals, METAL_COPPER) const shamshMetal = findMetalTotalById(metalTotals, METAL_GOLD) const weightUnit = copperMetal?.unit || 'کیلوگرم' const totalWeight = sumBoursecuHomeProductInventories(inventoryCards, countProducts) const katodCard = findBoursecuProductCard(inventoryCards, countProducts, isBoursecuKatodCard) const maftolCard = findBoursecuProductCard(inventoryCards, countProducts, isBoursecuMaftolCard) const shamshCard = findBoursecuProductCard(inventoryCards, countProducts, isBoursecuShamshCard) return [ { label: 'دارایی کاتد مس', value: resolveBoursecuCardQuantityValue(katodCard, undefined, 'کیلوگرم'), }, { label: 'دارایی مفتول مس', value: resolveBoursecuCardQuantityValue(maftolCard, undefined, 'کیلوگرم'), }, { label: 'دارایی شمش مس', value: resolveBoursecuCardQuantityValue(shamshCard, shamshMetal, 'عدد'), }, { label: 'مجموع کل دارایی مس', value: `${formatWeightAmount(totalWeight)} ${weightUnit}`, }, ] } function buildBoursecuRialPopupLines( summary: { cash_wallet?: number total_rial_value?: number assets_rial_value?: number metal_totals?: WalletMetalTotal[] profit_loss?: number }, liveCopperPrice: number ): BoursecuAssetPopupLine[] { const cash = Number(summary.cash_wallet ?? 0) const metalRial = resolveAssetsRialValue(summary, liveCopperPrice) const total = Number(summary.total_rial_value ?? 0) > 0 ? Number(summary.total_rial_value) : cash + metalRial return [ { label: 'کیف پول ریالی', value: `${formatRialNumber(cash)} ریال`, }, { label: 'ارزش ریالی مس (وزن × قیمت زنده)', value: `${formatRialNumber(metalRial)} ریال`, }, { label: 'مجموع کل دارایی ریالی', value: `${formatRialNumber(total)} ریال`, }, formatBoursecuProfitLossLine(summary.profit_loss), ] } /** boursecu home — دو ردیف کل دارایی مسی و ریالی */ export function buildBoursecuHomeAssetRows( summary: { inventory_cards?: WalletInventoryCard[] count_products?: WalletInventoryCard[] metal_totals?: WalletMetalTotal[] cash_wallet?: number total_rial_value?: number assets_rial_value?: number profit_loss?: number } | null | undefined, liveCopperPrice = 0 ): BoursecuHomeAssetRow[] { if (!summary) { return [] } const inventoryCards = summary.inventory_cards ?? [] const countProducts = summary.count_products ?? [] const metalTotals = summary.metal_totals ?? [] const copperMetal = findMetalTotalById(metalTotals, METAL_COPPER) const weightUnit = copperMetal?.unit || 'کیلوگرم' const totalCopperWeight = copperMetal != null ? Number(copperMetal.total_weight ?? 0) : sumBoursecuHomeProductInventories(inventoryCards, countProducts) const cash = Number(summary.cash_wallet ?? 0) const metalRial = resolveAssetsRialValue(summary, liveCopperPrice) const totalRial = Number(summary.total_rial_value ?? 0) > 0 ? Number(summary.total_rial_value) : cash + metalRial return [ { id: 'copper-total', label: 'کل دارایی مسی', value: `${formatWeightAmount(totalCopperWeight)} ${weightUnit}`, popupLines: buildBoursecuCopperPopupLines(summary), }, { id: 'rial-total', label: 'کل دارایی ریالی', value: `${formatRialNumber(totalRial)} ریال`, popupLines: buildBoursecuRialPopupLines(summary, liveCopperPrice), }, ] } export function sumInventoryCardsWeight( cards: Array<{ metal_id?: number; quantity?: number }>, metalId: number ): number { return cards .filter((card) => Number(card.metal_id) === metalId) .reduce((sum, card) => sum + Number(card.quantity ?? 0), 0) } /** ردیف‌های جزئی action sheet — فقط محصولاتی که show_in_assets روشن است. */ export function assetSheetInventoryCards( cards: T[], metalId?: number ): T[] { return cards.filter((card) => { if (!card.show_in_assets) { return false } if (metalId == null) { return true } return Number(card.metal_id) === metalId }) } /** همه کارت‌های صفحه دارایی‌ها — بدون فیلتر show_in_assets. */ export function fundPageInventoryCards(cards: T[]): T[] { return cards.filter((card) => Number((card as { quantity?: number }).quantity ?? 0) > 0) } export function resolveSummaryMetalWeight( metal: { total_weight: number }, _cards?: Array<{ metal_id?: number; quantity?: number }> ): number { return Number(metal.total_weight ?? 0) } /** متن توضیح مودال دارایی فلزی — بر اساس metal_totals سرور، بدون هاردکد طلا */ export function formatMetalAssetsInfoText(metal: WalletMetalTotal): string { const unit = metal.unit || 'گرم' return `${metal.label}: مجموع کیف محصولات (${metal.product_weight} ${unit}) + موجودی کیف اصلی (${metal.melted_weight} ${unit})` } /** متن توضیح مودال دارایی ریالی */ export function formatRialAssetsInfoText(cashWallet: number, metalRialValue: number): string { const total = cashWallet + metalRialValue return [ `کیف پول ریالی: ${formatRialNumber(cashWallet)} ریال`, `ارزش ریالی طلا (وزن × قیمت زنده): ${formatRialNumber(metalRialValue)} ریال`, `مجموع: ${formatRialNumber(total)} ریال`, ].join('\n') } /** ارزش ریالی فلزات از وزن × قیمت زنده — fallback کلاینت قبل از deploy بک‌اند */ export function computeMetalRialValue( metalTotals: Array<{ metal_id?: number; total_weight?: number }>, liveGoldPrice: number, livePricesByMetal?: Partial> ): number { return metalTotals.reduce((sum, metal) => { const weight = Number(metal.total_weight ?? 0) if (weight <= 0) { return sum } const metalId = Number(metal.metal_id ?? 0) const unitPrice = livePricesByMetal?.[metalId] ?? (metalId === METAL_GOLD ? liveGoldPrice : 0) return sum + weight * unitPrice }, 0) } export function resolveAssetsRialValue( summary: { assets_rial_value?: number; metal_totals?: Array<{ metal_id?: number; total_weight?: number }> } | null | undefined, liveGoldPrice: number ): number { const fromServer = Number(summary?.assets_rial_value ?? 0) if (fromServer > 0) { return fromServer } return computeMetalRialValue(summary?.metal_totals ?? [], liveGoldPrice) } export function formatInventoryCardValue(card: { quantity?: number unit?: string display_type?: string }): string { const quantity = Number(card.quantity ?? 0) const unit = card.unit ?? (card.display_type === 'count' ? 'عدد' : 'گرم') const formatted = card.display_type === 'count' ? usePriceWithoutType(quantity, false) : formatWeightAmount(quantity) return `${formatted} ${unit}` } export function findProductInventoryCard( cards: WalletInventoryCard[], productId?: number | null ): WalletInventoryCard | undefined { if (!productId) { return undefined } return cards.find((item) => Number(item.product_id) === Number(productId)) } /** * موجودی کیف یک محصول از inventory_cards. * fallback: پروژه‌های ساختار قدیم (مثل گلد علی) کیف محصول ندارند و موجودی روی gold کیف اصلی است. */ export function getProductInventoryQuantity( cards: WalletInventoryCard[], productId?: number | null, mainWalletGold?: number | null ): number { const card = findProductInventoryCard(cards, productId) if (card) { return Number(card.quantity ?? 0) } return Number(mainWalletGold ?? 0) } /** برچسب موجودی کیف محصول — برای فرم خرید/فروش */ export function formatProductWalletBalanceText( product: { id?: number; title?: string | null; unit?: string | null; group_type?: string | null } | null | undefined, cards: WalletInventoryCard[] = [], mainWalletGold?: number | null ): string { const card = findProductInventoryCard(cards, product?.id) const quantity = card ? Number(card.quantity ?? 0) : Number(mainWalletGold ?? 0) const unit = card?.unit ?? getProductQuantityUnit(product) const title = product?.title?.trim() || 'محصول' return `موجودی کیف ${title}: ${quantity} ${unit}` } export function isSingleMetalCatalog(metalIds: number[]): boolean { return metalIds.length === 1 } export function getCatalogMetalIds(products?: Product[] | null): number[] { const ids = new Set() for (const product of products ?? []) { const metalId = Number(product.metal_id) if (metalId > 0) { ids.add(metalId) } } return Array.from(ids).sort((a, b) => a - b) } export function catalogHasCountBasedProducts(products?: Product[] | null): boolean { return (products ?? []).some((product) => isCountBasedProduct(product)) } export function catalogHasMeltedProducts(products?: Product[] | null): boolean { return (products ?? []).some((product) => isMeltedTradeGroup(product.group_type)) } export function enrichInventoryCardsWithCatalog( cards: T[], catalog: Product[] ): T[] { return cards.map((card) => { const fromCatalog = catalog.find((product) => product.id === card.product_id) if (!fromCatalog) { return card } return { ...card, file: card.file ?? fromCatalog.file ?? null, } }) } export function enrichInventoryWithCatalog( items: T[], catalog: Product[] ): T[] { return items.map((item) => { const fromCatalog = catalog.find((product) => product.id === item.id) if (!fromCatalog) { return item } const current = item as T & Product return { ...fromCatalog, ...current, wallet: current.wallet ?? fromCatalog.wallet, file: current.file ?? fromCatalog.file, metal_id: current.metal_id ?? fromCatalog.metal_id, group_type: current.group_type ?? fromCatalog.group_type, } }) } export function getProductDisplayImage(product?: { group_type?: string | null title?: string | null file?: ProductFile | null img?: string | null } | null): string { const fileUrl = getFileUrl(product?.file) if (fileUrl) { return fileUrl } if (product?.img && /^https?:\/\//i.test(product.img)) { return product.img } const group = normalizeProductGroupType(product?.group_type) if (group && GROUP_DEFAULT_IMAGES[group]) { return GROUP_DEFAULT_IMAGES[group] } return defaultImg } type InventoryProduct = ProductUnitSource & { metal_id?: number | null title?: string | null wallet?: { gold?: number | null } | null } export function sumCountBasedInventory(products?: InventoryProduct[] | null): number { return (products ?? []) .filter((product) => product.title !== 'آبشده نقدی' && isCountBasedProduct(product)) .reduce((sum, product) => sum + Number(product.wallet?.gold ?? 0), 0) } export function sumProductWalletByMetal( products: InventoryProduct[] | null | undefined, metalId: number ): number { return (products ?? []) .filter((product) => Number(product.metal_id) === metalId) .reduce((sum, product) => sum + Number(product.wallet?.gold ?? 0), 0) } export function getMainWalletLine(wallets?: UserWalletLine[] | null): UserWalletLine | undefined { return wallets?.find((line) => line.product_id == null) } export function getMainWalletGold(wallets?: UserWalletLine[] | null): number { return Number(getMainWalletLine(wallets)?.gold ?? 0) } export function getMainWalletCash(wallets?: UserWalletLine[] | null): number { return Number(getMainWalletLine(wallets)?.cash ?? 0) } export function sumWalletsGold( wallets?: UserWalletLine[] | null, metalId?: number | null ): number { if (!wallets?.length) { return 0 } return wallets.reduce((sum, line) => { const weight = Number(line.gold ?? 0) if (metalId == null) { return sum + weight } if (line.product_id == null) { return metalId === METAL_GOLD ? sum + weight : sum } return Number(line.product?.metal_id) === metalId ? sum + weight : sum }, 0) } export function sumProductWalletsGold( wallets?: UserWalletLine[] | null, metalId?: number | null ): number { return (wallets ?? []) .filter((line) => line.product_id != null) .reduce((sum, line) => { const weight = Number(line.gold ?? 0) if (metalId == null) { return sum + weight } return Number(line.product?.metal_id) === metalId ? sum + weight : sum }, 0) } export function sumInventoryMetalWeight(products?: InventoryProduct[] | null): number { return (products ?? []) .filter((product) => product.title !== 'آبشده نقدی') .reduce((sum, product) => sum + Number(product.wallet?.gold ?? 0), 0) } export function getTotalMetalWeight( walletInfo?: Record | null, wallets?: UserWalletLine[] | null, inventoryProducts?: InventoryProduct[] | null ): number { const fromInventory = sumInventoryMetalWeight(inventoryProducts) if (fromInventory > 0) { return fromInventory } const fromProductWallets = sumProductWalletsGold(wallets) if (fromProductWallets > 0) { return fromProductWallets } if (wallets?.length) { return getMainWalletGold(wallets) } return Number(walletInfo?.treasure ?? walletInfo?.gold_sum ?? walletInfo?.count_gold ?? 0) } /** موجودی ریالی کیف اصلی — با main_wallet_products فیلد cash ممکن است کیف محصول باشد */ export function getRialCashAmount( walletInfo?: Record | null, wallets?: UserWalletLine[] | null ): number { const aggregated = walletInfo?.count_cash_wallet ?? walletInfo?.count_cash if (aggregated != null && aggregated !== '') { const parsed = Number(aggregated) if (!Number.isNaN(parsed)) { return parsed } } const mainCash = getMainWalletCash(wallets) if (mainCash > 0) { return mainCash } return Number(walletInfo?.cash ?? 0) } export function getCashWalletBalance( walletInfo?: Record | null, wallets?: UserWalletLine[] | null ): number { return getRialCashAmount(walletInfo, wallets) } export function getTotalRialEquivalent(walletInfo?: Record | null): number { return Number(walletInfo?.count_cash_total ?? 0) } export function getMetalAssetAmount( walletInfo: Record | null | undefined, metalId: number, inventoryProducts?: InventoryProduct[] | null ): number { if (inventoryProducts?.length) { const fromProducts = sumProductWalletByMetal(inventoryProducts, metalId) if (fromProducts > 0 || inventoryProducts.some((product) => Number(product.metal_id) === metalId)) { return fromProducts } } if (!walletInfo) { return 0 } switch (metalId) { case METAL_GOLD: return Number(walletInfo.count_gold_wallets ?? walletInfo.count_gold ?? 0) case METAL_SILVER: return Number(walletInfo.count_silver ?? 0) case METAL_COPPER: return Number(walletInfo.count_copper ?? 0) default: return 0 } } export function getMetalAssetAmountForDisplay( walletInfo: Record | null | undefined, metalId: number, catalogMetalIds: number[], inventoryProducts?: InventoryProduct[] | null, wallets?: UserWalletLine[] | null ): number { if (isSingleMetalCatalog(catalogMetalIds)) { return getTotalMetalWeight(walletInfo, wallets, inventoryProducts) } const productWeight = sumProductWalletByMetal(inventoryProducts, metalId) const mainWalletWeight = wallets?.length ? getMainWalletGold(wallets) : Number(walletInfo?.count_gold_wallet ?? 0) if (catalogHasMeltedProducts(inventoryProducts as Product[] | null | undefined) && metalId === catalogMetalIds[0]) { return productWeight + mainWalletWeight } if (wallets?.length) { const walletWeight = sumWalletsGold(wallets, metalId) if (walletWeight > 0) { return walletWeight } } return productWeight || getMetalAssetAmount(walletInfo, metalId, inventoryProducts) }