Harden document archive access and finalize TecnoRepair dedupe

This commit is contained in:
michele 2026-04-20 19:54:16 +00:00
parent dd825b4450
commit e462a0b033
11 changed files with 904 additions and 150 deletions

View File

@ -108,6 +108,7 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
'schede_lette' => count($schedeRows), 'schede_lette' => count($schedeRows),
'schede_create' => 0, 'schede_create' => 0,
'schede_aggiornate' => 0, 'schede_aggiornate' => 0,
'schede_consolidate' => 0,
'schede_saltate' => 0, 'schede_saltate' => 0,
'allegati_importati' => 0, 'allegati_importati' => 0,
'seriali_allineati' => 0, 'seriali_allineati' => 0,
@ -118,6 +119,13 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
$fornitore->forceFill(['is_tecnorepair_primary' => true])->save(); $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) { foreach ($schedeRows as $row) {
$legacyId = $this->toInt($row['ID'] ?? null); $legacyId = $this->toInt($row['ID'] ?? null);
if ($legacyId === null) { if ($legacyId === null) {
@ -168,10 +176,22 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
], ],
]; ];
$existing = AssistenzaTecnorepairScheda::query() $matchingSchede = $this->buildExistingSchedaQuery(
->where('imported_from_path', $mdbPath) amministratoreId: (int) $admin->id,
->where('legacy_id', $legacyId) legacyId: $legacyId,
->first(); 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 ($dryRun) {
if ($existing) { if ($existing) {
@ -179,16 +199,27 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
} else { } else {
$stats['schede_create']++; $stats['schede_create']++;
} }
if ($duplicateSchedaIds !== []) {
$stats['schede_consolidate'] += count($duplicateSchedaIds);
}
continue; continue;
} }
$scheda = AssistenzaTecnorepairScheda::query()->updateOrCreate( if ($duplicateSchedaIds !== []) {
[ ProductSerial::query()->whereIn('legacy_scheda_id', $duplicateSchedaIds)->delete();
'imported_from_path' => $mdbPath, AssistenzaTecnorepairScheda::query()->whereKey($duplicateSchedaIds)->delete();
'legacy_id' => $legacyId, $stats['schede_consolidate'] += count($duplicateSchedaIds);
], }
$payload,
); if ($existing instanceof AssistenzaTecnorepairScheda) {
$existing->fill($payload);
$existing->save();
$scheda = $existing;
} else {
$scheda = AssistenzaTecnorepairScheda::query()->create($payload);
}
if ($existing) { if ($existing) {
$stats['schede_aggiornate']++; $stats['schede_aggiornate']++;
@ -251,6 +282,7 @@ public function handle(TecnoRepairMdbReader $reader, FornitoreProductCatalogServ
['Schede lette', (string) $stats['schede_lette']], ['Schede lette', (string) $stats['schede_lette']],
['Schede create', (string) $stats['schede_create']], ['Schede create', (string) $stats['schede_create']],
['Schede aggiornate', (string) $stats['schede_aggiornate']], ['Schede aggiornate', (string) $stats['schede_aggiornate']],
['Schede consolidate', (string) $stats['schede_consolidate']],
['Schede saltate', (string) $stats['schede_saltate']], ['Schede saltate', (string) $stats['schede_saltate']],
['Allegati importati', (string) $stats['allegati_importati']], ['Allegati importati', (string) $stats['allegati_importati']],
['Seriali allineati', (string) $stats['seriali_allineati']], ['Seriali allineati', (string) $stats['seriali_allineati']],
@ -302,6 +334,93 @@ private function resolveFornitore(Amministratore $admin, string $fornitoreIdOpti
->first(); ->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 private function clean(mixed $value): ?string
{ {
if (! is_string($value) && ! is_numeric($value)) { if (! is_string($value) && ! is_numeric($value)) {

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Filament\Pages\Contabilita; namespace App\Filament\Pages\Contabilita;
use App\Models\DatiBancari; use App\Models\DatiBancari;
@ -14,6 +13,7 @@
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
@ -22,12 +22,16 @@
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Url;
use UnitEnum; use UnitEnum;
class SaldiContiArchivio extends Page implements HasTable class SaldiContiArchivio extends Page implements HasTable
{ {
use InteractsWithTable; use InteractsWithTable;
#[Url(as: 'conto_id')]
public ?int $contoId = null;
protected static ?string $navigationLabel = 'Saldi conti'; protected static ?string $navigationLabel = 'Saldi conti';
protected static ?string $title = 'Saldi conti'; protected static ?string $title = 'Saldi conti';
@ -59,6 +63,8 @@ public static function canAccess(): bool
public function mount(): void public function mount(): void
{ {
$this->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(); $this->mountInteractsWithTable();
} }
@ -76,7 +82,8 @@ protected function getTableQuery(): Builder
return SaldoConto::query() return SaldoConto::query()
->with('conto') ->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 public function table(Table $table): Table
@ -119,6 +126,13 @@ public function table(Table $table): Table
->alignEnd() ->alignEnd()
->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')), ->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') TextColumn::make('tipo_estratto')
->label('Tipo') ->label('Tipo')
->getStateUsing(fn(SaldoConto $record): string => $this->formatTipoEstrattoLabel($record->tipo_estratto)) ->getStateUsing(fn(SaldoConto $record): string => $this->formatTipoEstrattoLabel($record->tipo_estratto))
@ -168,6 +182,9 @@ public function table(Table $table): Table
->native(false) ->native(false)
->options($this->getTipoEstrattoOptions()) ->options($this->getTipoEstrattoOptions())
->default('estratto_conto'), ->default('estratto_conto'),
Toggle::make('is_partenza_contabile')
->label('Usa questo saldo come partenza contabile')
->default(false),
FileUpload::make('documento') FileUpload::make('documento')
->label('Allega estratto') ->label('Allega estratto')
->directory('tmp/banca') ->directory('tmp/banca')
@ -213,7 +230,7 @@ public function table(Table $table): Table
? $this->storeSaldoDocumento($stabileId, $conto, $data['documento'] ?? null, $user, $data) ? $this->storeSaldoDocumento($stabileId, $conto, $data['documento'] ?? null, $user, $data)
: null; : null;
SaldoConto::query()->create([ $saldo = SaldoConto::query()->create([
'stabile_id' => (int) $stabileId, 'stabile_id' => (int) $stabileId,
'conto_id' => $contoId, 'conto_id' => $contoId,
'iban' => $iban, 'iban' => $iban,
@ -221,12 +238,15 @@ public function table(Table $table): Table
'periodo_da' => $data['periodo_da'] ?? null, 'periodo_da' => $data['periodo_da'] ?? null,
'periodo_a' => $data['periodo_a'] ?? null, 'periodo_a' => $data['periodo_a'] ?? null,
'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : 0.0, '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), 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null),
'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null,
'documento_stabile_id' => $documento?->id, 'documento_stabile_id' => $documento?->id,
'created_by' => (int) ($user->id ?? 0) ?: null, 'created_by' => (int) ($user->id ?? 0) ?: null,
'updated_by' => (int) ($user->id ?? 0) ?: null, 'updated_by' => (int) ($user->id ?? 0) ?: null,
]); ]);
$this->syncContabilitaAnchorForSaldo($saldo);
}), }),
]) ])
->actions([ ->actions([
@ -242,6 +262,8 @@ public function table(Table $table): Table
->label('Tipo riferimento') ->label('Tipo riferimento')
->native(false) ->native(false)
->options($this->getTipoEstrattoOptions()), ->options($this->getTipoEstrattoOptions()),
Toggle::make('is_partenza_contabile')
->label('Usa questo saldo come partenza contabile'),
FileUpload::make('documento') FileUpload::make('documento')
->label('Sostituisci / aggiungi allegato') ->label('Sostituisci / aggiungi allegato')
->directory('tmp/banca') ->directory('tmp/banca')
@ -265,6 +287,7 @@ public function table(Table $table): Table
'periodo_da' => $record->periodo_da, 'periodo_da' => $record->periodo_da,
'periodo_a' => $record->periodo_a, 'periodo_a' => $record->periodo_a,
'saldo' => (float) $record->saldo, 'saldo' => (float) $record->saldo,
'is_partenza_contabile' => (bool) ($record->is_partenza_contabile ?? false),
'tipo_estratto' => $record->tipo_estratto, 'tipo_estratto' => $record->tipo_estratto,
'note' => $record->note, 'note' => $record->note,
]) ])
@ -279,12 +302,15 @@ public function table(Table $table): Table
'periodo_da' => $data['periodo_da'] ?? null, 'periodo_da' => $data['periodo_da'] ?? null,
'periodo_a' => $data['periodo_a'] ?? null, 'periodo_a' => $data['periodo_a'] ?? null,
'saldo' => isset($data['saldo']) ? (float) $data['saldo'] : (float) $record->saldo, '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), 'tipo_estratto' => $this->normalizeTipoEstrattoValue($data['tipo_estratto'] ?? null),
'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null, 'note' => isset($data['note']) && is_string($data['note']) ? trim((string) $data['note']) : null,
'documento_stabile_id' => $documento?->id ?: $record->documento_stabile_id, 'documento_stabile_id' => $documento?->id ?: $record->documento_stabile_id,
'updated_by' => $user instanceof User ? ((int) $user->id ?: null): null, 'updated_by' => $user instanceof User ? ((int) $user->id ?: null): null,
]); ]);
$record->save(); $record->save();
$this->syncContabilitaAnchorForSaldo($record);
}), }),
Action::make('delete') Action::make('delete')
@ -407,4 +433,17 @@ protected function storeSaldoDocumento(int $stabileId, DatiBancari $conto, mixed
'caricato_da' => (int) $user->id, '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]);
}
} }

