netgescon-day0/app/Filament/Pages/Gescon/FornitoreScheda.php

1165 lines
46 KiB
PHP
Executable File

<?php
namespace App\Filament\Pages\Gescon;
use App\Filament\Pages\Contabilita\FornitoriArchivio as ContabilitaFornitoriArchivio;
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
use App\Filament\Pages\Fornitore\ProdottiCatalogo;
use App\Filament\Pages\Fornitore\SerialiCatalogo;
use App\Models\Documento;
use App\Models\FatturaElettronica;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\FornitoreStabileImpostazione;
use App\Models\Product;
use App\Models\RegistroRitenuteAcconto;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
use App\Models\StabileServizio;
use App\Models\StgFatturaAde;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Modules\Contabilita\Models\FatturaFornitore as ContabilitaFatturaFornitore;
use App\Modules\Contabilita\Models\PianoConti;
use App\Services\Catalog\FornitoreProductCatalogService;
use App\Services\Catalog\ProductAssetIngestionService;
use App\Services\Catalog\ProductHubViewService;
use App\Services\Catalog\ProductOfferService;
use App\Services\Consumi\AcquaPdfTextParser;
use App\Services\Consumi\ConsumiAcquaIngestionService;
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
use App\Services\Documenti\PdfTextExtractionService;
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
use App\Support\Livewire\SupportsRubricaLookup;
use App\Support\StabileContext;
use Filament\Actions\Action;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class FornitoreScheda extends Page
{
use ResolvesOperatoreContext;
use SupportsRubricaLookup;
protected static ?string $title = 'Scheda fornitore';
protected static ?string $slug = 'anagrafica/fornitori/{record}';
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.pages.gescon.fornitore-scheda';
public Fornitore $fornitore;
/** @var array<string, mixed> */
public array $box = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaAde = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaFe = [];
/** @var array<int, array<string, mixed>> */
public array $anteprimaContabilita = [];
/** @var array<int, array<string, mixed>> */
public array $fattureAssociate = [];
/** @var array<int, array<string, mixed>> */
public array $raVersateRows = [];
/** @var array<int, array<string, mixed>> */
public array $catalogoProdottiRows = [];
/** @var array<int, array<string, mixed>> */
public array $tecnorepairRows = [];
public string $tagsInput = '';
/** @var array<int, string> */
public array $tagSuggestions = [];
/** @var array<int, array<string, mixed>> */
public array $dipendentiRows = [];
public string $nuovoDipendenteNome = '';
public string $nuovoDipendenteCognome = '';
public string $nuovoDipendenteEmail = '';
public string $nuovoDipendenteTelefono = '';
public string $dipendenteRubricaSearch = '';
public ?int $dipendenteRubricaId = null;
public string $sectionTab = 'profilo';
/** @var array<int, array<string, mixed>> */
public array $dipendenteRubricaMatches = [];
public ?string $lastGeneratedPassword = null;
public function cleanDisplayValue(?string $value, string $fallback = '-'): string
{
$v = trim((string) $value);
if ($v === '') {
return $fallback;
}
$upper = strtoupper($v);
if (in_array($upper, ['NULL', 'N/D', 'ND', 'N.A.', '-', '--', '[]', '{}'], true)) {
return $fallback;
}
return $v;
}
public function mount(int | string $record): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore', 'fornitore'])) {
abort(403);
}
if ($user->hasRole('fornitore') && ! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) {
[$resolvedFornitore] = $this->resolveOperatoreContext((int) $record);
$this->fornitore = $resolvedFornitore->loadMissing(['rubrica', 'amministratore']);
} else {
$this->fornitore = Fornitore::query()->with(['rubrica', 'amministratore'])->findOrFail((int) $record);
// Tenant guard: impedisce accesso cross-amministratore via URL.
if (! $user->hasAnyRole(['super-admin', 'admin'])) {
$allowedAdminIds = [];
$activeStabile = StabileContext::getActiveStabile($user);
$activeAdminId = (int) ($activeStabile?->amministratore_id ?: 0);
if ($activeAdminId > 0) {
$allowedAdminIds[] = $activeAdminId;
}
$userAdminId = (int) ($user->amministratore?->id ?: 0);
if ($userAdminId > 0) {
$allowedAdminIds[] = $userAdminId;
}
$ownerAdminId = (int) ($user->amministratoreOwner?->id ?: 0);
if ($ownerAdminId > 0) {
$allowedAdminIds[] = $ownerAdminId;
}
// Collaboratori: includi anche gli amministratori degli stabili assegnati.
try {
$assigned = $user->stabiliAssegnati()->pluck('amministratore_id')->all();
foreach ($assigned as $aid) {
$aid = (int) $aid;
if ($aid > 0) {
$allowedAdminIds[] = $aid;
}
}
} catch (\Throwable) {
// ignore
}
$allowedAdminIds = array_values(array_unique(array_filter($allowedAdminIds, fn($v) => (int) $v > 0)));
$fornitoreAdminIds = array_values(array_unique(array_filter([
(int) ($this->fornitore->amministratore_id ?? 0),
(int) ($this->fornitore->rubrica?->amministratore_id ?? 0),
], fn($v) => (int) $v > 0)));
if ($fornitoreAdminIds === []) {
if (! $user->hasAnyRole(['admin', 'amministratore'])) {
abort(404);
}
} else {
$hasIntersection = count(array_intersect($fornitoreAdminIds, $allowedAdminIds)) > 0;
if (! $hasIntersection) {
abort(404);
}
}
}
}
$this->hydrateBoxData($user);
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
$this->refreshDipendentiRows();
$this->refreshCatalogRows();
$requestedTab = (string) request()->query('tab', 'profilo');
if ($this->isValidSectionTab($requestedTab)) {
$this->sectionTab = $requestedTab;
}
}
public function setSectionTab(string $tab): void
{
if ($this->isValidSectionTab($tab)) {
$this->sectionTab = $tab;
}
}
public function getProdottiHubUrl(): string
{
return ProdottiCatalogo::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
}
public function getSerialiHubUrl(): string
{
return SerialiCatalogo::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
}
public function getContabilitaFornitoriUrl(): string
{
return ContabilitaFornitoriArchivio::getUrl(panel: 'admin-filament');
}
private function isValidSectionTab(string $tab): bool
{
return in_array($tab, ['profilo', 'accessi', 'assistenza', 'catalogo', 'contabilita'], true);
}
public function creaDipendenteFornitore(): void
{
$nome = trim($this->nuovoDipendenteNome);
$email = mb_strtolower(trim($this->nuovoDipendenteEmail));
if ($nome === '' || $email === '') {
Notification::make()->title('Nome ed email sono obbligatori')->warning()->send();
return;
}
$exists = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->whereRaw('LOWER(email) = ?', [$email])
->exists();
if ($exists) {
Notification::make()->title('Dipendente gia presente per questo fornitore')->warning()->send();
return;
}
FornitoreDipendente::query()->create([
'fornitore_id' => (int) $this->fornitore->id,
'nome' => $nome,
'cognome' => trim($this->nuovoDipendenteCognome) ?: null,
'email' => $email,
'telefono' => trim($this->nuovoDipendenteTelefono) ?: null,
'attivo' => true,
'created_by_user_id' => Auth::id(),
'updated_by_user_id' => Auth::id(),
]);
$this->nuovoDipendenteNome = '';
$this->nuovoDipendenteCognome = '';
$this->nuovoDipendenteEmail = '';
$this->nuovoDipendenteTelefono = '';
$this->refreshDipendentiRows();
Notification::make()->title('Dipendente aggiunto')->success()->send();
}
public function updatedDipendenteRubricaSearch(): void
{
$this->searchDipendenteRubricaMatches();
}
public function selezionaDipendenteRubrica(int $rubricaId): void
{
$rubrica = RubricaUniversale::query()->find($rubricaId);
if (! $rubrica instanceof RubricaUniversale) {
return;
}
$this->dipendenteRubricaId = (int) $rubrica->id;
$this->dipendenteRubricaSearch = $this->formatRubricaLookupLabel($rubrica);
$this->dipendenteRubricaMatches = [$this->mapRubricaLookupRow($rubrica)];
}
public function resetDipendenteRubricaSelection(): void
{
$this->dipendenteRubricaId = null;
$this->dipendenteRubricaSearch = '';
$this->dipendenteRubricaMatches = [];
}
public function collegaDipendenteDaRubrica(): void
{
$rubricaId = (int) ($this->dipendenteRubricaId ?? 0);
if ($rubricaId <= 0) {
Notification::make()->title('Seleziona un nominativo rubrica')->warning()->send();
return;
}
$rubrica = RubricaUniversale::query()->find($rubricaId);
if (! $rubrica instanceof RubricaUniversale) {
Notification::make()->title('Nominativo rubrica non trovato')->danger()->send();
return;
}
$email = mb_strtolower(trim((string) ($rubrica->email ?? '')));
$query = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->where(function ($builder) use ($rubricaId, $email, $rubrica): void {
$builder->where('rubrica_id', $rubricaId);
if ($email !== '') {
$builder->orWhereRaw('LOWER(email) = ?', [$email]);
}
$builder->orWhere(function ($sub) use ($rubrica): void {
$sub->where('nome', (string) ($rubrica->nome ?? ''))
->where('cognome', (string) ($rubrica->cognome ?? ''));
});
});
$dipendente = $query->first();
if (! $dipendente instanceof FornitoreDipendente) {
$dipendente = new FornitoreDipendente();
$dipendente->fornitore_id = (int) $this->fornitore->id;
$dipendente->created_by_user_id = Auth::id();
}
$dipendente->rubrica_id = $rubricaId;
$dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente');
$dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null;
$dipendente->email = $email !== '' ? $email : null;
$dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null;
$dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->resetDipendenteRubricaSelection();
$this->refreshDipendentiRows();
Notification::make()->title('Dipendente collegato dalla rubrica')->success()->send();
}
public function abilitaAccessoDipendente(int $dipendenteId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->find($dipendenteId);
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Dipendente non trovato')->danger()->send();
return;
}
$email = trim((string) ($dipendente->email ?? ''));
if ($email === '') {
Notification::make()->title('Email dipendente mancante')->warning()->send();
return;
}
$targetUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
$created = false;
if (! $targetUser instanceof User) {
$generatedPassword = Str::random(12);
$targetUser = User::query()->create([
'name' => trim((string) ($dipendente->nome_completo ?: 'Utente fornitore')),
'email' => $email,
'password' => Hash::make($generatedPassword),
'email_verified_at' => now(),
'is_active' => true,
]);
$this->lastGeneratedPassword = $generatedPassword;
$created = true;
}
$targetUser->assignRole('fornitore');
$dipendente->user_id = (int) $targetUser->id;
$dipendente->attivo = true;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->refreshDipendentiRows();
Notification::make()
->title('Accesso dipendente abilitato')
->body($created
? 'Accesso creato. Password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)')
: 'Accesso collegato ad utente esistente.')
->success()
->send();
}
public function resetPasswordDipendenteUser(int $userId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->where('user_id', $userId)
->first();
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Utente non collegato a questo fornitore')->danger()->send();
return;
}
$target = User::query()->find($userId);
if (! $target instanceof User) {
Notification::make()->title('Utente non trovato')->danger()->send();
return;
}
$newPassword = Str::random(12);
$target->password = Hash::make($newPassword);
$target->save();
$this->lastGeneratedPassword = $newPassword;
Notification::make()
->title('Password dipendente resettata')
->body('Nuova password temporanea: ' . $newPassword)
->success()
->send();
}
public function toggleDipendenteAttivo(int $dipendenteId): void
{
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->find($dipendenteId);
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Dipendente non trovato')->danger()->send();
return;
}
$dipendente->attivo = ! (bool) $dipendente->attivo;
$dipendente->updated_by_user_id = Auth::id();
$dipendente->save();
$this->refreshDipendentiRows();
Notification::make()->title('Stato dipendente aggiornato')->success()->send();
}
public function saveTags(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
Notification::make()->title('Colonna tags non presente')->warning()->send();
return;
}
$normalized = $this->normalizeTags($this->tagsInput);
$this->fornitore->tags = implode(', ', $normalized);
$this->fornitore->save();
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
Notification::make()->title('Tag fornitore salvati')->success()->send();
}
public function importLegacyTags(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
Notification::make()->title('Colonna tags non presente')->warning()->send();
return;
}
try {
Artisan::call('fornitori:import-legacy-tags', [
'--fornitore-id' => [(int) $this->fornitore->id],
]);
} catch (\Throwable $e) {
Notification::make()->title('Import tag legacy fallito')->body($e->getMessage())->danger()->send();
return;
}
$this->fornitore->refresh();
$this->tagsInput = (string) ($this->fornitore->tags ?? '');
$this->loadTagSuggestions();
Notification::make()->title('Tag legacy importati')->success()->send();
}
public function syncRubricaFromFornitore(): void
{
$payload = [
'tipo_contatto' => 'persona_giuridica',
'categoria' => 'fornitore',
'stato' => 'attivo',
'ragione_sociale' => $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')),
'nome' => $this->fornitore->nome,
'cognome' => $this->fornitore->cognome,
'partita_iva' => $this->fornitore->partita_iva,
'codice_fiscale' => $this->fornitore->codice_fiscale,
'indirizzo' => $this->fornitore->indirizzo,
'civico' => $this->fornitore->civico,
'cap' => $this->fornitore->cap,
'citta' => $this->fornitore->citta,
'provincia' => $this->fornitore->provincia,
'nazione' => $this->fornitore->nazione,
'email' => $this->fornitore->email,
'pec' => $this->fornitore->pec,
'telefono_ufficio' => $this->fornitore->telefono,
'telefono_cellulare' => $this->fornitore->cellulare,
'sito_web' => $this->fornitore->sito_web,
'note' => $this->fornitore->note,
'data_ultima_modifica' => now()->toDateString(),
];
$rubrica = null;
if ((int) ($this->fornitore->rubrica_id ?? 0) > 0) {
$rubrica = RubricaUniversale::query()->find((int) $this->fornitore->rubrica_id);
}
if (! $rubrica instanceof RubricaUniversale) {
$rubrica = RubricaUniversale::query()->create(array_merge($payload, [
'data_inserimento' => now()->toDateString(),
]));
} else {
$rubrica->fill($payload);
$rubrica->save();
}
if ((int) ($this->fornitore->rubrica_id ?? 0) !== (int) $rubrica->id) {
$this->fornitore->rubrica_id = (int) $rubrica->id;
$this->fornitore->save();
}
$this->fornitore->refresh();
Notification::make()->title('Rubrica aggiornata dal fornitore')->success()->send();
}
private function loadTagSuggestions(): void
{
if (! Schema::hasColumn('fornitori', 'tags')) {
$this->tagSuggestions = [];
return;
}
$all = Fornitore::query()
->whereNotNull('tags')
->pluck('tags')
->all();
$pool = [];
foreach ($all as $row) {
foreach ($this->splitTags((string) $row) as $tag) {
$pool[] = $tag;
}
}
$pool = array_values(array_unique($pool));
sort($pool, SORT_NATURAL | SORT_FLAG_CASE);
$this->tagSuggestions = array_slice($pool, 0, 300);
}
private function refreshDipendentiRows(): void
{
$this->dipendentiRows = FornitoreDipendente::query()
->with(['user:id,name,email', 'rubrica:id,nome,cognome,ragione_sociale'])
->where('fornitore_id', (int) $this->fornitore->id)
->orderByDesc('attivo')
->orderBy('nome')
->orderBy('cognome')
->get()
->map(function (FornitoreDipendente $d): array {
$userId = (int) ($d->user_id ?? 0);
return [
'id' => (int) $d->id,
'nome' => (string) ($d->nome_completo ?: 'Dipendente #' . $d->id),
'email' => (string) ($d->email ?? ''),
'telefono' => (string) ($d->telefono ?? ''),
'attivo' => (bool) $d->attivo,
'rubrica_id' => (int) ($d->rubrica_id ?? 0),
'rubrica' => $d->rubrica ? $this->formatRubricaLookupLabel($d->rubrica) : '',
'user_id' => $userId,
'user_label' => $userId > 0 ? ((string) ($d->user?->name ?: ('Utente #' . $userId))): '-',
];
})
->all();
}
private function searchDipendenteRubricaMatches(): void
{
$this->dipendenteRubricaMatches = $this->searchRubricaLookupMatches(
$this->dipendenteRubricaSearch,
$this->dipendenteRubricaId,
null,
12,
(int) ($this->fornitore->amministratore_id ?? 0)
);
}
private function refreshCatalogRows(): void
{
$this->catalogoProdottiRows = [];
$this->tecnorepairRows = [];
$this->catalogoProdottiRows = app(ProductHubViewService::class)
->buildRows($this->fornitore, null, 20);
if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) {
$this->tecnorepairRows = $this->fornitore->tecnorepairSchede()
->withCount('allegati')
->orderByRaw("CASE status_bucket WHEN 'open' THEN 0 WHEN 'waiting' THEN 1 WHEN 'closed' THEN 2 ELSE 3 END")
->orderByDesc('date_received')
->limit(12)
->get(['id', 'legacy_numero_scheda', 'product_model', 'product_code', 'serial_number', 'status_bucket', 'status_label'])
->map(fn($scheda) => [
'id' => (int) $scheda->id,
'legacy_numero' => (string) ($scheda->legacy_numero_scheda ?? ''),
'product_model' => (string) ($scheda->product_model ?? ''),
'product_code' => (string) ($scheda->product_code ?? ''),
'serial_number' => (string) ($scheda->serial_number ?? ''),
'status_bucket' => (string) ($scheda->status_bucket ?? ''),
'status_label' => (string) ($scheda->status_label ?? ''),
'allegati_count' => (int) ($scheda->allegati_count ?? 0),
])
->all();
}
}
private function resolveFornitoreAdminCode(): ?string
{
$admin = $this->fornitore->amministratore;
if (! $admin) {
return null;
}
$code = trim((string) ($admin->codice_amministratore ?? ''));
return $code !== '' ? $code : (string) $admin->id;
}
private function suggestWholesaleCsvPath(): string
{
$basePath = base_path('Miki-Bug-workspace/Fornitori/Listini da importare');
if (! is_dir($basePath)) {
return '';
}
$normalizedName = Str::of((string) ($this->fornitore->ragione_sociale ?? ''))
->ascii()
->lower()
->replaceMatches('/[^a-z0-9]+/', ' ')
->trim()
->value();
$candidates = glob($basePath . '/*/wholesale.csv') ?: [];
foreach ($candidates as $candidate) {
$segment = Str::of((string) basename(dirname($candidate)))
->ascii()
->lower()
->replaceMatches('/[^a-z0-9]+/', ' ')
->trim()
->value();
if ($segment !== '' && $normalizedName !== '' && Str::contains($normalizedName, $segment)) {
return $candidate;
}
}
return (string) ($candidates[0] ?? '');
}
private function suggestWholesaleXlsxPath(): string
{
$basePath = base_path('Miki-Bug-workspace/Fornitori/Listini da importare');
if (! is_dir($basePath)) {
return '';
}
$normalizedName = Str::of((string) ($this->fornitore->ragione_sociale ?? ''))
->ascii()
->lower()
->replaceMatches('/[^a-z0-9]+/', ' ')
->trim()
->value();
$candidates = glob($basePath . '/*/*.xlsx') ?: [];
foreach ($candidates as $candidate) {
$segment = Str::of((string) basename(dirname($candidate)))
->ascii()
->lower()
->replaceMatches('/[^a-z0-9]+/', ' ')
->trim()
->value();
if ($segment !== '' && $normalizedName !== '' && (Str::contains($normalizedName, $segment) || Str::contains($segment, $normalizedName))) {
return $candidate;
}
}
return (string) ($candidates[0] ?? '');
}
private function refreshOperationalBoxes(): void
{
$user = Auth::user();
if ($user instanceof User) {
$this->hydrateBoxData($user);
}
$this->fornitore->refresh();
$this->refreshCatalogRows();
}
/**
* @return array<int, string>
*/
private function normalizeTags(string $input): array
{
$tags = [];
foreach ($this->splitTags($input) as $tag) {
$normalized = $this->canonicalizeTag($tag);
if ($normalized !== null) {
$tags[] = $normalized;
}
}
$tags = array_values(array_unique($tags));
sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
return $tags;
}
/**
* @return array<int, string>
*/
private function splitTags(string $value): array
{
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
return array_values(array_filter(array_map(function (string $part): string {
$clean = trim($part);
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
return $clean;
}, $parts), fn(string $p): bool => $p !== ''));
}
private function canonicalizeTag(string $raw): ?string
{
$clean = trim(mb_strtolower($raw));
if ($clean === 'pc') {
return 'pc';
}
if ($clean === '' || mb_strlen($clean) < 3) {
return null;
}
$map = [
'informat' => 'informatica',
'assist' => 'assistenza',
'computer' => 'pc',
'apple' => 'apple',
'idr' => 'idraulico',
'idraul' => 'idraulico',
'elett' => 'elettricista',
'elettric' => 'elettricista',
'ascens' => 'ascensorista',
'puliz' => 'pulizie',
'giardin' => 'giardiniere',
'assicur' => 'assicurazione',
'manut' => 'manutenzione',
];
foreach ($map as $prefix => $canonical) {
if (str_starts_with($clean, $prefix)) {
return $canonical;
}
}
return $clean;
}
private function hydrateBoxData(User $user): void
{
$this->fattureAssociate = [];
$this->raVersateRows = [];
$identityCandidates = $this->getSupplierIdentityCandidates();
$this->box = [
'stabile_id' => null,
'fornitore_features' => [],
'fornitore_defaults' => [
'voce_spesa_default_id' => null,
'conto_costo_default_id' => null,
],
'catalogo_modulo' => [
'scope_code' => null,
'fe_products' => 0,
'csv_products' => 0,
'manual_products' => 0,
'internal_only' => 0,
'private_links' => 0,
'offers' => 0,
'amazon_links' => 0,
],
'prodotti' => [
'count' => 0,
'serializzati' => 0,
'identifiers' => 0,
'with_media' => 0,
],
'tecnorepair' => [
'schede' => 0,
'aperte' => 0,
'chiuse' => 0,
'seriali' => 0,
],
'ade' => ['count' => 0, 'lordo' => 0.0],
'fe' => ['count' => 0, 'totale' => 0.0, 'contabilizzate' => 0],
'contabilita' => [
'aperte_count' => 0,
'aperte_netto' => 0.0,
'pagate_count' => 0,
'pagate_netto' => 0.0,
],
'da_pagare' => ['source' => null, 'importo' => 0.0],
'ra' => [
'da_versare' => 0.0,
'versata' => 0.0,
'compensata' => 0.0,
],
];
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (! $activeStabileId) {
return;
}
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($activeStabileId);
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
if ($amministratoreId <= 0) {
return;
}
$this->box['stabile_id'] = $activeStabileId;
if (Schema::hasTable('products')) {
$productsBase = Product::query()->where('default_fornitore_id', (int) $this->fornitore->id);
$catalogProducts = (clone $productsBase)->get(['id', 'meta']);
$this->box['prodotti']['count'] = (int) (clone $productsBase)->count();
$this->box['prodotti']['serializzati'] = (int) (clone $productsBase)->where('track_serials', true)->count();
$this->box['prodotti']['with_media'] = (int) (clone $productsBase)->whereHas('media')->count();
$this->box['prodotti']['identifiers'] = (int) (clone $productsBase)->withCount('identifiers')->get()->sum('identifiers_count');
$scopeCode = trim((string) ($this->fornitore->codice_univoco ?? ''));
$this->box['catalogo_modulo']['scope_code'] = $scopeCode !== '' ? $scopeCode : ('FORN-' . str_pad((string) $this->fornitore->id, 6, '0', STR_PAD_LEFT));
foreach ($catalogProducts as $product) {
$meta = is_array($product->meta ?? null) ? $product->meta : [];
$source = (string) ($meta['source'] ?? 'manual');
$catalog = is_array($meta['catalog'] ?? null) ? $meta['catalog'] : [];
$privateLinks = is_array($catalog['private_source_links'] ?? null) ? $catalog['private_source_links'] : [];
if ($source === 'fattura_elettronica') {
$this->box['catalogo_modulo']['fe_products']++;
} elseif ($source === 'ncom_wholesale_csv') {
$this->box['catalogo_modulo']['csv_products']++;
} else {
$this->box['catalogo_modulo']['manual_products']++;
}
if ((string) ($catalog['publication_mode'] ?? '') === 'internal_only') {
$this->box['catalogo_modulo']['internal_only']++;
}
$this->box['catalogo_modulo']['private_links'] += count(array_filter($privateLinks, fn($value) => filled($value)));
}
if (Schema::hasTable('product_offers')) {
$offersBase = $this->fornitore->productOffers()->where('is_active', true);
$this->box['catalogo_modulo']['offers'] = (int) (clone $offersBase)->count();
$this->box['catalogo_modulo']['amazon_links'] = (int) (clone $offersBase)->whereIn('source_type', ['amazon_referral', 'amazon_creators_api'])->count();
}
}
if (Schema::hasTable('assistenza_tecnorepair_schede_legacy')) {
$stats = $this->fornitore->tecnorepair_stats;
$this->box['tecnorepair']['schede'] = (int) ($stats['total'] ?? 0);
$this->box['tecnorepair']['aperte'] = (int) ($stats['open'] ?? 0);
$this->box['tecnorepair']['chiuse'] = (int) ($stats['closed'] ?? 0);
$this->box['tecnorepair']['seriali'] = (int) ($stats['serials'] ?? 0);
}
// Feature flags & defaults (per stabile) + fallback (per fornitore)
$settings = null;
if (Schema::hasTable('fornitore_stabile_impostazioni')) {
$settings = FornitoreStabileImpostazione::query()
->where('stabile_id', (int) $activeStabileId)
->where('fornitore_id', (int) $this->fornitore->id)
->first();
if ($settings) {
$this->box['fornitore_defaults']['voce_spesa_default_id'] = $settings->voce_spesa_default_id ? (int) $settings->voce_spesa_default_id : null;
$this->box['fornitore_defaults']['conto_costo_default_id'] = $settings->conto_costo_default_id ? (int) $settings->conto_costo_default_id : null;
}
}
$features = [];
if (Schema::hasColumn('fornitori', 'fe_features')) {
$features = is_array($this->fornitore->fe_features ?? null) ? $this->fornitore->fe_features : [];
} elseif ($settings && is_array($settings->meta ?? null)) {
$features = is_array($settings->meta['fe_features'] ?? null) ? $settings->meta['fe_features'] : [];
}
$this->box['fornitore_features'] = $features;
$normalize = static function (?string $value): string {
$value = strtoupper(trim((string) $value));
$value = str_replace(' ', '', $value);
return $value;
};
$ids = [];
$piva = $normalize($this->fornitore->partita_iva);
$cf = $normalize($this->fornitore->codice_fiscale);
if ($piva !== '') {
$ids[] = $piva;
if (str_starts_with($piva, 'IT') && strlen($piva) > 2) {
$ids[] = substr($piva, 2);
}
}
if ($cf !== '') {
$ids[] = $cf;
}
$ids = array_values(array_unique(array_filter($ids, fn($v) => $v !== '')));
// AdE (staging)
if (! empty($ids) && Schema::hasTable('stg_fatture_ade')) {
$adeBase = StgFatturaAde::query()
->where('amministratore_id', $amministratoreId)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($activeStabileId): void {
$q->where('stabile_id', $activeStabileId)->orWhereNull('stabile_id');
})
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
foreach ($ids as $id) {
$q->orWhereRaw("REPLACE(UPPER(identificativo_fornitore), ' ', '') = ?", [$id]);
}
});
$this->box['ade']['count'] = (int) (clone $adeBase)->count();
$lordo = (float) (clone $adeBase)->selectRaw('COALESCE(SUM(imponibile + imposta), 0) as lordo')->value('lordo');
$this->box['ade']['lordo'] = $lordo;
$this->anteprimaAde = (clone $adeBase)
->orderByDesc('data_emissione')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_emissione', 'numero_documento', 'imponibile', 'imposta', 'sdi_file'])
->map(fn(StgFatturaAde $r) => [
'id' => $r->id,
'data' => $r->data_emissione?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'lordo' => (float) ($r->imponibile ?? 0) + (float) ($r->imposta ?? 0),
'sdi_file' => (string) ($r->sdi_file ?? ''),
])
->all();
}
// FE importate
if (Schema::hasTable('fatture_elettroniche')) {
$feBase = FatturaElettronica::query()
->where('stabile_id', $activeStabileId)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($identityCandidates): void {
$this->applySupplierIdentityFilterToFeQuery($q, $identityCandidates);
});
$this->box['fe']['count'] = (int) (clone $feBase)->count();
$this->box['fe']['totale'] = (float) (clone $feBase)->selectRaw('COALESCE(SUM(totale), 0) as totale')->value('totale');
$this->box['fe']['contabilizzate'] = (int) (clone $feBase)
->where(function (\Illuminate\Database\Eloquent\Builder $q): void {
$q->whereNotNull('registrazione_contabile_id')->orWhere('stato', 'contabilizzata');
})
->count();
$this->anteprimaFe = (clone $feBase)
->orderByDesc('data_fattura')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_fattura', 'numero_fattura', 'totale', 'sdi_file', 'stato'])
->map(fn(FatturaElettronica $r) => [
'id' => $r->id,
'data' => $r->data_fattura?->format('d/m/Y'),
'numero' => (string) ($r->numero_fattura ?? ''),
'totale' => (float) ($r->totale ?? 0),
'sdi_file' => (string) ($r->sdi_file ?? ''),
'stato' => (string) ($r->stato ?? ''),
])
->all();
}
// Contabilità (fatture fornitori)
if (Schema::hasTable('contabilita_fatture_fornitori')) {
$contabBase = ContabilitaFatturaFornitore::query()
->where('stabile_id', $activeStabileId)
->where('fornitore_id', (int) $this->fornitore->id)
->when($identityCandidates !== [], function ($query) use ($identityCandidates): void {
$query->where(function (\Illuminate\Database\Eloquent\Builder $inner) use ($identityCandidates): void {
$inner->whereNull('fattura_elettronica_id')
->orWhereDoesntHave('fatturaElettronica')
->orWhereHas('fatturaElettronica', function (\Illuminate\Database\Eloquent\Builder $feQuery) use ($identityCandidates): void {
$this->applySupplierIdentityFilterToFeQuery($feQuery, $identityCandidates);
});
});
});
// Coerente con la logica della scheda contabile: aperto = totale - pagato.
$totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
$this->box['contabilita']['pagate_count'] = (int) (clone $contabBase)->where('stato', 'pagato')->count();
$this->box['contabilita']['pagate_netto'] = (float) (clone $contabBase)->where('stato', 'pagato')->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
$this->box['contabilita']['aperte_netto'] = round($totNetto - (float) $this->box['contabilita']['pagate_netto'], 2);
$this->box['contabilita']['aperte_count'] = (int) (clone $contabBase)->where('stato', '!=', 'pagato')->count();
$this->anteprimaContabilita = (clone $contabBase)
->orderByDesc('data_documento')
->orderByDesc('id')
->limit(8)
->get(['id', 'data_documento', 'numero_documento', 'netto_da_pagare', 'stato'])
->map(fn(ContabilitaFatturaFornitore $r) => [
'id' => $r->id,
'data' => $r->data_documento?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'netto' => (float) ($r->netto_da_pagare ?? 0),
'stato' => (string) ($r->stato ?? ''),
])
->all();
$this->fattureAssociate = (clone $contabBase)
->orderByDesc('data_documento')
->orderByDesc('id')
->limit(30)
->get(['id', 'data_documento', 'numero_documento', 'totale', 'netto_da_pagare', 'stato', 'fattura_elettronica_id'])
->map(fn(ContabilitaFatturaFornitore $r) => [
'id' => (int) $r->id,
'data' => $r->data_documento?->format('d/m/Y'),
'numero' => (string) ($r->numero_documento ?? ''),
'totale' => (float) ($r->totale ?? 0),
'netto' => (float) ($r->netto_da_pagare ?? 0),
'stato' => (string) ($r->stato ?? ''),
'source' => ((int) ($r->fattura_elettronica_id ?? 0) > 0) ? 'FE' : 'manuale',
])
->all();
}
// Registro RA
if (Schema::hasTable('registro_ritenute_acconto')) {
$raBase = RegistroRitenuteAcconto::query()
->where('fornitore_id', (int) $this->fornitore->id)
->whereHas('gestione', fn(\Illuminate\Database\Eloquent\Builder $q) => $q->where('stabile_id', (int) $activeStabileId));
foreach (['da_versare', 'versata', 'compensata'] as $stato) {
$this->box['ra'][$stato] = (float) (clone $raBase)
->where('stato_versamento', $stato)
->selectRaw('COALESCE(SUM(importo_ritenuta), 0) as ra')
->value('ra');
}
$this->raVersateRows = (clone $raBase)
->whereIn('stato_versamento', ['versata', 'compensata'])
->orderByDesc('data_versamento')
->orderByDesc('id')
->limit(30)
->get(['id', 'data_versamento', 'importo_ritenuta', 'stato_versamento', 'codice_tributo', 'f24_riferimento'])
->map(fn(RegistroRitenuteAcconto $r) => [
'id' => (int) $r->id,
'data_versamento' => $r->data_versamento?->format('d/m/Y'),
'importo' => (float) ($r->importo_ritenuta ?? 0),
'stato' => (string) ($r->stato_versamento ?? ''),
'tributo' => (string) ($r->codice_tributo ?? ''),
'f24' => (string) ($r->f24_riferimento ?? ''),
])
->all();
}
// Totale da pagare (priorità: contabilità → FE → AdE)
if ((float) ($this->box['contabilita']['aperte_netto'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'contabilita', 'importo' => (float) $this->box['contabilita']['aperte_netto']];
} elseif ((int) ($this->box['fe']['count'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'fe', 'importo' => (float) ($this->box['fe']['totale'] ?? 0)];
} elseif ((int) ($this->box['ade']['count'] ?? 0) > 0) {
$this->box['da_pagare'] = ['source' => 'ade', 'importo' => (float) ($this->box['ade']['lordo'] ?? 0)];
}
}
/**
* @return array<int, string>
*/
private function getSupplierIdentityCandidates(): array
{
$values = [];
foreach ([(string) ($this->fornitore->partita_iva ?? ''), (string) ($this->fornitore->codice_fiscale ?? '')] as $raw) {
$normalized = strtoupper(str_replace(' ', '', trim($raw)));
if ($normalized === '' || $normalized === 'ND') {
continue;
}
$values[] = $normalized;
if (str_starts_with($normalized, 'IT') && strlen($normalized) > 2) {
$values[] = substr($normalized, 2);
}
}
$values = array_values(array_unique(array_filter($values, fn(string $value): bool => $value !== '')));
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return $values;
}
/**
* @param array<int, string> $identityCandidates
*/
private function applySupplierIdentityFilterToFeQuery(\Illuminate\Database\Eloquent\Builder $query, array $identityCandidates): void
{
$fornitoreId = (int) $this->fornitore->id;
if ($identityCandidates === []) {
$query->where('fornitore_id', $fornitoreId);
return;
}
$query->where(function (\Illuminate\Database\Eloquent\Builder $outer) use ($identityCandidates, $fornitoreId): void {
$outer->where(function (\Illuminate\Database\Eloquent\Builder $identityQuery) use ($identityCandidates): void {
foreach ($identityCandidates as $identity) {
$identityQuery->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$identity])
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$identity]);
}
})->orWhere(function (\Illuminate\Database\Eloquent\Builder $fallbackQuery) use ($fornitoreId): void {
$fallbackQuery->where('fornitore_id', $fornitoreId)
->where(function (\Illuminate\Database\Eloquent\Builder $missingPiva): void {
$missingPiva->whereNull('fornitore_piva')
->orWhere('fornitore_piva', '')
->orWhere('fornitore_piva', 'ND');
})
->where(function (\Illuminate\Database\Eloquent\Builder $missingCf): void {
$missingCf->whereNull('fornitore_cf')
->orWhere('fornitore_cf', '')
->orWhere('fornitore_cf', 'ND');
});
});
});
}
protected function getHeaderActions(): array
{
return [];
}
}