> */ public function buildRows(Fornitore $fornitore, ?int $stabileId = null, int $limit = 20): array { if (! Schema::hasTable('products')) { return []; } $products = Product::query() ->withCount(['identifiers', 'serials', 'media']) ->with([ 'identifiers', 'offers' => function ($offerQuery) use ($fornitore): void { $offerQuery ->where('is_active', true) ->where(function ($builder) use ($fornitore): void { $builder->whereNull('fornitore_id') ->orWhere('fornitore_id', (int) $fornitore->id); }) ->orderByDesc('is_internal') ->orderBy('price_amount'); }, ]) ->where('is_active', true) ->where(function ($query) use ($fornitore): void { $query->where('default_fornitore_id', (int) $fornitore->id) ->orWhereHas('offers', fn($offerQuery) => $offerQuery->where('fornitore_id', (int) $fornitore->id)); }) ->limit(max($limit * 3, 30)) ->get(['id', 'default_fornitore_id', 'internal_code', 'name', 'brand', 'model', 'color_label', 'track_serials', 'meta']); if ($products->isEmpty()) { return []; } $productIds = $products->pluck('id')->map(fn($value): int => (int) $value)->all(); $usageByProduct = $this->buildUsageSummary($fornitore, $productIds, $stabileId); $pricingByProduct = $this->buildPurchaseSummary($fornitore, $productIds); return $products ->map(function (Product $product) use ($fornitore, $usageByProduct, $pricingByProduct): array { $offers = $product->offers ?? collect(); $internalOffer = $offers->first(fn(ProductOffer $offer): bool => (bool) ($offer->is_internal ?? false)); $bestExternal = $offers ->filter(fn(ProductOffer $offer): bool => ! (bool) ($offer->is_internal ?? false) && filled($offer->price_amount)) ->sortBy('price_amount') ->first(); $amazonOffer = $offers->first(fn(ProductOffer $offer): bool => in_array((string) ($offer->source_type ?? ''), ['amazon_referral', 'amazon_creators_api'], true)); $identifier = $product->identifiers ->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $fornitore->id) ?? $product->identifiers->first(); $meta = is_array($product->meta ?? null) ? $product->meta : []; $catalogMeta = is_array($meta['catalog'] ?? null) ? $meta['catalog'] : []; $usage = $usageByProduct[(int) $product->id] ?? $this->emptyUsageSummary(); $purchaseSummary = $pricingByProduct[(int) $product->id] ?? $this->emptyPurchaseSummary(); return [ 'id' => (int) $product->id, 'internal_code' => (string) ($product->internal_code ?? ''), 'name' => (string) ($product->name ?? ''), 'brand' => (string) ($product->brand ?? ''), 'model' => (string) ($product->model ?? ''), 'color_label' => (string) ($product->color_label ?? ''), 'track_serials' => (bool) $product->track_serials, 'identifier' => (string) ($identifier->code_value ?? ''), 'identifiers_count' => (int) ($product->identifiers_count ?? 0), 'serials_count' => (int) ($product->serials_count ?? 0), 'media_count' => (int) ($product->media_count ?? 0), 'source' => (string) (($meta['source'] ?? 'manual') ?: 'manual'), 'publication_mode' => (string) (($catalogMeta['publication_mode'] ?? '') ?: ''), 'public_reference' => (string) (($catalogMeta['public_reference'] ?? '') ?: ($product->internal_code ?? '')), 'private_links' => count(array_filter(is_array($catalogMeta['private_source_links'] ?? null) ? $catalogMeta['private_source_links'] : [], fn($value) => filled($value))), 'categories' => (string) (($catalogMeta['categories'] ?? '') ?: ''), 'stock_quantity' => is_numeric($meta['stock']['quantity'] ?? null) ? (int) $meta['stock']['quantity'] : null, 'price_wholesale' => is_numeric($meta['pricing']['wholesale'] ?? null) ? (float) $meta['pricing']['wholesale'] : null, 'offers_count' => $offers->count(), 'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null, 'best_competitor_price' => $bestExternal && filled($bestExternal->price_amount) ? (float) $bestExternal->price_amount : null, 'best_competitor_source' => $bestExternal ? (string) ($bestExternal->source_name ?? $bestExternal->source_type ?? '') : '', 'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? $amazonOffer->external_url ?? '') : '', 'usage_count' => (int) ($usage['usage_count'] ?? 0), 'usage_quantity' => (float) ($usage['usage_quantity'] ?? 0), 'last_used_at' => (string) ($usage['last_used_at'] ?? ''), 'stable_usage_count' => (int) ($usage['stable_usage_count'] ?? 0), 'stable_usage_quantity' => (float) ($usage['stable_usage_quantity'] ?? 0), 'stable_last_used_at' => (string) ($usage['stable_last_used_at'] ?? ''), 'purchase_lines_count' => (int) ($purchaseSummary['purchase_lines_count'] ?? 0), 'last_purchase_total' => $purchaseSummary['last_purchase_total'] ?? null, 'last_purchase_date' => (string) ($purchaseSummary['last_purchase_date'] ?? ''), 'purchase_history_labels' => (array) ($purchaseSummary['purchase_history_labels'] ?? []), 'sort_score' => ((int) ($usage['stable_usage_count'] ?? 0) * 1000) + ((int) round((float) ($usage['stable_usage_quantity'] ?? 0) * 100)) + ((int) ($usage['usage_count'] ?? 0) * 10), ]; }) ->sort(function (array $left, array $right): int { if (($left['sort_score'] ?? 0) === ($right['sort_score'] ?? 0)) { return strcmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? '')); } return (int) ($right['sort_score'] ?? 0) <=> (int) ($left['sort_score'] ?? 0); }) ->take($limit) ->values() ->all(); } /** * @param array $productIds * @return array> */ private function buildUsageSummary(Fornitore $fornitore, array $productIds, ?int $stabileId): array { if ($productIds === [] || ! Schema::hasTable('ticket_interventi')) { return []; } $interventi = TicketIntervento::query() ->with(['ticket:id,stabile_id']) ->where('fornitore_id', (int) $fornitore->id) ->whereNotNull('materiali_utilizzati') ->latest('updated_at') ->limit(250) ->get(['id', 'ticket_id', 'materiali_utilizzati', 'updated_at']); $summary = []; foreach ($interventi as $intervento) { $currentStabileId = (int) ($intervento->ticket?->stabile_id ?? 0); foreach ((array) ($intervento->materiali_utilizzati ?? []) as $item) { if (! is_array($item)) { continue; } $productId = isset($item['product_id']) && is_numeric($item['product_id']) ? (int) $item['product_id'] : 0; if ($productId <= 0 || ! in_array($productId, $productIds, true)) { continue; } $qty = isset($item['qty']) && is_numeric($item['qty']) ? (float) $item['qty'] : 1.0; if (! isset($summary[$productId])) { $summary[$productId] = $this->emptyUsageSummary(); } $summary[$productId]['usage_count']++; $summary[$productId]['usage_quantity'] += $qty; $summary[$productId]['last_used_at'] = $summary[$productId]['last_used_at'] !== '' ? $summary[$productId]['last_used_at'] : (optional($intervento->updated_at)->format('d/m/Y H:i') ?: ''); if ($stabileId !== null && $stabileId > 0 && $currentStabileId === $stabileId) { $summary[$productId]['stable_usage_count']++; $summary[$productId]['stable_usage_quantity'] += $qty; $summary[$productId]['stable_last_used_at'] = $summary[$productId]['stable_last_used_at'] !== '' ? $summary[$productId]['stable_last_used_at'] : (optional($intervento->updated_at)->format('d/m/Y H:i') ?: ''); } } } return $summary; } /** * @param array $productIds * @return array> */ private function buildPurchaseSummary(Fornitore $fornitore, array $productIds): array { if ($productIds === [] || ! Schema::hasTable('contabilita_fatture_fornitori_righe') || ! Schema::hasTable('contabilita_fatture_fornitori')) { return []; } $rows = FatturaFornitoreRiga::query() ->with(['fattura:id,fornitore_id,data_documento,numero_documento']) ->whereIn('product_id', $productIds) ->whereHas('fattura', fn($query) => $query->where('fornitore_id', (int) $fornitore->id)) ->orderByDesc('id') ->limit(300) ->get(['id', 'fattura_id', 'product_id', 'descrizione', 'imponibile_euro', 'totale_euro']); $summary = []; foreach ($rows->groupBy('product_id') as $productId => $group) { $labels = $group ->take(3) ->map(function (FatturaFornitoreRiga $row): string { $date = $row->fattura?->data_documento?->format('d/m/Y') ?: 'data n/d'; $number = trim((string) ($row->fattura?->numero_documento ?? 'riga #' . $row->id)); $total = is_numeric($row->totale_euro) ? number_format((float) $row->totale_euro, 2, ',', '.') . ' EUR' : 'totale n/d'; return $date . ' · ' . $number . ' · ' . $total; }) ->values() ->all(); /** @var FatturaFornitoreRiga|null $latest */ $latest = $group->first(); $summary[(int) $productId] = [ 'purchase_lines_count' => $group->count(), 'last_purchase_total' => $latest && is_numeric($latest->totale_euro) ? (float) $latest->totale_euro : null, 'last_purchase_date' => $latest?->fattura?->data_documento?->format('d/m/Y') ?: '', 'purchase_history_labels' => $labels, ]; } return $summary; } /** @return array */ private function emptyUsageSummary(): array { return [ 'usage_count' => 0, 'usage_quantity' => 0.0, 'last_used_at' => '', 'stable_usage_count' => 0, 'stable_usage_quantity' => 0.0, 'stable_last_used_at' => '', ]; } /** @return array */ private function emptyPurchaseSummary(): array { return [ 'purchase_lines_count' => 0, 'last_purchase_total' => null, 'last_purchase_date' => '', 'purchase_history_labels' => [], ]; } }