Files
boursecu_user/src/utils/productAssets.ts
T
2026-06-23 23:31:08 +03:30

773 lines
23 KiB
TypeScript

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<string, string> = {
گوی: 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<T extends { metal_id?: number; show_in_assets?: boolean }>(
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<T>(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<Record<number, number>>
): 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<number>()
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<T extends { product_id?: number; file?: ProductFile | null }>(
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<T extends { id?: number }>(
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<string, unknown> | 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<string, unknown> | 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<string, unknown> | null,
wallets?: UserWalletLine[] | null
): number {
return getRialCashAmount(walletInfo, wallets)
}
export function getTotalRialEquivalent(walletInfo?: Record<string, unknown> | null): number {
return Number(walletInfo?.count_cash_total ?? 0)
}
export function getMetalAssetAmount(
walletInfo: Record<string, unknown> | 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<string, unknown> | 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)
}