diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index a70bd47..660f7e7 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -386,6 +386,12 @@ class ProductController extends SuperController $data['max_order_cash'] = $hasMax ? (int) $max : null; } + if (!Product::hasShowInAssetsColumn()) { + unset($data['show_in_assets']); + } elseif (array_key_exists('show_in_assets', $data)) { + $data['show_in_assets'] = !empty($data['show_in_assets']); + } + return $data; } } diff --git a/app/Http/Controllers/Customer/ProductController.php b/app/Http/Controllers/Customer/ProductController.php index 44f992a..1c30a02 100644 --- a/app/Http/Controllers/Customer/ProductController.php +++ b/app/Http/Controllers/Customer/ProductController.php @@ -36,7 +36,7 @@ class ProductController extends SuperController $query->where('owner', $user->id); })->with(['wallet' => function ($query) use ($user) { $query->where('owner', $user->id); - }])->get(); + }, 'file'])->get(); // Prepare the response $user['rows'] = $products->map(function ($product) { @@ -46,6 +46,10 @@ class ProductController extends SuperController 'group_type' => $product->group_type ?? null, ]); + if ($product->relationLoaded('file') && $product->file) { + $product->file->setAttribute('full_src', $product->file->src); + } + return $product; }); diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 7a334a7..30f74a6 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -14,6 +14,7 @@ use App\Models\BankAccount; use App\Models\Price; use App\Models\Setting; use App\Http\Controllers\Admin\TahesabController; +use App\Services\AssetSummaryService; use App\Services\ZaarboxPriceService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; @@ -47,6 +48,15 @@ class WalletController extends Controller return response()->json($wallet); } + public function assetsSummary(AssetSummaryService $assetSummaryService) + { + $user = auth('customer')->user(); + + return response()->json( + $assetSummaryService->buildForCustomer((int) $user->id, (int) $user->type) + ); + } + public function priceData(Request $request, ZaarboxPriceService $zaarboxPriceService) { return response()->json([ diff --git a/app/Models/Product.php b/app/Models/Product.php index f2b3f6d..903d10a 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -18,6 +18,7 @@ class Product extends Model private static ?bool $priceOffsetColumnExists = null; private static ?bool $priceColumnExists = null; private static ?bool $orderLimitColumnsExist = null; + private static ?bool $showInAssetsColumnExists = null; protected static function booted(): void { @@ -32,6 +33,9 @@ class Product extends Model $product->attributes['max_order_cash'] ); } + if (!static::hasShowInAssetsColumn()) { + unset($product->attributes['show_in_assets']); + } }); } @@ -87,6 +91,24 @@ class Product extends Model return static::$orderLimitColumnsExist; } + public static function hasShowInAssetsColumn(): bool + { + if (static::$showInAssetsColumnExists !== null) { + return static::$showInAssetsColumnExists; + } + + try { + static::$showInAssetsColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'show_in_assets' + ); + } catch (\Throwable) { + static::$showInAssetsColumnExists = false; + } + + return static::$showInAssetsColumnExists; + } + public function orderLimitsEnabled(): bool { if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) { @@ -137,6 +159,7 @@ class Product extends Model 'active', // The active status of the record (boolean). 'for_barayand', // Whether it is for Barayand (boolean). 'show', // Whether to show the record (boolean). + 'show_in_assets', // Show balance on customer assets page cards 'open_sell', // Whether selling is open (boolean). 'auto_open_at', // Whether selling is open (boolean). 'auto_close_at', // Whether selling is open (boolean). @@ -161,6 +184,7 @@ class Product extends Model 'can_canceled' => 'boolean', // Cast to boolean. 'active' => 'boolean', // Cast to boolean. 'show' => 'boolean', // Cast to boolean. + 'show_in_assets' => 'boolean', 'for_barayand' => 'boolean', // Cast to boolean. 'open_sell' => 'boolean', // Cast to boolean. 'auto_sell' => 'boolean', // Cast to boolean (you mentioned this in your casts but it's missing in `$fillable`). diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php new file mode 100644 index 0000000..6c622ad --- /dev/null +++ b/app/Services/AssetSummaryService.php @@ -0,0 +1,256 @@ + 'طلا', + 2 => 'نقره', + 3 => 'مس', + ]; + + public function buildForCustomer(int $customerId, int $ownerType): array + { + $catalog = Product::query() + ->where('active', 1) + ->with('file') + ->get(); + + $userWallets = Wallet::query() + ->with(['product.file']) + ->where('owner', $customerId) + ->where('owner_type', $ownerType) + ->get(); + + $mainWallet = $userWallets->first(fn (Wallet $w) => $w->product_id === null); + $metalIds = $this->catalogMetalIds($catalog); + $weightUnit = Controller::tradeWeightUnit(); + $hasCountBased = $this->catalogHasCountBased($catalog); + $hasWeightBased = $this->catalogHasWeightBased($catalog); + + $inventoryCards = $this->buildInventoryCards($userWallets); + $metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit); + $cashWallet = (float) ($mainWallet->cash ?? 0); + $assetsRialValue = $this->sumAssetsRialValue($userWallets); + $totalRialValue = $cashWallet + $assetsRialValue; + + return [ + 'meta' => [ + 'metal_ids' => $metalIds, + 'metal_count' => count($metalIds), + 'has_count_based_products' => $hasCountBased, + 'has_weight_based_products' => $hasWeightBased, + 'weight_unit' => $weightUnit, + 'single_metal' => count($metalIds) === 1, + ], + 'cash_wallet' => round($cashWallet, 0), + 'inventory_cards' => $inventoryCards, + 'count_products' => $hasCountBased + ? array_values(array_filter($inventoryCards, fn (array $row) => ($row['display_type'] ?? '') === 'count')) + : [], + 'metal_totals' => $metalTotals, + 'assets_rial_value' => round($assetsRialValue, 0), + 'total_rial_value' => round($totalRialValue, 0), + ]; + } + + private function catalogMetalIds(Collection $catalog): array + { + return $catalog + ->pluck('metal_id') + ->filter(fn ($id) => (int) $id > 0) + ->map(fn ($id) => (int) $id) + ->unique() + ->sort() + ->values() + ->all(); + } + + private function catalogHasCountBased(Collection $catalog): bool + { + return $catalog->contains(fn (Product $product) => $this->isCountBasedProduct($product)); + } + + private function catalogHasWeightBased(Collection $catalog): bool + { + return $catalog->contains(fn (Product $product) => !$this->isCountBasedProduct($product)); + } + + private function buildInventoryCards(Collection $userWallets): array + { + $cards = []; + + foreach ($userWallets as $walletLine) { + if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) { + continue; + } + + $product = $walletLine->product; + if (!$product || !$this->shouldShowInAssets($product)) { + continue; + } + + $productPayload = $this->productPayload($product); + $isCountBased = $this->isCountBasedProduct($product); + + $cards[] = [ + 'product_id' => (int) $product->id, + 'title' => (string) $product->title, + 'group_type' => (string) ($product->group_type ?? ''), + 'metal_id' => (int) ($product->metal_id ?? 0), + 'quantity' => round((float) $walletLine->gold, 3), + 'unit' => Controller::productQuantityUnit($productPayload), + 'display_type' => $isCountBased ? 'count' : 'weight', + 'unit_weight' => $this->productUnitWeightLabel($product), + 'show_in_assets' => true, + 'file' => $this->filePayload($product), + ]; + } + + return $cards; + } + + private function buildMetalTotals( + Collection $userWallets, + ?Wallet $mainWallet, + array $metalIds, + string $weightUnit + ): array { + if ($metalIds === []) { + return []; + } + + $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0)); + $totals = []; + + foreach ($metalIds as $metalId) { + $productGrams = 0.0; + + foreach ($userWallets as $walletLine) { + if ($walletLine->product_id === null || !$walletLine->product) { + continue; + } + + if ((int) $walletLine->product->metal_id !== (int) $metalId) { + continue; + } + + if ((string) ($walletLine->product->group_type ?? '') === 'سکه') { + continue; + } + + $productGrams += Controller::productLineWeightToGrams( + $walletLine->product, + $walletLine->gold + ); + } + + $meltedGrams = $this->meltedGramsForMetal($mainWalletGrams, $metalId, $metalIds); + $productWeight = round(Controller::gramsToTradeUnit($productGrams), 3); + $meltedWeight = round(Controller::gramsToTradeUnit($meltedGrams), 3); + + $totals[] = [ + 'metal_id' => (int) $metalId, + 'metal_label' => self::METAL_LABELS[$metalId] ?? 'فلز', + 'label' => 'کل دارایی ' . (self::METAL_LABELS[$metalId] ?? 'فلز'), + 'product_weight' => $productWeight, + 'melted_weight' => $meltedWeight, + 'total_weight' => round($productWeight + $meltedWeight, 3), + 'unit' => $weightUnit, + ]; + } + + return $totals; + } + + private function meltedGramsForMetal(float $mainWalletGrams, int $metalId, array $catalogMetalIds): float + { + if ($mainWalletGrams <= 0) { + return 0.0; + } + + if (count($catalogMetalIds) === 1) { + return (int) $catalogMetalIds[0] === (int) $metalId ? $mainWalletGrams : 0.0; + } + + return (int) $metalId === 1 ? $mainWalletGrams : 0.0; + } + + private function sumAssetsRialValue(Collection $userWallets): float + { + $total = 0.0; + + foreach ($userWallets as $walletLine) { + if ($walletLine->product_id === null || !$walletLine->product) { + continue; + } + + $product = $walletLine->product; + $qty = (float) $walletLine->gold; + $metalId = (int) ($product->metal_id ?? 0); + + if ($metalId === 1) { + $total += (float) ($product->price ?? 0) * $qty; + } elseif ($metalId === 2 || $metalId === 3) { + $total += ((float) ($product->price ?? 0) * (float) ($product->amount ?? 1)) * $qty; + } + } + + return $total; + } + + private function shouldShowInAssets(Product $product): bool + { + if (Product::hasShowInAssetsColumn()) { + return (bool) ($product->show_in_assets ?? true); + } + + return true; + } + + private function isCountBasedProduct(Product $product): bool + { + return Controller::isCountBasedProduct($this->productPayload($product)); + } + + private function productPayload(Product $product): array + { + return [ + 'unit' => $product->unit ?? null, + 'direct_price' => $product->direct_price ?? null, + 'group_type' => $product->group_type ?? null, + 'amount' => $product->amount ?? null, + ]; + } + + private function productUnitWeightLabel(Product $product): ?string + { + if (!Controller::hasProductUnitWeight($this->productPayload($product))) { + return null; + } + + $amount = (float) ($product->amount ?? 0); + $unit = Controller::productStorageWeightUnit($product); + + return trim($amount . ' ' . $unit); + } + + private function filePayload(Product $product): ?array + { + if (!$product->relationLoaded('file') || !$product->file) { + return null; + } + + $file = $product->file->toArray(); + $file['full_src'] = $product->file->src ?? null; + + return $file; + } +} diff --git a/database/sql/add_product_show_in_assets.sql b/database/sql/add_product_show_in_assets.sql new file mode 100644 index 0000000..2238559 --- /dev/null +++ b/database/sql/add_product_show_in_assets.sql @@ -0,0 +1,14 @@ +-- نمایش موجودی محصول در صفحه دارایی‌ها +-- USE دیتابیس_خودتان; + +SET @db = DATABASE(); + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'products' AND COLUMN_NAME = 'show_in_assets') = 0, + 'ALTER TABLE `products` ADD COLUMN `show_in_assets` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''نمایش در صفحه دارایی‌ها'' AFTER `show`', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/routes/customer.php b/routes/customer.php index f07d117..8e1fc91 100644 --- a/routes/customer.php +++ b/routes/customer.php @@ -57,7 +57,8 @@ Route::middleware('auth:customer')->group(function () { Route::get('/active_finger', [CustomerController::class, 'active_finger']); //------------------wallet - Route::get('wallet/me', [WalletController::class, 'walletMe']); + Route::get('wallet/me', [WalletController::class, 'walletMe']); + Route::get('wallet/assets', [WalletController::class, 'assetsSummary']); Route::post('wallet/withdraw', [WalletController::class, 'withdraw']); Route::post('wallet/withdraw_physical', [WalletController::class, 'withdraw_physical']); Route::post('/wallet/withdraw/table', [WalletController::class, 'indexWithdraw']);