diff --git a/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php b/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php index 4a0b895..ec3ac52 100644 --- a/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php +++ b/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php @@ -8,6 +8,7 @@ use App\Models\Stabile; use App\Models\User; use App\Support\ArchivioPaths; +use App\Support\GoogleAccountStore; use BackedEnum; use Filament\Notifications\Notification; use Filament\Pages\Page; @@ -130,12 +131,18 @@ public function importTecnoRepairFromArchive(bool $dryRun = false): void return; } - $mdbPath = trim($this->tecnorepairMdbPath); - if ($mdbPath === '' || ! is_file($mdbPath)) { - Notification::make()->title('Percorso MDB non valido')->warning()->send(); + $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(); @@ -390,7 +397,7 @@ private function refreshArchiveSummary(): void $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' => 'Rubrica e sincronizzazioni smartphone riusano l\'account Google del contesto amministratore.'], + ['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.'], @@ -545,16 +552,73 @@ private function resolveGoogleWorkspaceStatus(): ?array return null; } - $oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []); + $account = app(GoogleAccountStore::class)->get($amministratore, $this->getSupplierGoogleAccountKey()); + + if (! is_array($account)) { + return [ + 'connected' => false, + 'email' => '', + 'name' => '', + 'connected_at' => '', + ]; + } return [ - 'connected' => (bool) ($oauth['connected'] ?? false), - 'email' => (string) ($oauth['email'] ?? ''), - 'name' => (string) ($oauth['name'] ?? ''), - 'connected_at' => (string) ($oauth['connected_at'] ?? ''), + '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) { diff --git a/app/Filament/Pages/Gescon/FornitoreScheda.php b/app/Filament/Pages/Gescon/FornitoreScheda.php index c6b5210..ea6cc0b 100644 --- a/app/Filament/Pages/Gescon/FornitoreScheda.php +++ b/app/Filament/Pages/Gescon/FornitoreScheda.php @@ -753,11 +753,19 @@ private function splitTags(string $value): array 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', @@ -782,6 +790,7 @@ private function hydrateBoxData(User $user): void { $this->fattureAssociate = []; $this->raVersateRows = []; + $identityCandidates = $this->getSupplierIdentityCandidates(); $this->box = [ 'stabile_id' => null, @@ -967,17 +976,8 @@ private function hydrateBoxData(User $user): void if (Schema::hasTable('fatture_elettroniche')) { $feBase = FatturaElettronica::query() ->where('stabile_id', $activeStabileId) - ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void { - $q->where('fornitore_id', $this->fornitore->id); - - if (! empty($ids)) { - $q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void { - foreach ($ids as $id) { - $qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id]) - ->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]); - } - }); - } + ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($identityCandidates): void { + $this->applySupplierIdentityFilterToFeQuery($q, $identityCandidates); }); $this->box['fe']['count'] = (int) (clone $feBase)->count(); @@ -1008,7 +1008,16 @@ private function hydrateBoxData(User $user): void if (Schema::hasTable('contabilita_fatture_fornitori')) { $contabBase = ContabilitaFatturaFornitore::query() ->where('stabile_id', $activeStabileId) - ->where('fornitore_id', (int) $this->fornitore->id); + ->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'); @@ -1088,6 +1097,65 @@ private function hydrateBoxData(User $user): void } } + /** + * @return array + */ + 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 $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 [ diff --git a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php index cc38c8a..7b93640 100644 --- a/app/Services/FattureElettroniche/FatturaElettronicaImporter.php +++ b/app/Services/FattureElettroniche/FatturaElettronicaImporter.php @@ -1453,7 +1453,9 @@ private function updateExisting(FatturaElettronica $existing, string $xml, ?stri $fornitorePiva = $existing->fornitore_piva ?: 'ND'; } - $fornitoreId = $this->matchFornitoreId($fornitorePiva, $fornitoreCf); + $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId); + $adminId = (int) ($stabile?->amministratore_id ?: 0); + $fornitoreId = $this->matchFornitoreIdForAmministratore($adminId, $fornitorePiva, $fornitoreCf); $importRighe = $this->shouldImportRighe($fornitoreId, $extra); return DB::transaction(function () use ($existing, $data, $xml, $originalFilename, $extra, $stabileId, $fornitoreId, $fornitorePiva, $importRighe) { diff --git a/resources/views/filament/pages/fornitore/impostazioni-archivio.blade.php b/resources/views/filament/pages/fornitore/impostazioni-archivio.blade.php index c128c65..6117eb2 100644 --- a/resources/views/filament/pages/fornitore/impostazioni-archivio.blade.php +++ b/resources/views/filament/pages/fornitore/impostazioni-archivio.blade.php @@ -43,7 +43,7 @@
TecnoRepair MDB
-
Puoi caricare un archivio MDB dalla pagina web oppure indicare un percorso già disponibile sul server, poi lanciare la sincronizzazione verso il fornitore.
+
Puoi caricare un archivio MDB dalla pagina web oppure indicare un percorso già disponibile sul server. Sono accettati sia path assoluti sia percorsi relativi dentro storage/app.
-
Questo collegamento prepara la sincronizzazione rubrica lato fornitore e l'uso dei dati anche sul cellulare tramite l'account Google già governato dal backend.
+
Questo collegamento usa una chiave account dedicata al fornitore, così rubrica e sincronizzazioni non si mischiano con il profilo amministratore anche se il backend resta sotto lo stesso tenant.
@else
Contesto amministratore Google non disponibile per questo fornitore.
@endif