feat(catalog): add shared product hub panels
This commit is contained in:
parent
11bc06a8e4
commit
d0b1e58b46
|
|
@ -18,6 +18,7 @@
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
use App\Models\TicketInterventoSessione;
|
use App\Models\TicketInterventoSessione;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Catalog\ProductHubViewService;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
|
@ -93,6 +94,12 @@ class TicketInterventoScheda extends Page
|
||||||
/** @var array<int, array<string, mixed>> */
|
/** @var array<int, array<string, mixed>> */
|
||||||
public array $historicalMaterialRows = [];
|
public array $historicalMaterialRows = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $ticketProductHubRows = [];
|
||||||
|
|
||||||
|
/** @var array<int, array<string, mixed>> */
|
||||||
|
public array $stabileInstalledProductRows = [];
|
||||||
|
|
||||||
public string $apparatoMarca = '';
|
public string $apparatoMarca = '';
|
||||||
|
|
||||||
public string $apparatoModello = '';
|
public string $apparatoModello = '';
|
||||||
|
|
@ -999,9 +1006,23 @@ protected function reloadIntervento(): void
|
||||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
||||||
$this->loadAccessSummary();
|
$this->loadAccessSummary();
|
||||||
$this->loadMaterialMemory();
|
$this->loadMaterialMemory();
|
||||||
|
$this->loadTicketProductHubRows();
|
||||||
$this->qrToken = '';
|
$this->qrToken = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function loadTicketProductHubRows(): void
|
||||||
|
{
|
||||||
|
$stabileId = (int) ($this->intervento->ticket?->stabile_id ?? 0) ?: null;
|
||||||
|
|
||||||
|
$this->ticketProductHubRows = app(ProductHubViewService::class)
|
||||||
|
->buildRows($this->fornitore, $stabileId, 12);
|
||||||
|
|
||||||
|
$this->stabileInstalledProductRows = array_values(array_filter(
|
||||||
|
$this->ticketProductHubRows,
|
||||||
|
fn(array $row): bool => ((float) ($row['stable_usage_quantity'] ?? 0)) > 0 || ((int) ($row['stable_usage_count'] ?? 0)) > 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
protected function getActiveSession(): ?TicketInterventoSessione
|
protected function getActiveSession(): ?TicketInterventoSessione
|
||||||
{
|
{
|
||||||
if (! $this->sessionTrackingAvailable) {
|
if (! $this->sessionTrackingAvailable) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
|
||||||
use App\Modules\Contabilita\Models\PianoConti;
|
use App\Modules\Contabilita\Models\PianoConti;
|
||||||
use App\Services\Catalog\FornitoreProductCatalogService;
|
use App\Services\Catalog\FornitoreProductCatalogService;
|
||||||
|
use App\Services\Catalog\ProductHubViewService;
|
||||||
use App\Services\Catalog\ProductAssetIngestionService;
|
use App\Services\Catalog\ProductAssetIngestionService;
|
||||||
use App\Services\Catalog\ProductOfferService;
|
use App\Services\Catalog\ProductOfferService;
|
||||||
use App\Services\Consumi\AcquaPdfTextParser;
|
use App\Services\Consumi\AcquaPdfTextParser;
|
||||||
|
|
@ -590,57 +591,8 @@ private function refreshCatalogRows(): void
|
||||||
$this->catalogoProdottiRows = [];
|
$this->catalogoProdottiRows = [];
|
||||||
$this->tecnorepairRows = [];
|
$this->tecnorepairRows = [];
|
||||||
|
|
||||||
if (Schema::hasTable('products')) {
|
$this->catalogoProdottiRows = app(ProductHubViewService::class)
|
||||||
$query = Product::query()
|
->buildRows($this->fornitore, null, 20);
|
||||||
->withCount(['identifiers', 'serials', 'media'])
|
|
||||||
->where('default_fornitore_id', (int) $this->fornitore->id)
|
|
||||||
->orderBy('name')
|
|
||||||
->limit(20);
|
|
||||||
|
|
||||||
if (Schema::hasTable('product_offers')) {
|
|
||||||
$query->with(['offers' => function ($offerQuery): void {
|
|
||||||
$offerQuery->where('is_active', true)->orderBy('price_amount');
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->catalogoProdottiRows = $query
|
|
||||||
->get(['id', 'internal_code', 'name', 'brand', 'model', 'color_label', 'track_serials', 'meta'])
|
|
||||||
->map(function (Product $product): array {
|
|
||||||
$offers = Schema::hasTable('product_offers') ? ($product->offers ?? collect()) : collect();
|
|
||||||
$internalOffer = $offers->first(fn($offer) => (bool) ($offer->is_internal ?? false));
|
|
||||||
$bestCompetitor = $offers
|
|
||||||
->filter(fn($offer) => ! (bool) ($offer->is_internal ?? false) && filled($offer->price_amount))
|
|
||||||
->sortBy('price_amount')
|
|
||||||
->first();
|
|
||||||
$amazonOffer = $offers->first(fn($offer) => in_array((string) ($offer->source_type ?? ''), ['amazon_referral', 'amazon_creators_api'], true));
|
|
||||||
|
|
||||||
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,
|
|
||||||
'identifiers_count' => (int) ($product->identifiers_count ?? 0),
|
|
||||||
'serials_count' => (int) ($product->serials_count ?? 0),
|
|
||||||
'media_count' => (int) ($product->media_count ?? 0),
|
|
||||||
'source' => (string) (($product->meta['source'] ?? 'manual') ?: 'manual'),
|
|
||||||
'publication_mode' => (string) (($product->meta['catalog']['publication_mode'] ?? '') ?: ''),
|
|
||||||
'public_reference' => (string) (($product->meta['catalog']['public_reference'] ?? '') ?: ($product->internal_code ?? '')),
|
|
||||||
'private_links' => count(array_filter(is_array($product->meta['catalog']['private_source_links'] ?? null) ? $product->meta['catalog']['private_source_links'] : [], fn($value) => filled($value))),
|
|
||||||
'categories' => (string) (($product->meta['catalog']['categories'] ?? '') ?: ''),
|
|
||||||
'stock_quantity' => is_numeric($product->meta['stock']['quantity'] ?? null) ? (int) $product->meta['stock']['quantity'] : null,
|
|
||||||
'price_wholesale' => is_numeric($product->meta['pricing']['wholesale'] ?? null) ? (float) $product->meta['pricing']['wholesale'] : null,
|
|
||||||
'offers_count' => $offers->count(),
|
|
||||||
'own_offer_price' => $internalOffer && filled($internalOffer->price_amount) ? (float) $internalOffer->price_amount : null,
|
|
||||||
'best_competitor_price' => $bestCompetitor && filled($bestCompetitor->price_amount) ? (float) $bestCompetitor->price_amount : null,
|
|
||||||
'best_competitor_source' => $bestCompetitor ? (string) ($bestCompetitor->source_name ?? $bestCompetitor->source_type ?? '') : '',
|
|
||||||
'amazon_referral_url' => $amazonOffer ? (string) ($amazonOffer->referral_url ?? '') : '',
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) {
|
if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) {
|
||||||
$this->tecnorepairRows = $this->fornitore->tecnorepairSchede()
|
$this->tecnorepairRows = $this->fornitore->tecnorepairSchede()
|
||||||
|
|
|
||||||
250
app/Services/Catalog/ProductHubViewService.php
Normal file
250
app/Services/Catalog/ProductHubViewService.php
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Services\Catalog;
|
||||||
|
|
||||||
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\ProductIdentifier;
|
||||||
|
use App\Models\ProductOffer;
|
||||||
|
use App\Models\TicketIntervento;
|
||||||
|
use App\Modules\Contabilita\Models\FatturaFornitoreRiga;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class ProductHubViewService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
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<int, int> $productIds
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
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<int, int> $productIds
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
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<string, mixed> */
|
||||||
|
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<string, mixed> */
|
||||||
|
private function emptyPurchaseSummary(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchase_lines_count' => 0,
|
||||||
|
'last_purchase_total' => null,
|
||||||
|
'last_purchase_date' => '',
|
||||||
|
'purchase_history_labels' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
@php
|
||||||
|
$panelTitle = $title ?? 'Hub prodotti';
|
||||||
|
$panelDescription = $description ?? '';
|
||||||
|
$panelRows = is_array($rows ?? null) ? $rows : [];
|
||||||
|
$panelEmptyMessage = $emptyMessage ?? 'Nessun prodotto disponibile.';
|
||||||
|
$showStableUsage = (bool) ($showStableUsage ?? false);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="rounded-xl border bg-white p-4">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $panelTitle }}</div>
|
||||||
|
@if($panelDescription !== '')
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ $panelDescription }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-700">{{ count($panelRows) }} prodotti</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($panelRows === [])
|
||||||
|
<div class="mt-4 rounded-lg border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-sm text-slate-500">{{ $panelEmptyMessage }}</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-4 grid gap-3 xl:grid-cols-2">
|
||||||
|
@foreach($panelRows as $row)
|
||||||
|
<article class="rounded-xl border border-slate-200 bg-slate-50 p-4 shadow-sm">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white">{{ $row['internal_code'] !== '' ? $row['internal_code'] : ('PROD-' . $row['id']) }}</span>
|
||||||
|
@if(($row['identifier'] ?? '') !== '')
|
||||||
|
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] font-medium text-slate-700">{{ $row['identifier'] }}</span>
|
||||||
|
@endif
|
||||||
|
@if(($row['track_serials'] ?? false))
|
||||||
|
<span class="rounded-full bg-sky-100 px-2.5 py-1 text-[11px] font-medium text-sky-800">seriali</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-sm font-semibold text-slate-900">{{ $row['name'] !== '' ? $row['name'] : 'Prodotto senza nome' }}</div>
|
||||||
|
<div class="mt-1 text-xs text-slate-500">{{ trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? '')) !== '' ? trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? '')) : 'Marca/modello non disponibili' }}</div>
|
||||||
|
@if(($row['color_label'] ?? '') !== '')
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">Colore: {{ $row['color_label'] }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-white px-3 py-3 text-xs text-slate-700">
|
||||||
|
<div class="font-semibold uppercase tracking-wide text-slate-500">Giacenza e utilizzo</div>
|
||||||
|
<div class="mt-2">Giacenza nota: {{ ($row['stock_quantity'] ?? null) !== null ? (int) $row['stock_quantity'] : 'n/d' }}</div>
|
||||||
|
<div class="mt-1">Usato nei ticket: {{ rtrim(rtrim(number_format((float) ($row['usage_quantity'] ?? 0), 2, ',', ''), '0'), ',') ?: '0' }}</div>
|
||||||
|
@if($showStableUsage)
|
||||||
|
<div class="mt-1">Installato nello stabile: {{ rtrim(rtrim(number_format((float) ($row['stable_usage_quantity'] ?? 0), 2, ',', ''), '0'), ',') ?: '0' }}</div>
|
||||||
|
@endif
|
||||||
|
@if(($row['last_used_at'] ?? '') !== '')
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">Ultimo uso: {{ $row['last_used_at'] }}</div>
|
||||||
|
@endif
|
||||||
|
@if($showStableUsage && ($row['stable_last_used_at'] ?? '') !== '')
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">Ultimo uso stabile: {{ $row['stable_last_used_at'] }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-white px-3 py-3 text-xs text-slate-700">
|
||||||
|
<div class="font-semibold uppercase tracking-wide text-slate-500">Prezzi</div>
|
||||||
|
<div class="mt-2">Offerta interna: {{ ($row['own_offer_price'] ?? null) !== null ? ('€ ' . number_format((float) $row['own_offer_price'], 2, ',', '.')) : 'n/d' }}</div>
|
||||||
|
<div class="mt-1">Miglior esterno: {{ ($row['best_competitor_price'] ?? null) !== null ? ('€ ' . number_format((float) $row['best_competitor_price'], 2, ',', '.')) : 'n/d' }}{{ ($row['best_competitor_source'] ?? '') !== '' ? (' · ' . $row['best_competitor_source']) : '' }}</div>
|
||||||
|
<div class="mt-1">Ultimo acquisto: {{ ($row['last_purchase_total'] ?? null) !== null ? ('€ ' . number_format((float) $row['last_purchase_total'], 2, ',', '.')) : 'n/d' }}{{ ($row['last_purchase_date'] ?? '') !== '' ? (' · ' . $row['last_purchase_date']) : '' }}</div>
|
||||||
|
@if(($row['price_wholesale'] ?? null) !== null)
|
||||||
|
<div class="mt-1 text-[11px] text-slate-500">Wholesale salvato: € {{ number_format((float) $row['price_wholesale'], 2, ',', '.') }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($row['purchase_history_labels']))
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Storico acquisti registrato</div>
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
@foreach($row['purchase_history_labels'] as $label)
|
||||||
|
<span class="rounded-full bg-white px-2.5 py-1 text-[11px] text-slate-600 ring-1 ring-slate-200">{{ $label }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2 text-[11px] text-slate-500">
|
||||||
|
<span>Offerte: {{ (int) ($row['offers_count'] ?? 0) }}</span>
|
||||||
|
<span>Codici: {{ (int) ($row['identifiers_count'] ?? 0) }}</span>
|
||||||
|
<span>Seriali: {{ (int) ($row['serials_count'] ?? 0) }}</span>
|
||||||
|
<span>Media: {{ (int) ($row['media_count'] ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
|
@if(($row['amazon_referral_url'] ?? '') !== '')
|
||||||
|
<a href="{{ $row['amazon_referral_url'] }}" target="_blank" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50">Apri Amazon</a>
|
||||||
|
@endif
|
||||||
|
@if(($row['private_links'] ?? 0) > 0)
|
||||||
|
<span class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700">{{ (int) $row['private_links'] }} link sorgente</span>
|
||||||
|
@endif
|
||||||
|
@if(($row['categories'] ?? '') !== '')
|
||||||
|
<span class="inline-flex items-center rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium text-slate-700">{{ $row['categories'] }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<button type="button" x-on:click="activeTab = 'scheda'" x-bind:class="activeTab === 'scheda' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Segnalazione</button>
|
<button type="button" x-on:click="activeTab = 'scheda'" x-bind:class="activeTab === 'scheda' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Segnalazione</button>
|
||||||
<button type="button" x-on:click="activeTab = 'operativo'" x-bind:class="activeTab === 'operativo' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Operativo</button>
|
<button type="button" x-on:click="activeTab = 'operativo'" x-bind:class="activeTab === 'operativo' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Operativo</button>
|
||||||
|
<button type="button" x-on:click="activeTab = 'prodotti'" x-bind:class="activeTab === 'prodotti' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Prodotti</button>
|
||||||
<button type="button" x-on:click="activeTab = 'allegati'" x-bind:class="activeTab === 'allegati' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Allegati</button>
|
<button type="button" x-on:click="activeTab = 'allegati'" x-bind:class="activeTab === 'allegati' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Allegati</button>
|
||||||
<button type="button" x-on:click="activeTab = 'storico'" x-bind:class="activeTab === 'storico' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Storico</button>
|
<button type="button" x-on:click="activeTab = 'storico'" x-bind:class="activeTab === 'storico' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Storico</button>
|
||||||
<div class="ml-auto text-[11px] uppercase tracking-wide text-slate-500">Vista compatta TecnoRepair-like</div>
|
<div class="ml-auto text-[11px] uppercase tracking-wide text-slate-500">Vista compatta TecnoRepair-like</div>
|
||||||
|
|
@ -666,6 +667,26 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4" x-show="activeTab === 'prodotti'" x-cloak>
|
||||||
|
@if(count($stabileInstalledProductRows) > 0)
|
||||||
|
@include('filament.components.catalog.product-hub-panel', [
|
||||||
|
'title' => 'Prodotti già installati nello stabile',
|
||||||
|
'description' => 'Riepilogo calcolato dai ticket/interventi già registrati per questo stabile. Serve per riusare lampade, ricambi e codici coerenti con lo storico.',
|
||||||
|
'rows' => $stabileInstalledProductRows,
|
||||||
|
'emptyMessage' => 'Nessun prodotto installato rilevato nello storico ticket di questo stabile.',
|
||||||
|
'showStableUsage' => true,
|
||||||
|
])
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@include('filament.components.catalog.product-hub-panel', [
|
||||||
|
'title' => 'Hub catalogo per questo ticket',
|
||||||
|
'description' => 'Catalogo fornitore riusabile dal ticket: prezzi correnti, ultime righe acquisto, giacenza nota e riferimento Amazon quando disponibile.',
|
||||||
|
'rows' => $ticketProductHubRows,
|
||||||
|
'emptyMessage' => 'Nessun prodotto catalogato disponibile per questo fornitore.',
|
||||||
|
'showStableUsage' => true,
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border bg-white p-4" x-show="activeTab === 'allegati'" x-cloak>
|
<div class="rounded-xl border bg-white p-4" x-show="activeTab === 'allegati'" x-cloak>
|
||||||
<div class="text-sm font-semibold">Allegati ticket</div>
|
<div class="text-sm font-semibold">Allegati ticket</div>
|
||||||
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
|
|
||||||
|
|
@ -334,101 +334,14 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 overflow-x-auto">
|
<div class="mt-4">
|
||||||
<table class="min-w-full border-collapse border text-xs">
|
@include('filament.components.catalog.product-hub-panel', [
|
||||||
<thead>
|
'title' => 'Hub prodotti fornitore',
|
||||||
<tr class="bg-slate-100 text-slate-700">
|
'description' => 'Vista trasversale pronta da riusare anche nei ticket: giacenza nota, prezzi correnti, ultime righe acquisto e storico installazioni da ticket.',
|
||||||
<th class="border px-2 py-1.5 text-left">Codice</th>
|
'rows' => $catalogoProdottiRows,
|
||||||
<th class="border px-2 py-1.5 text-left">Prodotto</th>
|
'emptyMessage' => 'Nessun prodotto presente per questo fornitore.',
|
||||||
<th class="border px-2 py-1.5 text-left">Marca / modello</th>
|
'showStableUsage' => false,
|
||||||
<th class="border px-2 py-1.5 text-left">Origine</th>
|
])
|
||||||
<th class="border px-2 py-1.5 text-left">Rif. interno</th>
|
|
||||||
<th class="border px-2 py-1.5 text-left">Codici</th>
|
|
||||||
<th class="border px-2 py-1.5 text-left">Seriali</th>
|
|
||||||
<th class="border px-2 py-1.5 text-left">Offerte</th>
|
|
||||||
<th class="border px-2 py-1.5 text-left">Catalogo</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@forelse($catalogoProdottiRows as $row)
|
|
||||||
<tr class="hover:bg-slate-50">
|
|
||||||
<td class="border px-2 py-1.5 font-medium">{{ $row['internal_code'] !== '' ? $row['internal_code'] : '-' }}</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
<div>{{ $row['name'] !== '' ? $row['name'] : '-' }}</div>
|
|
||||||
@if($row['color_label'] !== '')
|
|
||||||
<div class="text-[11px] text-slate-500">Colore: {{ $row['color_label'] }}</div>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="border px-2 py-1.5">{{ trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? '')) ?: '-' }}</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
@if(($row['source'] ?? '') === 'fattura_elettronica')
|
|
||||||
<span class="rounded bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-800">FE</span>
|
|
||||||
@elseif(($row['source'] ?? '') === 'ncom_wholesale_csv')
|
|
||||||
<span class="rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-800">CSV</span>
|
|
||||||
@else
|
|
||||||
<span class="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-semibold text-slate-700">manuale</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
<div class="font-mono">{{ $row['public_reference'] !== '' ? $row['public_reference'] : '-' }}</div>
|
|
||||||
@if(($row['publication_mode'] ?? '') === 'internal_only')
|
|
||||||
<div class="text-[11px] text-slate-500">solo interno</div>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="border px-2 py-1.5">{{ (int) ($row['identifiers_count'] ?? 0) }}</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
{{ (int) ($row['serials_count'] ?? 0) }}
|
|
||||||
@if($row['track_serials'])
|
|
||||||
<span class="ml-1 rounded bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-800">track</span>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
<div>{{ (int) ($row['offers_count'] ?? 0) }} totali</div>
|
|
||||||
<div class="text-[11px] text-slate-500">
|
|
||||||
@if(($row['own_offer_price'] ?? null) !== null)
|
|
||||||
tuo € {{ number_format((float) $row['own_offer_price'], 2, ',', '.') }}
|
|
||||||
@else
|
|
||||||
tuo n/d
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="text-[11px] text-slate-500">
|
|
||||||
@if(($row['best_competitor_price'] ?? null) !== null)
|
|
||||||
competitor € {{ number_format((float) $row['best_competitor_price'], 2, ',', '.') }}
|
|
||||||
@if(($row['best_competitor_source'] ?? '') !== '')
|
|
||||||
| {{ $row['best_competitor_source'] }}
|
|
||||||
@endif
|
|
||||||
@else
|
|
||||||
competitor n/d
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if(($row['amazon_referral_url'] ?? '') !== '')
|
|
||||||
<div class="text-[11px] text-slate-500">Amazon referral pronto</div>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
<td class="border px-2 py-1.5">
|
|
||||||
<div>Link sorgente: {{ (int) ($row['private_links'] ?? 0) }}</div>
|
|
||||||
<div class="text-[11px] text-slate-500">
|
|
||||||
@if(($row['price_wholesale'] ?? null) !== null)
|
|
||||||
€ {{ number_format((float) $row['price_wholesale'], 2, ',', '.') }}
|
|
||||||
@else
|
|
||||||
prezzo n/d
|
|
||||||
@endif
|
|
||||||
@if(($row['stock_quantity'] ?? null) !== null)
|
|
||||||
| stock {{ (int) $row['stock_quantity'] }}
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@if(($row['categories'] ?? '') !== '')
|
|
||||||
<div class="truncate text-[11px] text-slate-500">{{ $row['categories'] }}</div>
|
|
||||||
@endif
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
@empty
|
|
||||||
<tr>
|
|
||||||
<td colspan="9" class="border px-2 py-3 text-center text-gray-500">Nessun prodotto presente per questo fornitore.</td>
|
|
||||||
</tr>
|
|
||||||
@endforelse
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -437,6 +350,7 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium"
|
||||||
<div class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2">
|
<div class="mt-2 grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||||
<div><span class="font-medium">IBAN:</span> {{ $fornitore->iban ?? '-' }}</div>
|
<div><span class="font-medium">IBAN:</span> {{ $fornitore->iban ?? '-' }}</div>
|
||||||
<div><span class="font-medium">BIC:</span> {{ $fornitore->bic ?? '-' }}</div>
|
<div><span class="font-medium">BIC:</span> {{ $fornitore->bic ?? '-' }}</div>
|
||||||
|
<div><span class="font-medium">SIA:</span> {{ $fornitore->sia_effettivo ?? '-' }}</div>
|
||||||
<div class="md:col-span-2"><span class="font-medium">Modalità predefinita:</span> {{ $fornitore->modalita_pagamento_predefinita ?? '-' }}</div>
|
<div class="md:col-span-2"><span class="font-medium">Modalità predefinita:</span> {{ $fornitore->modalita_pagamento_predefinita ?? '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user