View File

@ -2,9 +2,11 @@
namespace App\Filament\Pages\Strumenti; namespace App\Filament\Pages\Strumenti;
use App\Models\Documento; use App\Models\Documento;
use App\Models\DocumentoArchivioCartella;
use App\Models\MovimentoContabile; use App\Models\MovimentoContabile;
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\User; use App\Models\User;
use App\Services\Documenti\DocumentoArchivioVisibilityService;
use App\Services\Documenti\PdfTextExtractionService; use App\Services\Documenti\PdfTextExtractionService;
use App\Support\StabileContext; use App\Support\StabileContext;
use BackedEnum; use BackedEnum;
@ -53,6 +55,8 @@ class DocumentiArchivio extends Page implements HasTable
protected string $view = 'filament.pages.strumenti.documenti-archivio'; protected string $view = 'filament.pages.strumenti.documenti-archivio';
private ?DocumentoArchivioVisibilityService $visibilityService = null;
public static function canAccess(): bool public static function canAccess(): bool
{ {
$user = Auth::user(); $user = Auth::user();
@ -100,6 +104,15 @@ protected function getHeaderActions(): array
->action(function (array $data): void { ->action(function (array $data): void {
$this->createDocumentoDemoContrattoAscensori((int) $data['stabile_id'], (string) ($data['faldone'] ?? '')); $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'); return Documento::query()->whereRaw('1 = 0');
} }
$query = Documento::query(); return $this->getVisibilityService()
->scopeVisibleDocumenti(Documento::query()->with('cartellaArchivio'), $user);
$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);
} }
public function table(Table $table): Table public function table(Table $table): Table
@ -161,6 +155,10 @@ public function table(Table $table): Table
->searchable() ->searchable()
->limit(35) ->limit(35)
->toggleable(), ->toggleable(),
TextColumn::make('cartellaArchivio.nome')
->label('Cartella')
->toggleable()
->wrap(),
TextColumn::make('contenuto_ocr') TextColumn::make('contenuto_ocr')
->label('Testo (OCR)') ->label('Testo (OCR)')
->limit(50) ->limit(50)
@ -290,6 +288,44 @@ public function getArchivioFisicoTypesProperty(): array
return ['Faldone', 'Scatola', 'Cartella', 'Busta']; 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 public function getMnemonicCodeHintProperty(): ?string
{ {
$stabile = $this->resolveActiveStabile(); $stabile = $this->resolveActiveStabile();
@ -314,7 +350,13 @@ private function getDocumentoFormSchema(): array
->options(fn() => $this->getStabiliOptions()) ->options(fn() => $this->getStabiliOptions())
->default(fn() => $this->getDefaultStabileId()) ->default(fn() => $this->getDefaultStabileId())
->searchable() ->searchable()
->live()
->required(), ->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') Select::make('categoria')
->label('Categoria protocollo') ->label('Categoria protocollo')
->options($this->getCategorieOptions()) ->options($this->getCategorieOptions())
@ -352,6 +394,22 @@ private function getDocumentoFormSchema(): array
->placeholder('Es. CONTR-2024') ->placeholder('Es. CONTR-2024')
->maxLength(50) ->maxLength(50)
->nullable(), ->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') FileUpload::make('pdf')
->label('PDF') ->label('PDF')
->disk('local') ->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 private function getStabiliOptions(): array
{ {
$user = Auth::user(); $user = Auth::user();
@ -399,6 +506,68 @@ private function getDefaultStabileId(): ?int
return StabileContext::accessibleStabili($user)->first()?->id; 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 private function normalizeHubTab(string $tab): string
{ {
$allowed = ['archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi']; $allowed = ['archivio-digitale', 'template-drive', 'archivio-fisico', 'permessi'];
@ -447,6 +616,66 @@ private function getMovimentiOptions(int $stabileId): array
->all(); ->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 private function createDocumentoFromForm(array $data, bool $isDemo): void
{ {
$user = Auth::user(); $user = Auth::user();
@ -542,6 +771,7 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
'documentable_type' => null, 'documentable_type' => null,
'documentable_id' => null, 'documentable_id' => null,
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'cartella_archivio_id' => $this->resolveFolderIdForDocument($stabileId, $data['cartella_archivio_id'] ?? null),
'utente_id' => (int) $user->id, 'utente_id' => (int) $user->id,
'nome' => $titolo, 'nome' => $titolo,
'descrizione' => (string) ($data['descrizione'] ?? ''), 'descrizione' => (string) ($data['descrizione'] ?? ''),
@ -559,6 +789,9 @@ private function createDocumentoFromForm(array $data, bool $isDemo): void
'percorso_file' => $finalPath, 'percorso_file' => $finalPath,
'estensione' => 'pdf', 'estensione' => 'pdf',
'dimensione_file' => $size, '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(), 'data_upload' => now(),
'note' => $note, 'note' => $note,
'is_demo' => $isDemo, 'is_demo' => $isDemo,
@ -647,6 +880,7 @@ private function createDocumentoDemoContrattoAscensori(int $stabileId, string $f
'percorso_file' => $finalPath, 'percorso_file' => $finalPath,
'estensione' => 'pdf', 'estensione' => 'pdf',
'dimensione_file' => $size, 'dimensione_file' => $size,
'visibility_scope' => 'interno',
'data_upload' => now(), 'data_upload' => now(),
'note' => $note, 'note' => $note,
'is_demo' => true, 'is_demo' => true,
@ -726,4 +960,46 @@ private function mapCategoriaToTipologia(string $categoria): string
default => 'altro', 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();
}
} }

View File

@ -5,6 +5,7 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Documento; use App\Models\Documento;
use App\Models\User; use App\Models\User;
use App\Services\Documenti\DocumentoArchivioVisibilityService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -13,8 +14,13 @@
class DocumentiPrintController extends Controller 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 = []; $pathCandidates = [];
if (is_string($documento->percorso_file) && trim($documento->percorso_file) !== '') { if (is_string($documento->percorso_file) && trim($documento->percorso_file) !== '') {
$pathCandidates[] = 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); 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 = $request->query('format', '11354');
$format = in_array($format, ['11354', '99014'], true) ? $format : '11354'; $format = in_array($format, ['11354', '99014'], true) ? $format : '11354';

View File

@ -3,13 +3,21 @@
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\DocumentoStabile; use App\Models\DocumentoStabile;
use App\Models\User;
use App\Services\Documenti\DocumentoArchivioVisibilityService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class DocumentoStabileDownloadController extends Controller 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) : ''; $path = is_string($documentoStabile->percorso_file) ? trim((string) $documentoStabile->percorso_file) : '';
if ($path === '') { if ($path === '') {
abort(404); abort(404);

View File

@ -1,5 +1,4 @@
<?php <?php
namespace App\Livewire\Condomini; namespace App\Livewire\Condomini;
use App\Filament\Pages\Contabilita\CasseBancheMovimenti; use App\Filament\Pages\Contabilita\CasseBancheMovimenti;
@ -97,13 +96,13 @@ public function table(Table $table): Table
TextColumn::make('saldo_ufficiale') TextColumn::make('saldo_ufficiale')
->label('Saldo ufficiale') ->label('Saldo ufficiale')
->stateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record)) ->getStateUsing(fn(DatiBancari $record): ?float => $this->resolveLatestSaldoSnapshotValue($record))
->money('EUR') ->money('EUR')
->toggleable(), ->toggleable(),
TextColumn::make('ultimo_estratto') TextColumn::make('ultimo_estratto')
->label('Ultimo estratto') ->label('Ultimo estratto')
->stateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record)) ->getStateUsing(fn(DatiBancari $record): string => $this->resolveLatestSaldoSnapshotLabel($record))
->wrap() ->wrap()
->toggleable(), ->toggleable(),
@ -197,17 +196,13 @@ public function table(Table $table): Table
->icon('heroicon-o-receipt-percent') ->icon('heroicon-o-receipt-percent')
->url(function (DatiBancari $record): string { ->url(function (DatiBancari $record): string {
$base = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); $base = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
$iban = is_string($record->iban ?? null) ? trim((string) $record->iban) : ''; $contoId = (int) ($record->id ?? 0);
if ($iban === '') { if ($contoId <= 0) {
return $base; return $base;
} }
return $base . '?' . http_build_query([ return $base . '?' . http_build_query([
'tableFilters' => [ 'conto_id' => $contoId,
'conto' => [
'ibans' => [$iban],
],
],
]); ]);
}) })
->openUrlInNewTab(), ->openUrlInNewTab(),
@ -215,7 +210,7 @@ public function table(Table $table): Table
Action::make('saldi') Action::make('saldi')
->label('Saldi') ->label('Saldi')
->icon('heroicon-o-scale') ->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(), ->openUrlInNewTab(),
Action::make('contatto') Action::make('contatto')

View File

@ -38,6 +38,7 @@ class Documento extends Model
// Nuovi campi per sistema avanzato // Nuovi campi per sistema avanzato
'stabile_id', 'stabile_id',
'cartella_archivio_id',
'unita_id', 'unita_id',
'utente_id', 'utente_id',
'nome', 'nome',
@ -106,6 +107,11 @@ public function stabile(): BelongsTo
return $this->belongsTo(Stabile::class); return $this->belongsTo(Stabile::class);
} }
public function cartellaArchivio(): BelongsTo
{
return $this->belongsTo(DocumentoArchivioCartella::class, 'cartella_archivio_id');
}
public function unita(): BelongsTo public function unita(): BelongsTo
{ {
return $this->belongsTo(UnitaImmobiliare::class); return $this->belongsTo(UnitaImmobiliare::class);

View File

@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class DocumentoArchivioCartella extends Model
{
use HasFactory;
protected $table = 'documento_archivio_cartelle';
protected $fillable = [
'stabile_id',
'parent_id',
'nome',
'slug',
'codice',
'percorso_logico',
'visibility_scope',
'visibility_roles',
'visibility_groups',
'allowed_user_ids',
'note',
'sort_order',
'is_active',
'created_by',
'updated_by',
];
protected $casts = [
'visibility_roles' => '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);
}
}

View File

@ -0,0 +1,141 @@
<?php
namespace App\Services\Documenti;
use App\Models\Documento;
use App\Models\DocumentoArchivioCartella;
use App\Models\DocumentoStabile;
use App\Models\User;
use App\Support\StabileContext;
use Illuminate\Database\Eloquent\Builder;
class DocumentoArchivioVisibilityService
{
public function scopeVisibleDocumenti(Builder $query, User $user): Builder
{
$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);
}
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));
}
}

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('documento_archivio_cartelle')) {
Schema::create('documento_archivio_cartelle', function (Blueprint $table): void {
$table->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');
}
};

View File

@ -4,7 +4,7 @@
'archivio-digitale' => 'Archivio digitale', 'archivio-digitale' => 'Archivio digitale',
'template-drive' => 'Template Drive', 'template-drive' => 'Template Drive',
'archivio-fisico' => 'Archivio fisico', 'archivio-fisico' => 'Archivio fisico',
'permessi' => 'Permessi', 'permessi' => 'Permessi e cartelle',
]; ];
$stats = $this->hubStats; $stats = $this->hubStats;
@endphp @endphp
@ -108,15 +108,44 @@
@else @else
<x-filament::section> <x-filament::section>
<x-slot name="heading">Permessi e visibilita</x-slot> <x-slot name="heading">Permessi e visibilita</x-slot>
<x-slot name="description">Qui prepariamo la classificazione dei documenti per audience senza mischiare condomini, inquilini, fornitori e uso interno studio.</x-slot> <x-slot name="description">Qui prepariamo la classificazione dei documenti per audience e le cartelle visibili solo a ruoli, gruppi o utenti selezionati.</x-slot>
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.3fr)_minmax(280px,0.7fr)]">
<div>
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@foreach($this->visibleFolderRows as $folder)
<div class="rounded-xl border bg-white px-4 py-3">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">{{ $folder['nome'] }}</div>
<div class="mt-1 text-xs text-slate-500">{{ $folder['stabile'] }}</div>
</div>
<div class="rounded-full bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-700">{{ $folder['scope'] }}</div>
</div>
<div class="mt-2 text-xs text-slate-600">{{ $folder['path'] }}</div>
<div class="mt-3 text-xs text-slate-500">Documenti collegati: {{ $folder['documents_count'] }}</div>
<div class="mt-2 text-xs text-slate-500">Ruoli: {{ $folder['roles'] !== [] ? implode(', ', $folder['roles']) : 'nessun filtro' }}</div>
<div class="mt-1 text-xs text-slate-500">Gruppi: {{ $folder['groups'] !== [] ? implode(', ', $folder['groups']) : 'nessun filtro' }}</div>
<div class="mt-1 text-xs text-slate-500">Utenti: {{ $folder['users'] !== [] ? implode(', ', $folder['users']) : 'nessun utente specifico' }}</div>
</div>
@endforeach
@if(empty($this->visibleFolderRows))
<div class="rounded-xl border border-dashed bg-white px-4 py-6 text-sm text-slate-500 md:col-span-2 xl:col-span-3">Nessuna cartella archivio configurata per lo stabile attivo. Usa “Nuova cartella archivio” per iniziare a separare visibilita e struttura.</div>
@endif
</div>
</div>
<div class="space-y-3">
@foreach($this->audienceGroups as $group) @foreach($this->audienceGroups as $group)
<div class="rounded-xl border bg-white px-4 py-3"> <div class="rounded-xl border bg-white px-4 py-3">
<div class="text-sm font-semibold text-slate-900">{{ $group }}</div> <div class="text-sm font-semibold text-slate-900">{{ $group }}</div>
<div class="mt-1 text-xs text-slate-500">Audience candidata per visibilita documentale, export e portali dedicati.</div> <div class="mt-1 text-xs text-slate-500">Audience candidata per visibilita documentale, export e portali dedicati.</div>
</div> </div>
@endforeach @endforeach
<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-800">
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.
</div>
</div>
</div> </div>
</x-filament::section> </x-filament::section>
@endif @endif