diff --git a/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php b/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php index abb9ae4..c480b78 100644 --- a/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php +++ b/app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php @@ -108,6 +108,7 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ 'schede_lette' => count($schedeRows), 'schede_create' => 0, 'schede_aggiornate' => 0, + 'schede_consolidate' => 0, 'schede_saltate' => 0, 'allegati_importati' => 0, 'seriali_allineati' => 0, @@ -118,6 +119,13 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ $fornitore->forceFill(['is_tecnorepair_primary' => true])->save(); } + $this->consolidateLegacyDuplicates( + amministratoreId: (int) $admin->id, + fornitoreId: $fornitore?->id ? (int) $fornitore->id : null, + dryRun: $dryRun, + stats: $stats, + ); + foreach ($schedeRows as $row) { $legacyId = $this->toInt($row['ID'] ?? null); if ($legacyId === null) { @@ -168,10 +176,22 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ ], ]; - $existing = AssistenzaTecnorepairScheda::query() - ->where('imported_from_path', $mdbPath) - ->where('legacy_id', $legacyId) - ->first(); + $matchingSchede = $this->buildExistingSchedaQuery( + amministratoreId: (int) $admin->id, + legacyId: $legacyId, + fornitoreId: $fornitore?->id ? (int) $fornitore->id : null, + ) + ->orderByDesc('fornitore_id') + ->orderBy('id') + ->get(); + + $existing = $matchingSchede->first(); + $duplicateSchede = $matchingSchede->slice(1); + $duplicateSchedaIds = $duplicateSchede + ->pluck('id') + ->filter(fn($value): bool => is_numeric($value) && (int) $value > 0) + ->map(fn($value): int => (int) $value) + ->all(); if ($dryRun) { if ($existing) { @@ -179,16 +199,27 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ } else { $stats['schede_create']++; } + + if ($duplicateSchedaIds !== []) { + $stats['schede_consolidate'] += count($duplicateSchedaIds); + } + continue; } - $scheda = AssistenzaTecnorepairScheda::query()->updateOrCreate( - [ - 'imported_from_path' => $mdbPath, - 'legacy_id' => $legacyId, - ], - $payload, - ); + if ($duplicateSchedaIds !== []) { + ProductSerial::query()->whereIn('legacy_scheda_id', $duplicateSchedaIds)->delete(); + AssistenzaTecnorepairScheda::query()->whereKey($duplicateSchedaIds)->delete(); + $stats['schede_consolidate'] += count($duplicateSchedaIds); + } + + if ($existing instanceof AssistenzaTecnorepairScheda) { + $existing->fill($payload); + $existing->save(); + $scheda = $existing; + } else { + $scheda = AssistenzaTecnorepairScheda::query()->create($payload); + } if ($existing) { $stats['schede_aggiornate']++; @@ -251,6 +282,7 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ ['Schede lette', (string) $stats['schede_lette']], ['Schede create', (string) $stats['schede_create']], ['Schede aggiornate', (string) $stats['schede_aggiornate']], + ['Schede consolidate', (string) $stats['schede_consolidate']], ['Schede saltate', (string) $stats['schede_saltate']], ['Allegati importati', (string) $stats['allegati_importati']], ['Seriali allineati', (string) $stats['seriali_allineati']], @@ -302,6 +334,93 @@ private function resolveFornitore(Amministratore $admin, string $fornitoreIdOpti ->first(); } + private function buildExistingSchedaQuery(int $amministratoreId, int $legacyId, ?int $fornitoreId = null) + { + return AssistenzaTecnorepairScheda::query() + ->where('amministratore_id', $amministratoreId) + ->where('legacy_id', $legacyId) + ->when( + $fornitoreId !== null, + fn($query) => $query->where(function ($inner) use ($fornitoreId): void { + $inner->where('fornitore_id', $fornitoreId) + ->orWhereNull('fornitore_id'); + }) + ); + } + + private function consolidateLegacyDuplicates(int $amministratoreId, ?int $fornitoreId, bool $dryRun, array &$stats): void + { + $duplicateLegacyIds = AssistenzaTecnorepairScheda::query() + ->where('amministratore_id', $amministratoreId) + ->when( + $fornitoreId !== null, + fn($query) => $query->where(function ($inner) use ($fornitoreId): void { + $inner->where('fornitore_id', $fornitoreId) + ->orWhereNull('fornitore_id'); + }) + ) + ->whereNotNull('legacy_id') + ->select('legacy_id') + ->groupBy('legacy_id') + ->havingRaw('COUNT(*) > 1') + ->pluck('legacy_id') + ->filter(fn($value): bool => is_numeric($value) && (int) $value > 0) + ->map(fn($value): int => (int) $value) + ->all(); + + foreach ($duplicateLegacyIds as $legacyId) { + $matchingSchede = $this->buildExistingSchedaQuery( + amministratoreId: $amministratoreId, + legacyId: $legacyId, + fornitoreId: $fornitoreId, + ) + ->orderByDesc('fornitore_id') + ->orderBy('id') + ->get(); + + if ($matchingSchede->count() <= 1) { + continue; + } + + /** @var AssistenzaTecnorepairScheda $canonical */ + $canonical = $matchingSchede->first(); + $duplicateSchede = $matchingSchede->slice(1); + + foreach ($duplicateSchede as $duplicateScheda) { + $duplicateId = (int) $duplicateScheda->id; + if ($duplicateId <= 0) { + continue; + } + + if ($dryRun) { + $stats['schede_consolidate']++; + continue; + } + + AssistenzaTecnorepairAllegato::query() + ->where('scheda_id', $duplicateId) + ->update(['scheda_id' => (int) $canonical->id]); + + $serials = ProductSerial::query() + ->where('legacy_scheda_id', $duplicateId) + ->orderBy('id') + ->get(); + + foreach ($serials as $index => $serial) { + if ($index === 0 && ! ProductSerial::query()->where('legacy_scheda_id', (int) $canonical->id)->exists()) { + $serial->forceFill(['legacy_scheda_id' => (int) $canonical->id])->save(); + continue; + } + + $serial->delete(); + } + + $duplicateScheda->delete(); + $stats['schede_consolidate']++; + } + } + } + private function clean(mixed $value): ?string { if (! is_string($value) && ! is_numeric($value)) { diff --git a/app/Filament/Pages/Contabilita/SaldiContiArchivio.php b/app/Filament/Pages/Contabilita/SaldiContiArchivio.php index 1738b3a..ff6b630 100644 --- a/app/Filament/Pages/Contabilita/SaldiContiArchivio.php +++ b/app/Filament/Pages/Contabilita/SaldiContiArchivio.php @@ -1,5 +1,4 @@ contoId = is_numeric(request()->query('conto_id')) ? (int) request()->query('conto_id') : null; + $this->contoId = $this->contoId && $this->contoId > 0 ? $this->contoId : null; $this->mountInteractsWithTable(); } @@ -76,7 +82,8 @@ protected function getTableQuery(): Builder return SaldoConto::query() ->with('conto') - ->where('stabile_id', $stabileId); + ->where('stabile_id', $stabileId) + ->when($this->contoId !== null, fn(Builder $query): Builder => $query->where('conto_id', $this->contoId)); } public function table(Table $table): Table @@ -119,6 +126,13 @@ public function table(Table $table): Table ->alignEnd() ->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')), + TextColumn::make('is_partenza_contabile') + ->label('Partenza') + ->badge() + ->getStateUsing(fn(SaldoConto $record): string => (bool) ($record->is_partenza_contabile ?? false) ? 'Si' : '—') + ->color(fn(SaldoConto $record): string => (bool) ($record->is_partenza_contabile ?? false) ? 'info' : 'gray') + ->toggleable(), + TextColumn::make('tipo_estratto') ->label('Tipo') ->getStateUsing(fn(SaldoConto $record): string => $this->formatTipoEstrattoLabel($record->tipo_estratto)) @@ -156,7 +170,7 @@ public function table(Table $table): Table ->label('Conto') ->native(false) ->searchable() - ->options(fn(): array => $this->getContiOptions()) + ->options(fn(): array=> $this->getContiOptions()) ->required(), DatePicker::make('data_saldo')->label('Data')->required(), @@ -168,6 +182,9 @@ public function table(Table $table): Table ->native(false) ->options($this->getTipoEstrattoOptions()) ->default('estratto_conto'), + Toggle::make('is_partenza_contabile') + ->label('Usa questo saldo come partenza contabile') + ->default(false), FileUpload::make('documento') ->label('Allega estratto') ->directory('tmp/banca') @@ -213,20 +230,23 @@ public function table(Table $table): Table ? $this->storeSaldoDocumento($stabileId, $conto, $data['documento'] ?? null, $user, $data) : null; - SaldoConto::query()->create([ - 'stabile_id' => (int) $stabileId, - 'conto_id' => $contoId, - 'iban' => $iban, - 'data_saldo' => $data['data_saldo'] ?? null, - 'periodo_da' => $data['periodo_da'] ?? null, - 'periodo_a' => $data['periodo_a'] ?? null, - 'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : 0.0, - 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null), - 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, - 'documento_stabile_id' => $documento?->id, - 'created_by' => (int) ($user->id ?? 0) ?: null, - 'updated_by' => (int) ($user->id ?? 0) ?: null, + $saldo = SaldoConto::query()->create([ + 'stabile_id' => (int) $stabileId, + 'conto_id' => $contoId, + 'iban' => $iban, + 'data_saldo' => $data['data_saldo'] ?? null, + 'periodo_da' => $data['periodo_da'] ?? null, + 'periodo_a' => $data['periodo_a'] ?? null, + 'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : 0.0, + 'is_partenza_contabile' => (bool) ($data['is_partenza_contabile'] ?? false), + 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null), + 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, + 'documento_stabile_id' => $documento?->id, + 'created_by' => (int) ($user->id ?? 0) ?: null, + 'updated_by' => (int) ($user->id ?? 0) ?: null, ]); + + $this->syncContabilitaAnchorForSaldo($saldo); }), ]) ->actions([ @@ -242,6 +262,8 @@ public function table(Table $table): Table ->label('Tipo riferimento') ->native(false) ->options($this->getTipoEstrattoOptions()), + Toggle::make('is_partenza_contabile') + ->label('Usa questo saldo come partenza contabile'), FileUpload::make('documento') ->label('Sostituisci / aggiungi allegato') ->directory('tmp/banca') @@ -260,31 +282,35 @@ public function table(Table $table): Table ]), Textarea::make('note')->label('Note (opzionale)')->rows(2)->maxLength(255), ]) - ->fillForm(fn(SaldoConto $record): array => [ - 'data_saldo' => $record->data_saldo, - 'periodo_da' => $record->periodo_da, - 'periodo_a' => $record->periodo_a, - 'saldo' => (float) $record->saldo, - 'tipo_estratto' => $record->tipo_estratto, - 'note' => $record->note, + ->fillForm(fn(SaldoConto $record): array=> [ + 'data_saldo' => $record->data_saldo, + 'periodo_da' => $record->periodo_da, + 'periodo_a' => $record->periodo_a, + 'saldo' => (float) $record->saldo, + 'is_partenza_contabile' => (bool) ($record->is_partenza_contabile ?? false), + 'tipo_estratto' => $record->tipo_estratto, + 'note' => $record->note, ]) ->action(function (SaldoConto $record, array $data): void { - $user = Auth::user(); + $user = Auth::user(); $documento = $record->conto ? $this->storeSaldoDocumento((int) $record->stabile_id, $record->conto, $data['documento'] ?? null, $user instanceof User ? $user : null, $data) : null; $record->fill([ - 'data_saldo' => $data['data_saldo'] ?? $record->data_saldo, - 'periodo_da' => $data['periodo_da'] ?? null, - 'periodo_a' => $data['periodo_a'] ?? null, - 'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : (float) $record->saldo, - 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null), - 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, - 'documento_stabile_id' => $documento?->id ?: $record->documento_stabile_id, - 'updated_by' => $user instanceof User ? ((int) $user->id ?: null) : null, + 'data_saldo' => $data['data_saldo'] ?? $record->data_saldo, + 'periodo_da' => $data['periodo_da'] ?? null, + 'periodo_a' => $data['periodo_a'] ?? null, + 'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : (float) $record->saldo, + 'is_partenza_contabile' => (bool) ($data['is_partenza_contabile'] ?? false), + 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null), + 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, + 'documento_stabile_id' => $documento?->id ?: $record->documento_stabile_id, + 'updated_by' => $user instanceof User ? ((int) $user->id ?: null): null, ]); $record->save(); + + $this->syncContabilitaAnchorForSaldo($record); }), Action::make('delete') @@ -321,7 +347,7 @@ protected function getContiOptions(): array ->orderBy('id') ->get(['id', 'denominazione_banca', 'iban']) ->mapWithKeys(function (DatiBancari $r): array { - $iban = is_string($r->iban) ? trim((string) $r->iban) : ''; + $iban = is_string($r->iban) ? trim((string) $r->iban) : ''; $label = trim((string) ($r->denominazione_banca ?: 'Conto') . ($iban !== '' ? (' · ' . $iban) : '')); return [(int) $r->id => $label]; }) @@ -331,10 +357,10 @@ protected function getContiOptions(): array protected function getTipoEstrattoOptions(): array { return [ - 'estratto_conto' => 'Estratto conto', - 'saldo_banca' => 'Saldo banca', + 'estratto_conto' => 'Estratto conto', + 'saldo_banca' => 'Saldo banca', 'riconciliazione' => 'Riconciliazione', - 'altro' => 'Altro', + 'altro' => 'Altro', ]; } @@ -361,7 +387,7 @@ protected function formatTipoEstrattoLabel(?string $value): string protected function formatPeriodoEstrattoLabel(mixed $from, mixed $to): string { $fromDate = $from ? \Illuminate\Support\Carbon::parse((string) $from) : null; - $toDate = $to ? \Illuminate\Support\Carbon::parse((string) $to) : null; + $toDate = $to ? \Illuminate\Support\Carbon::parse((string) $to) : null; if ($fromDate && $toDate) { return $fromDate->format('d/m/Y') . ' - ' . $toDate->format('d/m/Y'); @@ -383,9 +409,9 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed return null; } - $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); - $extension = $extension !== '' ? $extension : 'bin'; - $filename = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension; + $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + $extension = $extension !== '' ? $extension : 'bin'; + $filename = 'estratto-conto-' . $stabileId . '-' . (int) $conto->id . '-' . now()->format('YmdHis') . '.' . $extension; $publicPath = 'documenti/stabili/' . $stabileId . '/bancari/' . $filename; \Illuminate\Support\Facades\Storage::disk('public')->put($publicPath, \Illuminate\Support\Facades\Storage::disk('local')->get($path)); @@ -396,15 +422,28 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed } return DocumentoStabile::query()->create([ - 'stabile_id' => $stabileId, - 'nome_file' => $filename, + 'stabile_id' => $stabileId, + 'nome_file' => $filename, 'nome_originale' => $filename, - 'percorso_file' => $publicPath, - 'categoria' => 'bancari', - 'tipo_mime' => \Illuminate\Support\Facades\Storage::disk('public')->mimeType($publicPath), - 'dimensione' => \Illuminate\Support\Facades\Storage::disk('public')->size($publicPath), - 'descrizione' => 'Estratto conto ' . trim((string) ($conto->denominazione_banca ?: 'Conto')) . ' · ' . $this->formatPeriodoEstrattoLabel($data['periodo_da'] ?? null, $data['periodo_a'] ?? null), - 'caricato_da' => (int) $user->id, + 'percorso_file' => $publicPath, + 'categoria' => 'bancari', + 'tipo_mime' => \Illuminate\Support\Facades\Storage::disk('public')->mimeType($publicPath), + 'dimensione' => \Illuminate\Support\Facades\Storage::disk('public')->size($publicPath), + 'descrizione' => 'Estratto conto ' . trim((string) ($conto->denominazione_banca ?: 'Conto')) . ' · ' . $this->formatPeriodoEstrattoLabel($data['periodo_da'] ?? null, $data['periodo_a'] ?? null), + 'caricato_da' => (int) $user->id, ]); } + + protected function syncContabilitaAnchorForSaldo(SaldoConto $saldo): void + { + if (! ((bool) ($saldo->is_partenza_contabile ?? false))) { + return; + } + + SaldoConto::query() + ->where('stabile_id', (int) $saldo->stabile_id) + ->where('conto_id', (int) $saldo->conto_id) + ->whereKeyNot($saldo->id) + ->update(['is_partenza_contabile' => false]); + } } diff --git a/app/Filament/Pages/Strumenti/DocumentiArchivio.php b/app/Filament/Pages/Strumenti/DocumentiArchivio.php index 0e996aa..bd02d38 100644 --- a/app/Filament/Pages/Strumenti/DocumentiArchivio.php +++ b/app/Filament/Pages/Strumenti/DocumentiArchivio.php @@ -2,9 +2,11 @@ namespace App\Filament\Pages\Strumenti; use App\Models\Documento; +use App\Models\DocumentoArchivioCartella; use App\Models\MovimentoContabile; use App\Models\Stabile; use App\Models\User; +use App\Services\Documenti\DocumentoArchivioVisibilityService; use App\Services\Documenti\PdfTextExtractionService; use App\Support\StabileContext; use BackedEnum; @@ -53,6 +55,8 @@ class DocumentiArchivio extends Page implements HasTable protected string $view = 'filament.pages.strumenti.documenti-archivio'; + private ?DocumentoArchivioVisibilityService $visibilityService = null; + public static function canAccess(): bool { $user = Auth::user(); @@ -100,6 +104,15 @@ protected function getHeaderActions(): array ->action(function (array $data): void { $this->createDocumentoDemoContrattoAscensori((int) $data['stabile_id'], (string) ($data['faldone'] ?? '')); }), + + Action::make('nuova_cartella_archivio') + ->label('Nuova cartella archivio') + ->icon('heroicon-o-folder-plus') + ->color('gray') + ->form($this->getArchivioFolderFormSchema()) + ->action(function (array $data): void { + $this->createArchivioFolder($data); + }), ]; } @@ -110,27 +123,8 @@ protected function getTableQuery(): Builder return Documento::query()->whereRaw('1 = 0'); } - $query = Documento::query(); - - $activeStabile = StabileContext::getActiveStabile($user); - if ($activeStabile instanceof Stabile) { - return $query->where('stabile_id', (int) $activeStabile->id); - } - - if ($user->hasAnyRole(['super-admin', 'admin'])) { - return $query; - } - - $allowedIds = StabileContext::accessibleStabili($user) - ->pluck('id') - ->map(fn($v) => (int) $v) - ->values(); - - if ($allowedIds->isEmpty()) { - return Documento::query()->whereRaw('1 = 0'); - } - - return $query->whereIn('stabile_id', $allowedIds); + return $this->getVisibilityService() + ->scopeVisibleDocumenti(Documento::query()->with('cartellaArchivio'), $user); } public function table(Table $table): Table @@ -161,6 +155,10 @@ public function table(Table $table): Table ->searchable() ->limit(35) ->toggleable(), + TextColumn::make('cartellaArchivio.nome') + ->label('Cartella') + ->toggleable() + ->wrap(), TextColumn::make('contenuto_ocr') ->label('Testo (OCR)') ->limit(50) @@ -290,6 +288,44 @@ public function getArchivioFisicoTypesProperty(): array return ['Faldone', 'Scatola', 'Cartella', 'Busta']; } + public function getVisibleFolderRowsProperty(): array + { + $user = Auth::user(); + if (! $user instanceof User) { + return []; + } + + $stabileId = $this->resolveActiveStabile()?->id; + + return $this->getVisibilityService() + ->scopeVisibleCartelle(DocumentoArchivioCartella::query()->with('parent', 'stabile'), $user, $stabileId ? (int) $stabileId : null) + ->orderBy('sort_order') + ->orderBy('nome') + ->get() + ->map(function (DocumentoArchivioCartella $folder): array { + $roles = collect($folder->visibility_roles ?? [])->map(fn($value): string => (string) $value)->filter()->values()->all(); + $groups = collect($folder->visibility_groups ?? [])->map(fn($value): string => (string) $value)->filter()->values()->all(); + $users = collect($folder->allowed_user_ids ?? [])->filter(fn($value): bool => is_numeric($value) && (int) $value > 0)->map(fn($value): int => (int) $value)->all(); + + $userNames = $users === [] + ? [] + : User::query()->whereIn('id', $users)->orderBy('name')->pluck('name')->map(fn($value): string => (string) $value)->all(); + + return [ + 'id' => (int) $folder->id, + 'nome' => (string) $folder->nome, + 'path' => (string) ($folder->display_path ?: $folder->nome), + 'stabile' => (string) ($folder->stabile?->denominazione ?: ('Stabile #' . (int) $folder->stabile_id)), + 'scope' => (string) ($folder->visibility_scope ?: 'interno'), + 'roles' => $roles, + 'groups' => $groups, + 'users' => $userNames, + 'documents_count' => (int) $folder->documenti()->count(), + ]; + }) + ->all(); + } + public function getMnemonicCodeHintProperty(): ?string { $stabile = $this->resolveActiveStabile(); @@ -314,7 +350,13 @@ private function getDocumentoFormSchema(): array ->options(fn() => $this->getStabiliOptions()) ->default(fn() => $this->getDefaultStabileId()) ->searchable() + ->live() ->required(), + Select::make('cartella_archivio_id') + ->label('Cartella archivio') + ->options(fn(callable $get): array => $this->getArchivioFolderOptions((int) ($get('stabile_id') ?: 0))) + ->searchable() + ->nullable(), Select::make('categoria') ->label('Categoria protocollo') ->options($this->getCategorieOptions()) @@ -352,6 +394,22 @@ private function getDocumentoFormSchema(): array ->placeholder('Es. CONTR-2024') ->maxLength(50) ->nullable(), + Select::make('visibility_scope') + ->label('Visibilità') + ->options($this->getVisibilityScopeOptions()) + ->default('interno') + ->native(false) + ->required(), + Select::make('visibility_roles') + ->label('Ruoli ammessi') + ->options($this->getVisibilityRoleOptions()) + ->multiple() + ->native(false), + Select::make('visibility_groups') + ->label('Gruppi audience') + ->options(array_combine($this->audienceGroups, $this->audienceGroups)) + ->multiple() + ->native(false), FileUpload::make('pdf') ->label('PDF') ->disk('local') @@ -360,6 +418,55 @@ private function getDocumentoFormSchema(): array ]; } + private function getArchivioFolderFormSchema(): array + { + return [ + Select::make('stabile_id') + ->label('Stabile') + ->options(fn(): array => $this->getStabiliOptions()) + ->default(fn(): ?int => $this->getDefaultStabileId()) + ->searchable() + ->live() + ->required(), + Select::make('parent_id') + ->label('Cartella padre') + ->options(fn(callable $get): array => $this->getArchivioFolderOptions((int) ($get('stabile_id') ?: 0))) + ->searchable() + ->nullable(), + TextInput::make('nome') + ->label('Nome cartella') + ->required() + ->maxLength(160), + TextInput::make('codice') + ->label('Codice cartella') + ->maxLength(80), + Select::make('visibility_scope') + ->label('Visibilità') + ->options($this->getVisibilityScopeOptions()) + ->default('interno') + ->native(false) + ->required(), + Select::make('visibility_roles') + ->label('Ruoli ammessi') + ->options($this->getVisibilityRoleOptions()) + ->multiple() + ->native(false), + Select::make('visibility_groups') + ->label('Gruppi audience') + ->options(array_combine($this->audienceGroups, $this->audienceGroups)) + ->multiple() + ->native(false), + Select::make('allowed_user_ids') + ->label('Utenti specifici') + ->options($this->getCollaboratorUserOptions()) + ->multiple() + ->native(false), + Textarea::make('note') + ->label('Note cartella') + ->rows(3), + ]; + } + private function getStabiliOptions(): array { $user = Auth::user(); @@ -399,6 +506,68 @@ private function getDefaultStabileId(): ?int return StabileContext::accessibleStabili($user)->first()?->id; } + private function getVisibilityService(): DocumentoArchivioVisibilityService + { + return $this->visibilityService ??= app(DocumentoArchivioVisibilityService::class); + } + + private function getVisibilityScopeOptions(): array + { + return [ + 'interno' => 'Interno studio', + 'studio' => 'Studio completo', + 'restricted' => 'Limitato a ruoli / utenti', + ]; + } + + private function getVisibilityRoleOptions(): array + { + return [ + 'super-admin' => 'Super admin', + 'admin' => 'Admin', + 'amministratore' => 'Amministratore', + 'collaboratore' => 'Collaboratore', + ]; + } + + private function getCollaboratorUserOptions(): array + { + return User::query() + ->role(['super-admin', 'admin', 'amministratore', 'collaboratore']) + ->orderBy('name') + ->limit(300) + ->get(['id', 'name', 'email']) + ->mapWithKeys(function (User $user): array { + $label = trim((string) $user->name); + if (trim((string) $user->email) !== '') { + $label .= ' <' . trim((string) $user->email) . '>'; + } + + return [(string) $user->id => $label !== '' ? $label : ('Utente #' . (int) $user->id)]; + }) + ->all(); + } + + private function getArchivioFolderOptions(int $stabileId): array + { + if ($stabileId <= 0) { + return []; + } + + return DocumentoArchivioCartella::query() + ->where('stabile_id', $stabileId) + ->where('is_active', true) + ->orderBy('sort_order') + ->orderBy('nome') + ->get(['id', 'nome', 'percorso_logico']) + ->mapWithKeys(function (DocumentoArchivioCartella $folder): array { + $label = trim((string) ($folder->percorso_logico ?: $folder->nome)); + + return [(string) $folder->id => $label !== '' ? $label : ('Cartella #' . (int) $folder->id)]; + }) + ->all(); + } + private function normalizeHubTab(string $tab): string { $allowed = ['archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi']; @@ -447,6 +616,66 @@ private function getMovimentiOptions(int $stabileId): array ->all(); } + private function createArchivioFolder(array $data): void + { + $user = Auth::user(); + if (! $user instanceof User) { + Notification::make()->title('Utente non valido')->danger()->send(); + return; + } + + $stabileId = isset($data['stabile_id']) && is_numeric($data['stabile_id']) ? (int) $data['stabile_id'] : 0; + if ($stabileId <= 0) { + Notification::make()->title('Stabile obbligatorio')->danger()->send(); + return; + } + + $allowedIds = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($value): int => (int) $value)->all(); + if (! in_array($stabileId, $allowedIds, true) && ! $user->hasAnyRole(['super-admin', 'admin'])) { + Notification::make()->title('Stabile non accessibile')->danger()->send(); + return; + } + + $name = trim((string) ($data['nome'] ?? '')); + if ($name === '') { + Notification::make()->title('Nome cartella obbligatorio')->danger()->send(); + return; + } + + $parentId = isset($data['parent_id']) && is_numeric($data['parent_id']) ? (int) $data['parent_id'] : null; + $parent = $parentId + ? DocumentoArchivioCartella::query()->where('stabile_id', $stabileId)->find($parentId) + : null; + + $slug = Str::slug($name); + if ($slug === '') { + $slug = 'cartella-' . now()->format('YmdHis'); + } + + $logicalPath = collect([ + $parent?->percorso_logico, + $name, + ])->filter(fn($value): bool => is_string($value) && trim($value) !== '')->implode('/'); + + DocumentoArchivioCartella::query()->create([ + 'stabile_id' => $stabileId, + 'parent_id' => $parent?->id, + 'nome' => $name, + 'slug' => $slug, + 'codice' => trim((string) ($data['codice'] ?? '')) ?: null, + 'percorso_logico' => $logicalPath !== '' ? $logicalPath : $name, + 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), + 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), + 'visibility_groups' => $this->normalizeStringArray($data['visibility_groups'] ?? []), + 'allowed_user_ids' => $this->normalizeIntArray($data['allowed_user_ids'] ?? []), + 'note' => trim((string) ($data['note'] ?? '')) ?: null, + 'created_by' => (int) $user->id, + 'updated_by' => (int) $user->id, + ]); + + Notification::make()->title('Cartella archivio creata')->success()->send(); + } + private function createDocumentoFromForm(array $data, bool $isDemo): void { $user = Auth::user(); @@ -542,6 +771,7 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void 'documentable_type' => null, 'documentable_id' => null, 'stabile_id' => $stabileId, + 'cartella_archivio_id' => $this->resolveFolderIdForDocument($stabileId, $data['cartella_archivio_id'] ?? null), 'utente_id' => (int) $user->id, 'nome' => $titolo, 'descrizione' => (string) ($data['descrizione'] ?? ''), @@ -559,6 +789,9 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void 'percorso_file' => $finalPath, 'estensione' => 'pdf', 'dimensione_file' => $size, + 'visibility_scope' => (string) ($data['visibility_scope'] ?? 'interno'), + 'visibility_roles' => $this->normalizeStringArray($data['visibility_roles'] ?? []), + 'visibility_groups' => $this->normalizeStringArray($data['visibility_groups'] ?? []), 'data_upload' => now(), 'note' => $note, 'is_demo' => $isDemo, @@ -647,6 +880,7 @@ private function createDocumentoDemoContrattoAscensori(int $stabileId, string $f 'percorso_file' => $finalPath, 'estensione' => 'pdf', 'dimensione_file' => $size, + 'visibility_scope' => 'interno', 'data_upload' => now(), 'note' => $note, 'is_demo' => true, @@ -726,4 +960,46 @@ private function mapCategoriaToTipologia(string $categoria): string default => 'altro', }; } + + private function resolveFolderIdForDocument(int $stabileId, mixed $folderId): ?int + { + if (! is_numeric($folderId)) { + return null; + } + + $folder = DocumentoArchivioCartella::query() + ->where('stabile_id', $stabileId) + ->whereKey((int) $folderId) + ->first(['id']); + + return $folder ? (int) $folder->id : null; + } + + private function normalizeStringArray(mixed $values): array + { + if (! is_array($values)) { + return []; + } + + return collect($values) + ->map(fn($value): string => trim((string) $value)) + ->filter(fn(string $value): bool => $value !== '') + ->unique() + ->values() + ->all(); + } + + private function normalizeIntArray(mixed $values): array + { + if (! is_array($values)) { + return []; + } + + return collect($values) + ->filter(fn($value): bool => is_numeric($value) && (int) $value > 0) + ->map(fn($value): int => (int) $value) + ->unique() + ->values() + ->all(); + } } diff --git a/app/Http/Controllers/Filament/DocumentiPrintController.php b/app/Http/Controllers/Filament/DocumentiPrintController.php index b6ba2d1..a939fb7 100644 --- a/app/Http/Controllers/Filament/DocumentiPrintController.php +++ b/app/Http/Controllers/Filament/DocumentiPrintController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\Documento; use App\Models\User; +use App\Services\Documenti\DocumentoArchivioVisibilityService; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; @@ -13,8 +14,13 @@ class DocumentiPrintController extends Controller { - public function download(Request $request, Documento $documento) + public function download(Request $request, Documento $documento, DocumentoArchivioVisibilityService $visibilityService) { + $user = Auth::user(); + if (! $user instanceof User || ! $visibilityService->canAccessDocumento($user, $documento)) { + abort(403); + } + $pathCandidates = []; if (is_string($documento->percorso_file) && trim($documento->percorso_file) !== '') { $pathCandidates[] = trim($documento->percorso_file); @@ -57,8 +63,13 @@ public function download(Request $request, Documento $documento) return Storage::disk($selectedDisk)->download($selectedPath, $filename); } - public function etichetta(Request $request, Documento $documento) + public function etichetta(Request $request, Documento $documento, DocumentoArchivioVisibilityService $visibilityService) { + $user = Auth::user(); + if (! $user instanceof User || ! $visibilityService->canAccessDocumento($user, $documento)) { + abort(403); + } + $format = $request->query('format', '11354'); $format = in_array($format, ['11354', '99014'], true) ? $format : '11354'; diff --git a/app/Http/Controllers/Filament/DocumentoStabileDownloadController.php b/app/Http/Controllers/Filament/DocumentoStabileDownloadController.php index 4f56a04..cb5c9d3 100644 --- a/app/Http/Controllers/Filament/DocumentoStabileDownloadController.php +++ b/app/Http/Controllers/Filament/DocumentoStabileDownloadController.php @@ -3,13 +3,21 @@ use App\Http\Controllers\Controller; use App\Models\DocumentoStabile; +use App\Models\User; +use App\Services\Documenti\DocumentoArchivioVisibilityService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; class DocumentoStabileDownloadController extends Controller { - public function download(Request $request, DocumentoStabile $documentoStabile) + public function download(Request $request, DocumentoStabile $documentoStabile, DocumentoArchivioVisibilityService $visibilityService) { + $user = Auth::user(); + if (! $user instanceof User || ! $visibilityService->canAccessDocumentoStabile($user, $documentoStabile)) { + abort(403); + } + $path = is_string($documentoStabile->percorso_file) ? trim((string) $documentoStabile->percorso_file) : ''; if ($path === '') { abort(404); diff --git a/app/Livewire/Condomini/StabileDatiBancariTable.php b/app/Livewire/Condomini/StabileDatiBancariTable.php index 3282844..489bfbd 100644 --- a/app/Livewire/Condomini/StabileDatiBancariTable.php +++ b/app/Livewire/Condomini/StabileDatiBancariTable.php @@ -1,5 +1,4 @@ label('Saldo ufficiale') - ->stateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record)) + ->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record)) ->money('EUR') ->toggleable(), TextColumn::make('ultimo_estratto') ->label('Ultimo estratto') - ->stateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record)) + ->getStateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record)) ->wrap() ->toggleable(), @@ -131,8 +130,8 @@ public function table(Table $table): Table return; } - $amm = $stabile->amministratore; - $adminCode = $amm ? (string) ($amm->codice_univoco ?: $amm->codice_amministratore ?: '') : ''; + $amm = $stabile->amministratore; + $adminCode = $amm ? (string) ($amm->codice_univoco ?: $amm->codice_amministratore ?: ''): ''; $adminCode = trim($adminCode); if ($adminCode === '') { $adminCode = $amm ? ('ID' . $amm->id) : 'ID0'; @@ -174,12 +173,12 @@ public function table(Table $table): Table ->form($this->formSchema()) ->action(function (array $data): void { $user = Auth::user(); - $uid = $user instanceof User ? (int) $user->id : null; + $uid = $user instanceof User ? (int) $user->id : null; - $payload = $this->normalizePayload($data); + $payload = $this->normalizePayload($data); $payload['stabile_id'] = (int) $this->stabileId; if ($uid) { - $payload['creato_da'] = $uid; + $payload['creato_da'] = $uid; $payload['modificato_da'] = $uid; } @@ -197,17 +196,13 @@ public function table(Table $table): Table ->icon('heroicon-o-receipt-percent') ->url(function (DatiBancari $record): string { $base = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); - $iban = is_string($record->iban ?? null) ? trim((string) $record->iban) : ''; - if ($iban === '') { + $contoId = (int) ($record->id ?? 0); + if ($contoId <= 0) { return $base; } return $base . '?' . http_build_query([ - 'tableFilters' => [ - 'conto' => [ - 'ibans' => [$iban], - ], - ], + 'conto_id' => $contoId, ]); }) ->openUrlInNewTab(), @@ -215,7 +210,7 @@ public function table(Table $table): Table Action::make('saldi') ->label('Saldi') ->icon('heroicon-o-scale') - ->url(fn(): string => SaldiContiArchivio::getUrl(panel: 'admin-filament')) + ->url(fn(DatiBancari $record): string => SaldiContiArchivio::getUrl(['conto_id' => (int) $record->id], panel: 'admin-filament')) ->openUrlInNewTab(), Action::make('contatto') @@ -230,30 +225,30 @@ public function table(Table $table): Table ->label('Modifica') ->icon('heroicon-o-pencil-square') ->modalHeading('Modifica conto') - ->fillForm(fn(DatiBancari $record): array => [ + ->fillForm(fn(DatiBancari $record): array=> [ 'denominazione_banca' => $record->denominazione_banca, - 'tipo_conto' => $record->tipo_conto, - 'stato_conto' => $record->stato_conto, - 'iban' => $record->iban, - 'numero_conto' => $record->numero_conto, - 'abi' => $record->abi, - 'cab' => $record->cab, - 'cin' => $record->cin, - 'bic_swift' => $record->bic_swift, - 'cuc' => $record->cuc, - 'sia' => $record->sia, - 'intestazione_conto' => $record->intestazione_conto, + 'tipo_conto' => $record->tipo_conto, + 'stato_conto' => $record->stato_conto, + 'iban' => $record->iban, + 'numero_conto' => $record->numero_conto, + 'abi' => $record->abi, + 'cab' => $record->cab, + 'cin' => $record->cin, + 'bic_swift' => $record->bic_swift, + 'cuc' => $record->cuc, + 'sia' => $record->sia, + 'intestazione_conto' => $record->intestazione_conto, 'data_saldo_iniziale' => $record->data_saldo_iniziale, - 'saldo_iniziale' => $record->saldo_iniziale, - 'valuta' => $record->valuta, - 'is_nostro_conto' => (bool) $record->is_nostro_conto, - 'contatto_id' => $record->contatto_id, - 'note' => $record->note, + 'saldo_iniziale' => $record->saldo_iniziale, + 'valuta' => $record->valuta, + 'is_nostro_conto' => (bool) $record->is_nostro_conto, + 'contatto_id' => $record->contatto_id, + 'note' => $record->note, ]) ->form($this->formSchema()) ->action(function (DatiBancari $record, array $data): void { $user = Auth::user(); - $uid = $user instanceof User ? (int) $user->id : null; + $uid = $user instanceof User ? (int) $user->id : null; $payload = $this->normalizePayload($data); if ($uid) { @@ -306,8 +301,8 @@ private function formSchema(): array Select::make('tipo_conto') ->label('Tipo conto') ->options([ - 'corrente' => 'Corrente', - 'deposito' => 'Deposito', + 'corrente' => 'Corrente', + 'deposito' => 'Deposito', 'risparmio' => 'Risparmio', ]) ->default('corrente') @@ -316,9 +311,9 @@ private function formSchema(): array Select::make('stato_conto') ->label('Stato') ->options([ - 'attivo' => 'Attivo', + 'attivo' => 'Attivo', 'sospeso' => 'Sospeso', - 'chiuso' => 'Chiuso', + 'chiuso' => 'Chiuso', ]) ->default('attivo') ->required(), @@ -420,23 +415,23 @@ private function normalizePayload(array $data): array { return [ 'denominazione_banca' => isset($data['denominazione_banca']) ? trim((string) $data['denominazione_banca']) : null, - 'tipo_conto' => isset($data['tipo_conto']) ? (string) $data['tipo_conto'] : null, - 'stato_conto' => isset($data['stato_conto']) ? (string) $data['stato_conto'] : null, - 'iban' => isset($data['iban']) ? trim((string) $data['iban']) : null, - 'numero_conto' => isset($data['numero_conto']) ? trim((string) $data['numero_conto']) : null, - 'abi' => isset($data['abi']) ? trim((string) $data['abi']) : null, - 'cab' => isset($data['cab']) ? trim((string) $data['cab']) : null, - 'cin' => isset($data['cin']) ? trim((string) $data['cin']) : null, - 'bic_swift' => isset($data['bic_swift']) ? trim((string) $data['bic_swift']) : null, - 'cuc' => isset($data['cuc']) ? trim((string) $data['cuc']) : null, - 'sia' => isset($data['sia']) ? trim((string) $data['sia']) : null, - 'intestazione_conto' => isset($data['intestazione_conto']) ? trim((string) $data['intestazione_conto']) : null, + 'tipo_conto' => isset($data['tipo_conto']) ? (string) $data['tipo_conto'] : null, + 'stato_conto' => isset($data['stato_conto']) ? (string) $data['stato_conto'] : null, + 'iban' => isset($data['iban']) ? trim((string) $data['iban']) : null, + 'numero_conto' => isset($data['numero_conto']) ? trim((string) $data['numero_conto']) : null, + 'abi' => isset($data['abi']) ? trim((string) $data['abi']) : null, + 'cab' => isset($data['cab']) ? trim((string) $data['cab']) : null, + 'cin' => isset($data['cin']) ? trim((string) $data['cin']) : null, + 'bic_swift' => isset($data['bic_swift']) ? trim((string) $data['bic_swift']) : null, + 'cuc' => isset($data['cuc']) ? trim((string) $data['cuc']) : null, + 'sia' => isset($data['sia']) ? trim((string) $data['sia']) : null, + 'intestazione_conto' => isset($data['intestazione_conto']) ? trim((string) $data['intestazione_conto']) : null, 'data_saldo_iniziale' => $data['data_saldo_iniziale'] ?? null, - 'saldo_iniziale' => $data['saldo_iniziale'] ?? null, - 'valuta' => isset($data['valuta']) ? trim((string) $data['valuta']) : null, - 'is_nostro_conto' => (bool) ($data['is_nostro_conto'] ?? false), - 'contatto_id' => ! empty($data['contatto_id']) ? (int) $data['contatto_id'] : null, - 'note' => isset($data['note']) ? trim((string) $data['note']) : null, + 'saldo_iniziale' => $data['saldo_iniziale'] ?? null, + 'valuta' => isset($data['valuta']) ? trim((string) $data['valuta']) : null, + 'is_nostro_conto' => (bool) ($data['is_nostro_conto'] ?? false), + 'contatto_id' => ! empty($data['contatto_id']) ? (int) $data['contatto_id'] : null, + 'note' => isset($data['note']) ? trim((string) $data['note']) : null, ]; } @@ -475,15 +470,15 @@ private function resolveLatestSaldoSnapshotLabel(DatiBancari $record): string $parts = []; if ($snapshot->tipo_estratto) { $parts[] = match ($snapshot->tipo_estratto) { - 'estratto_conto' => 'Estratto conto', - 'saldo_banca' => 'Saldo banca', + 'estratto_conto' => 'Estratto conto', + 'saldo_banca' => 'Saldo banca', 'riconciliazione' => 'Riconciliazione', - default => 'Altro', + default => 'Altro', }; } if ($snapshot->periodo_da || $snapshot->periodo_a) { - $from = $snapshot->periodo_da?->format('d/m/Y') ?: '...'; - $to = $snapshot->periodo_a?->format('d/m/Y') ?: '...'; + $from = $snapshot->periodo_da?->format('d/m/Y') ?: '...'; + $to = $snapshot->periodo_a?->format('d/m/Y') ?: '...'; $parts[] = $from . ' - ' . $to; } else { $parts[] = $snapshot->data_saldo?->format('d/m/Y') ?: 'Data non indicata'; diff --git a/app/Models/Documento.php b/app/Models/Documento.php index c0e71a8..f390cbf 100755 --- a/app/Models/Documento.php +++ b/app/Models/Documento.php @@ -38,6 +38,7 @@ class Documento extends Model // Nuovi campi per sistema avanzato 'stabile_id', + 'cartella_archivio_id', 'unita_id', 'utente_id', 'nome', @@ -106,6 +107,11 @@ public function stabile(): BelongsTo return $this->belongsTo(Stabile::class); } + public function cartellaArchivio(): BelongsTo + { + return $this->belongsTo(DocumentoArchivioCartella::class, 'cartella_archivio_id'); + } + public function unita(): BelongsTo { return $this->belongsTo(UnitaImmobiliare::class); diff --git a/app/Models/DocumentoArchivioCartella.php b/app/Models/DocumentoArchivioCartella.php new file mode 100644 index 0000000..02ef94f --- /dev/null +++ b/app/Models/DocumentoArchivioCartella.php @@ -0,0 +1,73 @@ + 'array', + 'visibility_groups' => 'array', + 'allowed_user_ids' => 'array', + 'sort_order' => 'integer', + 'is_active' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + public function stabile(): BelongsTo + { + return $this->belongsTo(Stabile::class, 'stabile_id'); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(self::class, 'parent_id'); + } + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order')->orderBy('nome'); + } + + public function documenti(): HasMany + { + return $this->hasMany(Documento::class, 'cartella_archivio_id'); + } + + public function getDisplayPathAttribute(): string + { + $parts = array_filter([ + trim((string) ($this->parent?->display_path ?? '')), + trim((string) ($this->nome ?? '')), + ]); + + return $parts === [] ? (string) ($this->nome ?? '') : implode(' / ', $parts); + } +} \ No newline at end of file diff --git a/app/Services/Documenti/DocumentoArchivioVisibilityService.php b/app/Services/Documenti/DocumentoArchivioVisibilityService.php new file mode 100644 index 0000000..fd18312 --- /dev/null +++ b/app/Services/Documenti/DocumentoArchivioVisibilityService.php @@ -0,0 +1,141 @@ +where('stabile_id', (int) $activeStabile->id); + } elseif (! $user->hasAnyRole(['super-admin', 'admin'])) { + $allowedIds = StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value): int => (int) $value) + ->all(); + + if ($allowedIds === []) { + $query->whereRaw('1 = 0'); + + return $query; + } + + $query->whereIn('stabile_id', $allowedIds); + } + + return $query->where(function (Builder $documentQuery) use ($user): void { + $documentQuery->whereNull('cartella_archivio_id') + ->where(function (Builder $baseVisibility) use ($user): void { + $baseVisibility->whereNull('visibility_scope') + ->orWhere('visibility_scope', 'interno') + ->orWhere(function (Builder $restricted) use ($user): void { + $this->applyVisibilityConstraint($restricted, $user, 'documenti'); + }); + }) + ->orWhereHas('cartellaArchivio', function (Builder $folderQuery) use ($user): void { + $folderQuery->where('is_active', true); + $this->applyVisibilityConstraint($folderQuery, $user, 'documento_archivio_cartelle'); + }); + }); + } + + public function scopeVisibleCartelle(Builder $query, User $user, ?int $stabileId = null): Builder + { + if ($stabileId !== null && $stabileId > 0) { + $query->where('stabile_id', $stabileId); + } else { + $activeStabile = StabileContext::getActiveStabile($user); + if ($activeStabile !== null) { + $query->where('stabile_id', (int) $activeStabile->id); + } elseif (! $user->hasAnyRole(['super-admin', 'admin'])) { + $allowedIds = StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value): int => (int) $value) + ->all(); + + if ($allowedIds === []) { + $query->whereRaw('1 = 0'); + + return $query; + } + + $query->whereIn('stabile_id', $allowedIds); + } + } + + $query->where('is_active', true); + $this->applyVisibilityConstraint($query, $user, 'documento_archivio_cartelle'); + + return $query; + } + + public function canAccessDocumento(User $user, Documento $documento): bool + { + return $this->scopeVisibleDocumenti(Documento::query()->whereKey($documento->getKey()), $user)->exists(); + } + + public function canAccessDocumentoStabile(User $user, DocumentoStabile $documentoStabile): bool + { + $allowedIds = StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value): int => (int) $value) + ->all(); + + return in_array((int) $documentoStabile->stabile_id, $allowedIds, true); + } + + private function applyVisibilityConstraint(Builder $query, User $user, string $table): void + { + $roles = $user->getRoleNames()->map(fn($role): string => (string) $role)->values()->all(); + $groupLabels = $this->resolveAudienceGroupsForUser($user); + $userId = (int) $user->id; + + $query->where(function (Builder $visibilityQuery) use ($table, $roles, $groupLabels, $userId): void { + $visibilityQuery->whereNull($table . '.visibility_scope') + ->orWhere($table . '.visibility_scope', 'interno') + ->orWhere($table . '.visibility_scope', 'studio') + ->orWhere(function (Builder $restrictedQuery) use ($table, $roles, $groupLabels, $userId): void { + $restrictedQuery->where($table . '.visibility_scope', 'restricted') + ->where(function (Builder $matchQuery) use ($table, $roles, $groupLabels, $userId): void { + if ($roles !== []) { + foreach ($roles as $role) { + $matchQuery->orWhereJsonContains($table . '.visibility_roles', $role); + } + } + + if ($groupLabels !== []) { + foreach ($groupLabels as $groupLabel) { + $matchQuery->orWhereJsonContains($table . '.visibility_groups', $groupLabel); + } + } + + $matchQuery->orWhereJsonContains($table . '.allowed_user_ids', $userId); + }); + }); + }); + } + + private function resolveAudienceGroupsForUser(User $user): array + { + $groups = []; + + if ($user->hasAnyRole(['super-admin', 'admin', 'amministratore'])) { + $groups[] = 'Amministratore'; + $groups[] = 'Studio completo'; + } + + if ($user->hasRole('collaboratore')) { + $groups[] = 'Collaboratori interni'; + } + + return array_values(array_unique($groups)); + } +} \ No newline at end of file diff --git a/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php b/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php new file mode 100644 index 0000000..e164f13 --- /dev/null +++ b/database/migrations/2026_04_20_210000_create_documento_archivio_cartelle_table.php @@ -0,0 +1,57 @@ +id(); + $table->foreignId('stabile_id')->constrained('stabili')->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('documento_archivio_cartelle')->nullOnDelete(); + $table->string('nome'); + $table->string('slug', 160); + $table->string('codice', 80)->nullable(); + $table->string('percorso_logico')->nullable(); + $table->string('visibility_scope', 40)->default('interno'); + $table->json('visibility_roles')->nullable(); + $table->json('visibility_groups')->nullable(); + $table->json('allowed_user_ids')->nullable(); + $table->text('note')->nullable(); + $table->unsignedInteger('sort_order')->default(0); + $table->boolean('is_active')->default(true); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->unique(['stabile_id', 'parent_id', 'slug'], 'doc_arch_folder_parent_slug_unique'); + $table->index(['stabile_id', 'visibility_scope'], 'doc_arch_folder_stabile_scope_idx'); + }); + } + + Schema::table('documenti', function (Blueprint $table): void { + if (! Schema::hasColumn('documenti', 'cartella_archivio_id')) { + $table->foreignId('cartella_archivio_id') + ->nullable() + ->after('stabile_id') + ->constrained('documento_archivio_cartelle') + ->nullOnDelete(); + } + }); + } + + public function down(): void + { + Schema::table('documenti', function (Blueprint $table): void { + if (Schema::hasColumn('documenti', 'cartella_archivio_id')) { + $table->dropConstrainedForeignId('cartella_archivio_id'); + } + }); + + Schema::dropIfExists('documento_archivio_cartelle'); + } +}; \ No newline at end of file diff --git a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php index 51d6c40..661dcba 100644 --- a/resources/views/filament/pages/strumenti/documenti-archivio.blade.php +++ b/resources/views/filament/pages/strumenti/documenti-archivio.blade.php @@ -4,7 +4,7 @@ 'archivio-digitale' => 'Archivio digitale', 'template-drive' => 'Template Drive', 'archivio-fisico' => 'Archivio fisico', - 'permessi' => 'Permessi', + 'permessi' => 'Permessi e cartelle', ]; $stats = $this->hubStats; @endphp @@ -108,15 +108,44 @@ @else Permessi e visibilita - Qui prepariamo la classificazione dei documenti per audience senza mischiare condomini, inquilini, fornitori e uso interno studio. + Qui prepariamo la classificazione dei documenti per audience e le cartelle visibili solo a ruoli, gruppi o utenti selezionati. -
- @foreach($this->audienceGroups as $group) -
-
{{ $group }}
-
Audience candidata per visibilita documentale, export e portali dedicati.
+
+
+
+ @foreach($this->visibleFolderRows as $folder) +
+
+
+
{{ $folder['nome'] }}
+
{{ $folder['stabile'] }}
+
+
{{ $folder['scope'] }}
+
+
{{ $folder['path'] }}
+
Documenti collegati: {{ $folder['documents_count'] }}
+
Ruoli: {{ $folder['roles'] !== [] ? implode(', ', $folder['roles']) : 'nessun filtro' }}
+
Gruppi: {{ $folder['groups'] !== [] ? implode(', ', $folder['groups']) : 'nessun filtro' }}
+
Utenti: {{ $folder['users'] !== [] ? implode(', ', $folder['users']) : 'nessun utente specifico' }}
+
+ @endforeach + @if(empty($this->visibleFolderRows)) +
Nessuna cartella archivio configurata per lo stabile attivo. Usa “Nuova cartella archivio” per iniziare a separare visibilita e struttura.
+ @endif
- @endforeach +
+ +
+ @foreach($this->audienceGroups as $group) +
+
{{ $group }}
+
Audience candidata per visibilita documentale, export e portali dedicati.
+
+ @endforeach +
+ Le cartelle ristrette sono applicate sia nella lista documenti sia nei download diretti, cosi l'ID del file non bypassa i permessi dello stabile o della cartella. +
+
@endif