Separate supplier archive roots and normalize Drive templates

This commit is contained in:
michele 2026-04-14 18:37:14 +00:00
parent 24c2318436
commit c114090882
9 changed files with 239 additions and 82 deletions

View File

@ -335,6 +335,31 @@ private function ensureDriveTemplateStructure(string $token, string $rootFolderI
} }
$this->ensureDriveFolder($token, $name, $templateRoot); $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 private function ensureDriveFolder(string $token, string $name, string $parentId): ?string

View File

@ -1259,11 +1259,23 @@ public function getDocumentazioneStatsProperty(): array
*/ */
public function getDriveTemplateFoldersProperty(): 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) $folders = is_array($items)
? array_values(array_filter(array_map('strval', $folders), fn(string $row) : bool => trim($row) !== '')) ? 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));
} }
/** /**

View File

@ -7,6 +7,7 @@
use App\Models\FornitoreStabileImpostazione; use App\Models\FornitoreStabileImpostazione;
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\User; use App\Models\User;
use App\Services\TenantArchivePathService;
use App\Support\ArchivioPaths; use App\Support\ArchivioPaths;
use App\Support\GoogleAccountStore; use App\Support\GoogleAccountStore;
use BackedEnum; use BackedEnum;
@ -353,23 +354,12 @@ private function refreshArchiveSummary(): void
{ {
$relativeBase = $this->getArchiveRelativeBase(); $relativeBase = $this->getArchiveRelativeBase();
$absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null; $absoluteBase = $relativeBase ? storage_path('app/' . $relativeBase) : null;
$legacyRelativeBase = $this->getLegacyArchiveRelativeBase();
$legacyAbsoluteBase = $legacyRelativeBase ? storage_path('app/' . $legacyRelativeBase) : null;
$this->operationalConfigAvailable = Schema::hasColumn('fornitori', 'operational_config'); $this->operationalConfigAvailable = Schema::hasColumn('fornitori', 'operational_config');
if ($relativeBase !== null) { if ($this->fornitore instanceof Fornitore) {
foreach ([ $this->fornitore->ensureArchiveStructure();
'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 ? [] : [ $this->archiveFolders = $relativeBase === null ? [] : [
@ -379,17 +369,19 @@ private function refreshArchiveSummary(): void
['label' => 'Prodotti e catalogo', 'path' => storage_path('app/' . $relativeBase . '/prodotti')], ['label' => 'Prodotti e catalogo', 'path' => storage_path('app/' . $relativeBase . '/prodotti')],
['label' => 'Comunicazioni / Post-it', 'path' => storage_path('app/' . $relativeBase . '/comunicazioni')], ['label' => 'Comunicazioni / Post-it', 'path' => storage_path('app/' . $relativeBase . '/comunicazioni')],
['label' => 'TecnoRepair import', 'path' => storage_path('app/' . $relativeBase . '/tecnorepair/imports')], ['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 === '') { if (is_string($legacyAbsoluteBase) && $legacyAbsoluteBase !== '' && $legacyAbsoluteBase !== $absoluteBase && is_dir($legacyAbsoluteBase)) {
$latest = collect(Storage::disk('local')->files($relativeBase . '/tecnorepair/imports')) $this->archiveFolders[] = ['label' => 'Base archivio legacy', 'path' => $legacyAbsoluteBase];
->filter(fn(string $path): bool => str_ends_with(strtolower($path), '.mdb')) $this->archiveFolders[] = ['label' => 'TecnoRepair import legacy', 'path' => storage_path('app/' . $legacyRelativeBase . '/tecnorepair/imports')];
->sortDesc() }
->first();
if (is_string($latest) && $latest !== '') { $currentMdbPath = $this->resolveTecnoRepairMdbPath($this->tecnorepairMdbPath);
$this->tecnorepairMdbPath = storage_path('app/' . $latest); if ($currentMdbPath === null) {
} $this->tecnorepairMdbPath = $this->discoverLatestTecnoRepairMdbPath() ?? '';
} else {
$this->tecnorepairMdbPath = $currentMdbPath;
} }
$stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0]; $stats = $this->fornitore?->tecnorepair_stats ?? ['total' => 0, 'open' => 0, 'closed' => 0, 'serials' => 0];
@ -397,7 +389,7 @@ private function refreshArchiveSummary(): void
$this->loadOperationalWorkflowSettings(); $this->loadOperationalWorkflowSettings();
$this->moduleRows = [ $this->moduleRows = [
['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'], ['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' => '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' => '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' => '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)) { if (Storage::disk('local')->exists($relative)) {
$candidatePaths[] = storage_path('app/' . $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) { foreach (array_values(array_unique($candidatePaths)) as $candidate) {
@ -619,12 +618,48 @@ private function resolveTecnoRepairMdbPath(?string $rawPath): ?string
return null; 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<int, string>
*/
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 private function getArchiveRelativeBase(): ?string
{ {
if (! $this->fornitore instanceof Fornitore) { if (! $this->fornitore instanceof Fornitore) {
return null; 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);
} }
} }

