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);
}
$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

View File

@ -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));
}
/**

View File

@ -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($latest) && $latest !== '') {
$this->tecnorepairMdbPath = storage_path('app/' . $latest);
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')];
}
$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<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
{
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);
}
}

View File

@ -742,9 +742,22 @@ private function buildContactNameParts(string $nomeRaw, string $cognomeRaw, stri
private function loadDriveTemplateFolders(): void
{
$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) !== ''))
: [];
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

View File

@ -117,13 +117,28 @@ protected static function booted(): void
static::created(function (Fornitore $fornitore) {
try {
$base = ArchivioPaths::fornitoreBase($fornitore);
if (! $base) {
$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',
@ -133,13 +148,11 @@ protected static function booted(): void
'temp/upload',
'temp/processing',
'logs',
'google_drive',
] as $folder) {
Storage::disk('local')->makeDirectory($base . '/' . $folder);
}
} catch (\Throwable) {
// ignore
}
});
}
protected function shouldGenerateCodiceUnivocoLocally(): bool

View File

@ -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, '/');

View File

@ -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;

View File

@ -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',
],
];

View File

@ -136,7 +136,7 @@
@if(($showTechnicalBoxes ?? true) === true)
<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-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)
<div class="text-sm text-gray-500">Template non configurato.</div>
@else