diff --git a/app/Console/Commands/NetgesconPreupdateBackupCommand.php b/app/Console/Commands/NetgesconPreupdateBackupCommand.php index 437b451..f76b21b 100644 --- a/app/Console/Commands/NetgesconPreupdateBackupCommand.php +++ b/app/Console/Commands/NetgesconPreupdateBackupCommand.php @@ -335,6 +335,31 @@ private function ensureDriveTemplateStructure(string $token, string $rootFolderI } $this->ensureDriveFolder($token, $name, $templateRoot); } + + $yearRootName = trim((string) config('netgescon.google.drive_template_year_root', '')); + $yearModelName = trim((string) config('netgescon.google.drive_template_year_model', '')); + + if ($yearRootName === '' || $yearModelName === '') { + return; + } + + $yearRootId = $this->ensureDriveFolder($token, $yearRootName, $templateRoot); + if ($yearRootId === null) { + return; + } + + $yearModelId = $this->ensureDriveFolder($token, $yearModelName, $yearRootId); + if ($yearModelId === null) { + return; + } + + foreach ($folders as $folder) { + $name = trim((string) $folder); + if ($name === '') { + continue; + } + $this->ensureDriveFolder($token, $name, $yearModelId); + } } private function ensureDriveFolder(string $token, string $name, string $parentId): ?string diff --git a/app/Filament/Pages/Condomini/StabilePage.php b/app/Filament/Pages/Condomini/StabilePage.php index 5788a10..f7a8c53 100644 --- a/app/Filament/Pages/Condomini/StabilePage.php +++ b/app/Filament/Pages/Condomini/StabilePage.php @@ -1259,11 +1259,23 @@ public function getDocumentazioneStatsProperty(): array */ public function getDriveTemplateFoldersProperty(): array { - $folders = config('netgescon.google.drive_template_folders', []); + $items = config('netgescon.google.drive_template_folders', []); + $yearRoot = trim((string) config('netgescon.google.drive_template_year_root', '')); + $yearModel = trim((string) config('netgescon.google.drive_template_year_model', '')); - return is_array($folders) - ? array_values(array_filter(array_map('strval', $folders), fn(string $row) : bool => trim($row) !== '')) + $folders = is_array($items) + ? array_values(array_filter(array_map('strval', $items), fn(string $row) : bool => trim($row) !== '')) : []; + + if ($yearRoot !== '' && $yearModel !== '') { + $baseFolders = $folders; + + foreach ($baseFolders as $folder) { + $folders[] = $yearRoot . '/' . $yearModel . '/' . $folder; + } + } + + return array_values(array_unique($folders)); } /** diff --git a/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php b/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php index ec3ac52..2b274d1 100644 --- a/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php +++ b/app/Filament/Pages/Fornitore/ImpostazioniArchivio.php @@ -7,6 +7,7 @@ use App\Models\FornitoreStabileImpostazione; use App\Models\Stabile; use App\Models\User; +use App\Services\TenantArchivePathService; use App\Support\ArchivioPaths; use App\Support\GoogleAccountStore; use BackedEnum; @@ -353,23 +354,12 @@ private function refreshArchiveSummary(): void { $relativeBase = $this->getArchiveRelativeBase(); $absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null; + $legacyRelativeBase = $this->getLegacyArchiveRelativeBase(); + $legacyAbsoluteBase = $legacyRelativeBase ? storage_path('app/' . $legacyRelativeBase) : 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); - } + if ($this->fornitore instanceof Fornitore) { + $this->fornitore->ensureArchiveStructure(); } $this->archiveFolders = $relativeBase === null ? [] : [ @@ -379,17 +369,19 @@ private function refreshArchiveSummary(): void ['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')], + ['label' => 'Archivio per anno', 'path' => storage_path('app/' . $relativeBase . '/documenti/archivio_per_anno')], ]; - 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($legacyAbsoluteBase) && $legacyAbsoluteBase !== '' && $legacyAbsoluteBase !== $absoluteBase && is_dir($legacyAbsoluteBase)) { + $this->archiveFolders[] = ['label' => 'Base archivio legacy', 'path' => $legacyAbsoluteBase]; + $this->archiveFolders[] = ['label' => 'TecnoRepair import legacy', 'path' => storage_path('app/' . $legacyRelativeBase . '/tecnorepair/imports')]; + } - if (is_string($latest) && $latest !== '') { - $this->tecnorepairMdbPath = storage_path('app/' . $latest); - } + $currentMdbPath = $this->resolveTecnoRepairMdbPath($this->tecnorepairMdbPath); + if ($currentMdbPath === null) { + $this->tecnorepairMdbPath = $this->discoverLatestTecnoRepairMdbPath() ?? ''; + } else { + $this->tecnorepairMdbPath = $currentMdbPath; } $stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0]; @@ -397,7 +389,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' => 'Account dedicato al fornitore, isolato con chiave tecnica separata pur restando governato dal backend amministratore.'], + ['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Account dedicato al fornitore, con archivio root separato e template cartelle in naming Linux con underscore.'], ['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.'], @@ -603,6 +595,13 @@ private function resolveTecnoRepairMdbPath(?string $rawPath): ?string if (Storage::disk('local')->exists($relative)) { $candidatePaths[] = storage_path('app/' . $relative); } + + foreach ($this->getTecnoRepairImportBases() as $importBase) { + $joined = trim($importBase . '/' . $relative, '/'); + if (Storage::disk('local')->exists($joined)) { + $candidatePaths[] = storage_path('app/' . $joined); + } + } } foreach (array_values(array_unique($candidatePaths)) as $candidate) { @@ -619,12 +618,48 @@ private function resolveTecnoRepairMdbPath(?string $rawPath): ?string return null; } + private function discoverLatestTecnoRepairMdbPath(): ?string + { + foreach ($this->getTecnoRepairImportBases() as $importBase) { + $latest = collect(Storage::disk('local')->files($importBase)) + ->filter(fn(string $path): bool => str_ends_with(strtolower($path), '.mdb')) + ->sortDesc() + ->first(); + + if (is_string($latest) && $latest !== '') { + return storage_path('app/' . $latest); + } + } + + return null; + } + + /** + * @return array + */ + private function getTecnoRepairImportBases(): array + { + return array_values(array_filter([ + $this->getArchiveRelativeBase() ? $this->getArchiveRelativeBase() . '/tecnorepair/imports' : null, + $this->getLegacyArchiveRelativeBase() ? $this->getLegacyArchiveRelativeBase() . '/tecnorepair/imports' : null, + ])); + } + private function getArchiveRelativeBase(): ?string { if (! $this->fornitore instanceof Fornitore) { return null; } - return ArchivioPaths::fornitoreBase($this->fornitore, $this->fornitore->amministratore); + return app(TenantArchivePathService::class)->fornitoreRelativePath($this->fornitore); + } + + private function getLegacyArchiveRelativeBase(): ?string + { + if (! $this->fornitore instanceof Fornitore || ! $this->fornitore->amministratore) { + return null; + } + + return app(TenantArchivePathService::class)->fornitoreLegacyRelativePath($this->fornitore); } } diff --git a/app/Filament/Widgets/GoogleWorkspaceOverview.php b/app/Filament/Widgets/GoogleWorkspaceOverview.php index b5691e6..ae18d68 100644 --- a/app/Filament/Widgets/GoogleWorkspaceOverview.php +++ b/app/Filament/Widgets/GoogleWorkspaceOverview.php @@ -741,10 +741,23 @@ private function buildContactNameParts(string $nomeRaw, string $cognomeRaw, stri private function loadDriveTemplateFolders(): void { - $items = config('netgescon.google.drive_template_folders', []); - $this->driveTemplateFolders = is_array($items) + $items = config('netgescon.google.drive_template_folders', []); + $yearRoot = trim((string) config('netgescon.google.drive_template_year_root', '')); + $yearModel = trim((string) config('netgescon.google.drive_template_year_model', '')); + + $folders = is_array($items) ? array_values(array_filter(array_map('strval', $items), fn(string $v) : bool => trim($v) !== '')) : []; + + if ($yearRoot !== '' && $yearModel !== '') { + $baseFolders = $folders; + + foreach ($baseFolders as $folder) { + $folders[] = $yearRoot . '/' . $yearModel . '/' . $folder; + } + } + + $this->driveTemplateFolders = array_values(array_unique($folders)); } private function googleErrorBody($response, string $fallback): string diff --git a/app/Models/Fornitore.php b/app/Models/Fornitore.php index f2161ec..e22d421 100755 --- a/app/Models/Fornitore.php +++ b/app/Models/Fornitore.php @@ -117,31 +117,44 @@ protected static function booted(): void static::created(function (Fornitore $fornitore) { try { - $base = ArchivioPaths::fornitoreBase($fornitore); - if (! $base) { - return; - } - - foreach ([ - 'documenti', - 'rubrica', - 'fe', - 'prodotti', - 'comunicazioni', - 'tecnorepair/imports', - 'tecnorepair/allegati', - 'temp/upload', - 'temp/processing', - 'logs', - ] as $folder) { - Storage::disk('local')->makeDirectory($base . '/' . $folder); - } + $fornitore->ensureArchiveStructure(); } catch (\Throwable) { // ignore } }); } + public function ensureArchiveStructure(): void + { + $bases = array_values(array_filter([ + ArchivioPaths::fornitoreBase($this), + ArchivioPaths::fornitoreLegacyBase($this), + ])); + + if ($bases === []) { + return; + } + + foreach ($bases as $base) { + foreach ([ + 'documenti', + 'documenti/archivio_per_anno', + 'rubrica', + 'fe', + 'prodotti', + 'comunicazioni', + 'tecnorepair/imports', + 'tecnorepair/allegati', + 'temp/upload', + 'temp/processing', + 'logs', + 'google_drive', + ] as $folder) { + Storage::disk('local')->makeDirectory($base . '/' . $folder); + } + } + } + protected function shouldGenerateCodiceUnivocoLocally(): bool { return $this->getConnection()->getDriverName() !== 'mysql'; diff --git a/app/Services/TenantArchivePathService.php b/app/Services/TenantArchivePathService.php index df8e402..4cee8cc 100644 --- a/app/Services/TenantArchivePathService.php +++ b/app/Services/TenantArchivePathService.php @@ -2,6 +2,7 @@ namespace App\Services; use App\Models\Amministratore; +use App\Models\Fornitore; use App\Models\Stabile; use InvalidArgumentException; @@ -73,6 +74,51 @@ public function stabileAbsolutePath(Stabile $stabile, ?string $suffix = null): s return storage_path('app/' . $this->stabileRelativePath($stabile, $suffix)); } + public function fornitoreCode(Fornitore | string $fornitore): string + { + if (is_string($fornitore)) { + $code = trim($fornitore); + + if ($code === '') { + throw new InvalidArgumentException('Codice fornitore non valido.'); + } + + return $code; + } + + $code = trim((string) ($fornitore->codice_univoco ?: '')); + + if ($code === '') { + throw new InvalidArgumentException('Fornitore senza codice archivio canonico.'); + } + + return $code; + } + + public function fornitoreRelativePath(Fornitore | string $fornitore, ?string $suffix = null): string + { + $basePath = 'fornitori/' . $this->fornitoreCode($fornitore); + + return $this->appendSuffix($basePath, $suffix); + } + + public function fornitoreAbsolutePath(Fornitore | string $fornitore, ?string $suffix = null): string + { + return storage_path('app/' . $this->fornitoreRelativePath($fornitore, $suffix)); + } + + public function fornitoreLegacyRelativePath(Fornitore $fornitore, ?string $suffix = null): string + { + $basePath = $this->amministratoreRelativePath($fornitore->amministratore) . '/fornitori/' . $this->fornitoreCode($fornitore); + + return $this->appendSuffix($basePath, $suffix); + } + + public function fornitoreLegacyAbsolutePath(Fornitore $fornitore, ?string $suffix = null): string + { + return storage_path('app/' . $this->fornitoreLegacyRelativePath($fornitore, $suffix)); + } + private function appendSuffix(string $basePath, ?string $suffix): string { $normalizedSuffix = trim((string) $suffix, '/'); diff --git a/app/Support/ArchivioPaths.php b/app/Support/ArchivioPaths.php index 2bc4046..5c21a3d 100644 --- a/app/Support/ArchivioPaths.php +++ b/app/Support/ArchivioPaths.php @@ -75,6 +75,17 @@ public static function fornitoreBase(Fornitore $fornitore, ?Amministratore $admi return null; } + return 'fornitori/' . $code; + } + + public static function fornitoreLegacyBase(Fornitore $fornitore, ?Amministratore $admin = null): ?string + { + $code = (string) ($fornitore->codice_univoco ?: ''); + $code = self::sanitizeFolder($code); + if ($code === '') { + return null; + } + $admin = $admin ?: $fornitore->amministratore; if (! $admin instanceof Amministratore) { return null; diff --git a/config/netgescon.php b/config/netgescon.php index 3814568..f598372 100755 --- a/config/netgescon.php +++ b/config/netgescon.php @@ -138,38 +138,40 @@ 'google' => [ 'drive_template_folders' => [ - 'Bilanci', - 'Riscaldamento', - 'Visure catastali', - 'Ricevute Incassi', - 'Passaggio consegne', - 'Locali Condominiali', - 'Cassetto Fiscale', - 'Certificazione Unica', - 'Agenzia entrate', - 'Fatture XML', - 'Posta e comunicazioni', - 'Fatture PDF', - 'PEC', - 'Verbali Assemblea', - 'Regolamento Condominio', - 'Tabelle Millesimali', - 'Fornitori', - 'Banca CC', - 'Posta CC', - 'Utenze', - 'Assicurazione', - 'Assemblee', - 'Codice Fiscale', - 'Scambio', - 'ZIP', - 'Anagrafiche', - 'Bonifici', - 'F 24', - 'Condomini', - 'Lavori Vari', - 'Informative', - 'Verifiche periodiche', + 'bilanci', + 'riscaldamento', + 'visure_catastali', + 'ricevute_incassi', + 'passaggio_consegne', + 'locali_condominiali', + 'cassetto_fiscale', + 'certificazione_unica', + 'agenzia_entrate', + 'fatture_xml', + 'posta_e_comunicazioni', + 'fatture_pdf', + 'pec', + 'verbali_assemblea', + 'regolamento_condominio', + 'tabelle_millesimali', + 'fornitori', + 'banca_cc', + 'posta_cc', + 'utenze', + 'assicurazione', + 'assemblee', + 'codice_fiscale', + 'scambio', + 'zip', + 'anagrafiche', + 'bonifici', + 'f_24', + 'condomini', + 'lavori_vari', + 'informative', + 'verifiche_periodiche', ], + 'drive_template_year_root' => 'archivio_per_anno', + 'drive_template_year_model' => '_anno_modello', ], ]; diff --git a/resources/views/filament/widgets/google-workspace-overview.blade.php b/resources/views/filament/widgets/google-workspace-overview.blade.php index 1e86cc8..b0dbaa9 100644 --- a/resources/views/filament/widgets/google-workspace-overview.blade.php +++ b/resources/views/filament/widgets/google-workspace-overview.blade.php @@ -136,7 +136,7 @@ @if(($showTechnicalBoxes ?? true) === true)
Template Cartelle Drive Condominio
-
Struttura base replicabile per ogni stabile (allineata alla tua organizzazione in Drive).
+
Struttura base replicabile per ogni stabile, con naming Linux a underscore e modello annuale sotto archivio_per_anno.
@if(count($driveTemplateFolders) === 0)
Template non configurato.
@else