View File

@ -741,10 +741,23 @@ private function buildContactNameParts(string $nomeRaw, string $cognomeRaw, stri
private function loadDriveTemplateFolders(): void private function loadDriveTemplateFolders(): void
{ {
$items = config('netgescon.google.drive_template_folders', []); $items = config('netgescon.google.drive_template_folders', []);
$this->driveTemplateFolders = is_array($items) $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) !== '')) ? 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 private function googleErrorBody($response, string $fallback): string

View File

@ -117,31 +117,44 @@ protected static function booted(): void
static::created(function (Fornitore $fornitore) { static::created(function (Fornitore $fornitore) {
try { try {
$base = ArchivioPaths::fornitoreBase($fornitore); $fornitore->ensureArchiveStructure();
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);
}
} catch (\Throwable) { } catch (\Throwable) {
// ignore // 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 protected function shouldGenerateCodiceUnivocoLocally(): bool
{ {
return $this->getConnection()->getDriverName() !== 'mysql'; return $this->getConnection()->getDriverName() !== 'mysql';

View File

@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use App\Models\Amministratore; use App\Models\Amministratore;
use App\Models\Fornitore;
use App\Models\Stabile; use App\Models\Stabile;
use InvalidArgumentException; use InvalidArgumentException;
@ -73,6 +74,51 @@ public function stabileAbsolutePath(Stabile $stabile, ?string $suffix = null): s
return storage_path('app/' . $this->stabileRelativePath($stabile, $suffix)); 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 private function appendSuffix(string $basePath, ?string $suffix): string
{ {
$normalizedSuffix = trim((string) $suffix, '/'); $normalizedSuffix = trim((string) $suffix, '/');

View File

@ -75,6 +75,17 @@ public static function fornitoreBase(Fornitore $fornitore, ?Amministratore $admi
return null; 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; $admin = $admin ?: $fornitore->amministratore;
if (! $admin instanceof Amministratore) { if (! $admin instanceof Amministratore) {
return null; return null;

View File

@ -138,38 +138,40 @@
'google' => [ 'google' => [
'drive_template_folders' => [ 'drive_template_folders' => [
'Bilanci', 'bilanci',
'Riscaldamento', 'riscaldamento',
'Visure catastali', 'visure_catastali',
'Ricevute Incassi', 'ricevute_incassi',
'Passaggio consegne', 'passaggio_consegne',
'Locali Condominiali', 'locali_condominiali',
'Cassetto Fiscale', 'cassetto_fiscale',
'Certificazione Unica', 'certificazione_unica',
'Agenzia entrate', 'agenzia_entrate',
'Fatture XML', 'fatture_xml',
'Posta e comunicazioni', 'posta_e_comunicazioni',
'Fatture PDF', 'fatture_pdf',
'PEC', 'pec',
'Verbali Assemblea', 'verbali_assemblea',
'Regolamento Condominio', 'regolamento_condominio',
'Tabelle Millesimali', 'tabelle_millesimali',
'Fornitori', 'fornitori',
'Banca CC', 'banca_cc',
'Posta CC', 'posta_cc',
'Utenze', 'utenze',
'Assicurazione', 'assicurazione',
'Assemblee', 'assemblee',
'Codice Fiscale', 'codice_fiscale',
'Scambio', 'scambio',
'ZIP', 'zip',
'Anagrafiche', 'anagrafiche',
'Bonifici', 'bonifici',
'F 24', 'f_24',
'Condomini', 'condomini',
'Lavori Vari', 'lavori_vari',
'Informative', 'informative',
'Verifiche periodiche', 'verifiche_periodiche',
], ],
'drive_template_year_root' => 'archivio_per_anno',
'drive_template_year_model' => '_anno_modello',
], ],
]; ];

View File

@ -136,7 +136,7 @@
@if(($showTechnicalBoxes ?? true) === true) @if(($showTechnicalBoxes ?? true) === true)
<div class="rounded-lg border border-gray-200 p-3 lg:col-span-2"> <div class="rounded-lg border border-gray-200 p-3 lg:col-span-2">
<div class="mb-2 text-sm font-semibold text-gray-800">Template Cartelle Drive Condominio</div> <div class="mb-2 text-sm font-semibold text-gray-800">Template Cartelle Drive Condominio</div>
<div class="mb-2 text-xs text-gray-500">Struttura base replicabile per ogni stabile (allineata alla tua organizzazione in Drive).</div> <div class="mb-2 text-xs text-gray-500">Struttura base replicabile per ogni stabile, con naming Linux a underscore e modello annuale sotto archivio_per_anno.</div>
@if(count($driveTemplateFolders) === 0) @if(count($driveTemplateFolders) === 0)
<div class="text-sm text-gray-500">Template non configurato.</div> <div class="text-sm text-gray-500">Template non configurato.</div>
@else @else