> */ public array $archiveFolders = []; /** @var array> */ public array $moduleRows = []; /** @var array|null */ public ?array $googleWorkspaceStatus = null; public bool $operationalConfigAvailable = false; /** @var array */ public array $stabileOptions = []; /** @var array> */ public array $stabileAccessRows = []; public string $dispatchBoardNote = ''; /** @var array> */ public array $preferredMaterialRows = []; public static function canAccess(): bool { $user = auth()->user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']); } public function mount(): void { [$fornitore] = $this->resolveOperatoreContext(allowAdminWithoutSupplier: true); if (! $fornitore instanceof Fornitore) { $this->missingAdminContext = true; return; } $this->fornitore = $fornitore; $this->refreshArchiveSummary(); } public function saveUploadedTecnoRepairMdb(): void { if (! $this->fornitore instanceof Fornitore) { return; } $upload = $this->tecnorepairMdbUpload; if (! $upload instanceof TemporaryUploadedFile) { Notification::make()->title('Seleziona prima un file MDB')->warning()->send(); return; } $extension = strtolower((string) $upload->getClientOriginalExtension()); if ($extension !== 'mdb') { Notification::make()->title('Carica un file .mdb')->warning()->send(); return; } $relativeBase = $this->getArchiveRelativeBase(); if ($relativeBase === null) { Notification::make()->title('Archivio fornitore non disponibile')->danger()->send(); return; } $filename = 'TecnoRepairDB-' . now()->format('Ymd-His') . '.mdb'; $stored = $upload->storeAs($relativeBase . '/tecnorepair/imports', $filename, 'local'); $this->tecnorepairMdbPath = storage_path('app/' . $stored); $this->tecnorepairMdbUpload = null; $this->refreshArchiveSummary(); Notification::make()->title('Archivio TecnoRepair caricato')->body($filename)->success()->send(); } public function importTecnoRepairFromArchive(bool $dryRun = false): void { if (! $this->fornitore instanceof Fornitore) { return; } $mdbPath = $this->resolveTecnoRepairMdbPath($this->tecnorepairMdbPath); if ($mdbPath === null) { Notification::make() ->title('Percorso MDB non valido') ->body('Indica un file .mdb esistente sul server oppure salva prima l\'upload nell\'archivio fornitore.') ->warning() ->send(); return; } $this->tecnorepairMdbPath = $mdbPath; $adminId = (int) ($this->fornitore->amministratore_id ?? 0); if ($adminId <= 0) { Notification::make()->title('Amministratore del fornitore non trovato')->danger()->send(); return; } try { Artisan::call('tecnorepair:import-legacy', [ 'amministratore' => $adminId, '--mdb' => $mdbPath, '--fornitore-id' => (int) $this->fornitore->id, '--force-primary' => true, '--dry-run' => $dryRun, ]); } catch (\Throwable $e) { Notification::make()->title('Import TecnoRepair fallito')->body($e->getMessage())->danger()->send(); return; } $this->lastImportOutput = trim(Artisan::output()); $this->refreshArchiveSummary(); Notification::make() ->title($dryRun ? 'Simulazione TecnoRepair completata' : 'Import TecnoRepair completato') ->body($this->lastImportOutput !== '' ? $this->lastImportOutput : 'Operazione completata.') ->success() ->send(); } public function getSchedaFornitoreUrl(): string { return FornitoreScheda::getUrl(['record' => (int) ($this->fornitore?->id ?? 0)], panel: 'admin-filament'); } public function getTicketsUrl(): string { return TicketOperativi::getUrl(['fornitore' => (int) ($this->fornitore?->id ?? 0)], panel: 'admin-filament'); } public function getProdottiUrl(): string { return ProdottiCatalogo::getUrl(['fornitore' => (int) ($this->fornitore?->id ?? 0)], panel: 'admin-filament'); } public function getGoogleConnectUrl(): string { return route('oauth.google.redirect', [ 'account_key' => 'fornitore-' . (int) ($this->fornitore?->id ?? 0), 'label' => trim((string) ($this->fornitore?->ragione_sociale ?? '')) ?: ('Fornitore #' . (int) ($this->fornitore?->id ?? 0)), 'return_to' => request()->getRequestUri(), ]); } public function getGoogleDisconnectUrl(): string { return route('oauth.google.disconnect', [ 'account_key' => 'fornitore-' . (int) ($this->fornitore?->id ?? 0), 'return_to' => request()->getRequestUri(), ]); } public function addStabileAccessRow(): void { $unusedStableId = null; $usedIds = collect($this->stabileAccessRows) ->pluck('stabile_id') ->filter(fn($value): bool => (int) $value > 0) ->map(fn($value): int => (int) $value) ->all(); foreach (array_keys($this->stabileOptions) as $stabileId) { if (! in_array((int) $stabileId, $usedIds, true)) { $unusedStableId = (int) $stabileId; break; } } $this->stabileAccessRows[] = [ 'stabile_id' => $unusedStableId, 'chi_suonare' => '', 'contatto_nome' => '', 'contatto_telefono' => '', 'dove_sono_chiavi' => '', 'istruzioni_accesso' => '', 'fascia_reperibilita' => '', 'note_urgenza' => '', 'is_priority' => false, ]; } public function removeStabileAccessRow(int $index): void { unset($this->stabileAccessRows[$index]); $this->stabileAccessRows = array_values($this->stabileAccessRows); } public function addPreferredMaterialRow(): void { $this->preferredMaterialRows[] = [ 'label' => '', 'brand_model' => '', 'barcode' => '', 'internal_code' => '', 'preferred_source' => '', 'preferred_url' => '', 'default_qty' => 1, 'last_price' => null, 'currency' => 'EUR', 'note' => '', ]; } public function removePreferredMaterialRow(int $index): void { unset($this->preferredMaterialRows[$index]); $this->preferredMaterialRows = array_values($this->preferredMaterialRows); } public function saveOperationalWorkflow(): void { if (! $this->fornitore instanceof Fornitore) { return; } $normalizedAccessRows = collect($this->stabileAccessRows) ->map(fn(array $row): ?array=> $this->normalizeStabileAccessRow($row)) ->filter() ->values() ->all(); $selectedStableIds = collect($normalizedAccessRows) ->pluck('stabile_id') ->map(fn($value): int => (int) $value) ->all(); $existingSettings = FornitoreStabileImpostazione::query() ->where('fornitore_id', (int) $this->fornitore->id) ->get() ->keyBy(fn(FornitoreStabileImpostazione $setting): int => (int) $setting->stabile_id); foreach ($normalizedAccessRows as $row) { $setting = $existingSettings->get((int) $row['stabile_id']) ?? new FornitoreStabileImpostazione([ 'fornitore_id' => (int) $this->fornitore->id, 'stabile_id' => (int) $row['stabile_id'], ]); $meta = is_array($setting->meta ?? null) ? $setting->meta : []; $meta['access_operativo'] = $row; $setting->fornitore_id = (int) $this->fornitore->id; $setting->stabile_id = (int) $row['stabile_id']; $setting->meta = $meta; $setting->save(); } foreach ($existingSettings as $stabileId => $setting) { if (in_array((int) $stabileId, $selectedStableIds, true)) { continue; } $meta = is_array($setting->meta ?? null) ? $setting->meta : []; if (! array_key_exists('access_operativo', $meta)) { continue; } unset($meta['access_operativo']); if ($meta === [] && ! $setting->voce_spesa_default_id && ! $setting->conto_costo_default_id) { $setting->delete(); continue; } $setting->meta = $meta; $setting->save(); } if ($this->operationalConfigAvailable) { $config = $this->fornitore->operational_config_safe; $dispatchBoardNote = trim($this->dispatchBoardNote); if ($dispatchBoardNote === '') { unset($config['dispatch_board_note']); } else { $config['dispatch_board_note'] = $dispatchBoardNote; } $config['preferred_materials'] = collect($this->preferredMaterialRows) ->map(fn(array $row): ?array=> $this->normalizePreferredMaterialRow($row)) ->filter() ->values() ->all(); $this->fornitore->operational_config = $config; $this->fornitore->save(); } $this->refreshArchiveSummary(); Notification::make() ->title('Workflow operativo aggiornato') ->body($this->operationalConfigAvailable ? 'Chiavi, reperibilità e memoria materiali sono stati salvati.' : 'Chiavi e reperibilità per stabile salvate. Esegui la migrazione per attivare anche i preferiti materiali.') ->success() ->send(); } private function refreshArchiveSummary(): void { $relativeBase = $this->getArchiveRelativeBase(); $absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null; $this->operationalConfigAvailable = Schema::hasColumn('fornitori', 'operational_config'); if ($relativeBase !== null) { foreach ([ 'documenti', 'rubrica', 'fe', 'prodotti', 'comunicazioni', 'tecnorepair/imports', 'tecnorepair/allegati', 'temp/upload', 'temp/processing', 'logs', ] as $folder) { Storage::disk('local')->makeDirectory($relativeBase . '/' . $folder); } } $this->archiveFolders = $relativeBase === null ? [] : [ ['label' => 'Base archivio', 'path' => (string) $absoluteBase], ['label' => 'Rubrica fornitore', 'path' => storage_path('app/' . $relativeBase . '/rubrica')], ['label' => 'FE fornitore', 'path' => storage_path('app/' . $relativeBase . '/fe')], ['label' => 'Prodotti e catalogo', 'path' => storage_path('app/' . $relativeBase . '/prodotti')], ['label' => 'Comunicazioni / Post-it', 'path' => storage_path('app/' . $relativeBase . '/comunicazioni')], ['label' => 'TecnoRepair import', 'path' => storage_path('app/' . $relativeBase . '/tecnorepair/imports')], ]; if ($relativeBase !== null && $this->tecnorepairMdbPath === '') { $latest = collect(Storage::disk('local')->files($relativeBase . '/tecnorepair/imports')) ->filter(fn(string $path): bool => str_ends_with(strtolower($path), '.mdb')) ->sortDesc() ->first(); if (is_string($latest) && $latest !== '') { $this->tecnorepairMdbPath = storage_path('app/' . $latest); } } $stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0]; $this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus(); $this->loadOperationalWorkflowSettings(); $this->moduleRows = [ ['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'], ['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Account dedicato al fornitore, isolato con chiave tecnica separata pur restando governato dal backend amministratore.'], ['module' => 'TecnoRepair MDB', 'status' => ((int) ($stats['total'] ?? 0) > 0) ? 'importato' : 'da importare', 'note' => 'Schede: ' . (int) ($stats['total'] ?? 0) . ' · aperte: ' . (int) ($stats['open'] ?? 0) . ' · chiuse: ' . (int) ($stats['closed'] ?? 0)], ['module' => 'Catalogo fornitore', 'status' => 'attivo', 'note' => 'Il catalogo vero resta nella pagina Prodotti.'], ['module' => 'Chiavi e reperibilità', 'status' => count($this->stabileAccessRows) > 0 ? 'configurato' : 'da configurare', 'note' => 'Istruzioni per accesso, chi suonare e dove recuperare chiavi per ciascuno stabile.'], ['module' => 'Memoria materiali', 'status' => count($this->preferredMaterialRows) > 0 ? 'attiva' : ($this->operationalConfigAvailable ? 'da compilare' : 'migrazione richiesta'), 'note' => 'Preferiti ricorrenti per lampade, ricambi e prodotti ripetuti dal fornitore.'], ['module' => 'Comunicazioni / Post-it', 'status' => 'predisposto', 'note' => 'Cartelle e contesto archivio pronti per agganciare chiamate, post-it ed email.'], ['module' => 'FE e documenti', 'status' => 'predisposto', 'note' => 'Archivio separato per FE, documenti e flussi contabili del fornitore.'], ]; } private function loadOperationalWorkflowSettings(): void { if (! $this->fornitore instanceof Fornitore) { $this->stabileOptions = []; $this->stabileAccessRows = []; $this->dispatchBoardNote = ''; $this->preferredMaterialRows = []; return; } $adminId = (int) ($this->fornitore->amministratore_id ?? 0); $stabili = Stabile::query() ->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId)) ->when(Schema::hasColumn('stabili', 'attivo'), fn($query) => $query->where('attivo', true)) ->orderBy('codice_stabile') ->orderBy('denominazione') ->get(['id', 'codice_stabile', 'denominazione']); $this->stabileOptions = $stabili ->mapWithKeys(fn(Stabile $stabile): array=> [ (int) $stabile->id => trim((string) ($stabile->codice_stabile ?: ('#' . $stabile->id)) . ' - ' . (string) ($stabile->denominazione ?: 'Stabile')), ]) ->all(); $accessRows = FornitoreStabileImpostazione::query() ->with('stabile:id,codice_stabile,denominazione') ->where('fornitore_id', (int) $this->fornitore->id) ->when($stabili->isNotEmpty(), fn($query) => $query->whereIn('stabile_id', $stabili->pluck('id')->all())) ->get() ->map(function (FornitoreStabileImpostazione $setting): ?array { $meta = is_array($setting->meta ?? null) ? $setting->meta : []; $access = is_array($meta['access_operativo'] ?? null) ? $meta['access_operativo'] : null; if (! is_array($access)) { return null; } return [ 'stabile_id' => (int) ($setting->stabile_id ?? 0), 'chi_suonare' => trim((string) ($access['chi_suonare'] ?? '')), 'contatto_nome' => trim((string) ($access['contatto_nome'] ?? '')), 'contatto_telefono' => trim((string) ($access['contatto_telefono'] ?? '')), 'dove_sono_chiavi' => trim((string) ($access['dove_sono_chiavi'] ?? '')), 'istruzioni_accesso' => trim((string) ($access['istruzioni_accesso'] ?? '')), 'fascia_reperibilita' => trim((string) ($access['fascia_reperibilita'] ?? '')), 'note_urgenza' => trim((string) ($access['note_urgenza'] ?? '')), 'is_priority' => (bool) ($access['is_priority'] ?? false), ]; }) ->filter() ->sortByDesc(fn(array $row): int => $row['is_priority'] ? 1 : 0) ->values() ->all(); $this->stabileAccessRows = $accessRows; $config = $this->operationalConfigAvailable ? $this->fornitore->operational_config_safe : []; $this->dispatchBoardNote = trim((string) ($config['dispatch_board_note'] ?? '')); $this->preferredMaterialRows = collect((array) ($config['preferred_materials'] ?? [])) ->map(function ($row): ?array { if (! is_array($row)) { return null; } return [ 'label' => trim((string) ($row['label'] ?? '')), 'brand_model' => trim((string) ($row['brand_model'] ?? '')), 'barcode' => trim((string) ($row['barcode'] ?? '')), 'internal_code' => trim((string) ($row['internal_code'] ?? '')), 'preferred_source' => trim((string) ($row['preferred_source'] ?? '')), 'preferred_url' => trim((string) ($row['preferred_url'] ?? '')), 'default_qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? (float) $row['default_qty'] : 1, 'last_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? (float) $row['last_price'] : null, 'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR', 'note' => trim((string) ($row['note'] ?? '')), ]; }) ->filter() ->values() ->all(); } private function normalizeStabileAccessRow(array $row): ?array { $stabileId = (int) ($row['stabile_id'] ?? 0); if ($stabileId <= 0) { return null; } $normalized = [ 'stabile_id' => $stabileId, 'chi_suonare' => trim((string) ($row['chi_suonare'] ?? '')), 'contatto_nome' => trim((string) ($row['contatto_nome'] ?? '')), 'contatto_telefono' => trim((string) ($row['contatto_telefono'] ?? '')), 'dove_sono_chiavi' => trim((string) ($row['dove_sono_chiavi'] ?? '')), 'istruzioni_accesso' => trim((string) ($row['istruzioni_accesso'] ?? '')), 'fascia_reperibilita' => trim((string) ($row['fascia_reperibilita'] ?? '')), 'note_urgenza' => trim((string) ($row['note_urgenza'] ?? '')), 'is_priority' => (bool) ($row['is_priority'] ?? false), ]; $hasContent = collect($normalized) ->except(['stabile_id', 'is_priority']) ->contains(fn($value): bool => is_string($value) && $value !== ''); return $hasContent ? $normalized : null; } private function normalizePreferredMaterialRow(array $row): ?array { $label = trim((string) ($row['label'] ?? '')); $brandModel = trim((string) ($row['brand_model'] ?? '')); $barcode = trim((string) ($row['barcode'] ?? '')); $internalCode = trim((string) ($row['internal_code'] ?? '')); if ($label === '' && $brandModel === '' && $barcode === '' && $internalCode === '') { return null; } return [ 'label' => $label, 'brand_model' => $brandModel, 'barcode' => $barcode, 'internal_code' => $internalCode, 'preferred_source' => trim((string) ($row['preferred_source'] ?? '')), 'preferred_url' => trim((string) ($row['preferred_url'] ?? '')), 'default_qty' => isset($row['default_qty']) && is_numeric($row['default_qty']) ? max(0.01, round((float) $row['default_qty'], 2)) : 1.0, 'last_price' => isset($row['last_price']) && is_numeric($row['last_price']) ? round((float) $row['last_price'], 2) : null, 'currency' => trim((string) ($row['currency'] ?? 'EUR')) ?: 'EUR', 'note' => trim((string) ($row['note'] ?? '')), ]; } /** * @return array|null */ private function resolveGoogleWorkspaceStatus(): ?array { $amministratore = $this->fornitore?->amministratore; if (! $amministratore) { return null; } $account = app(GoogleAccountStore::class)->get($amministratore, $this->getSupplierGoogleAccountKey()); if (! is_array($account)) { return [ 'connected' => false, 'email' => '', 'name' => '', 'connected_at' => '', ]; } return [ 'connected' => (bool) ($account['connected'] ?? false), 'email' => (string) ($account['email'] ?? ''), 'name' => (string) ($account['name'] ?? ''), 'connected_at' => (string) ($account['connected_at'] ?? ''), ]; } private function getSupplierGoogleAccountKey(): string { return 'fornitore-' . (int) ($this->fornitore?->id ?? 0); } private function resolveTecnoRepairMdbPath(?string $rawPath): ?string { $path = trim((string) $rawPath); if ($path === '') { return null; } $storageBase = str_replace('\\', '/', storage_path('app')); $normalizedInput = str_replace('\\', '/', $path); $candidatePaths = []; if (is_file($path)) { $candidatePaths[] = $path; } if (str_starts_with($normalizedInput, $storageBase . '/')) { $relative = ltrim(substr($normalizedInput, strlen($storageBase)), '/'); if ($relative !== '' && Storage::disk('local')->exists($relative)) { $candidatePaths[] = storage_path('app/' . $relative); } } if (! str_starts_with($normalizedInput, '/')) { $relative = ltrim($normalizedInput, '/'); if (Storage::disk('local')->exists($relative)) { $candidatePaths[] = storage_path('app/' . $relative); } } foreach (array_values(array_unique($candidatePaths)) as $candidate) { $real = realpath($candidate); if (is_string($real) && $real !== '' && is_file($real)) { return $real; } if (is_file($candidate)) { return $candidate; } } return null; } private function getArchiveRelativeBase(): ?string { if (! $this->fornitore instanceof Fornitore) { return null; } return ArchivioPaths::fornitoreBase($this->fornitore, $this->fornitore->amministratore); } }