1013 lines
38 KiB
PHP
1013 lines
38 KiB
PHP
<?php
|
|
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;
|
|
use Carbon\Carbon;
|
|
use Dompdf\Dompdf;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\DatePicker;
|
|
use Filament\Forms\Components\FileUpload;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\BadgeColumn;
|
|
use Filament\Tables\Columns\IconColumn;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Attributes\Url;
|
|
use UnitEnum;
|
|
|
|
class DocumentiArchivio extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
|
|
#[Url( as : 'tab')]
|
|
public string $hubTab = 'archivio-digitale';
|
|
|
|
protected static ?string $navigationLabel = 'Documenti (PDF)';
|
|
|
|
protected static ?string $title = 'Documenti (PDF)';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-document-text';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Strumenti';
|
|
|
|
protected static ?int $navigationSort = 61;
|
|
|
|
protected static ?string $slug = 'strumenti/documenti';
|
|
|
|
protected string $view = 'filament.pages.strumenti.documenti-archivio';
|
|
|
|
private ?DocumentoArchivioVisibilityService $visibilityService = null;
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->hubTab = $this->normalizeHubTab($this->hubTab);
|
|
$this->mountInteractsWithTable();
|
|
}
|
|
|
|
public function selectHubTab(string $tab): void
|
|
{
|
|
$this->hubTab = $this->normalizeHubTab($tab);
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('registra_documento')
|
|
->label('Registra documento (PDF)')
|
|
->icon('heroicon-o-plus')
|
|
->form($this->getDocumentoFormSchema())
|
|
->action(function (array $data): void {
|
|
$this->createDocumentoFromForm($data, false);
|
|
}),
|
|
|
|
Action::make('crea_demo_contratto_ascensori')
|
|
->label('Crea demo: Contratto Ascensori')
|
|
->icon('heroicon-o-sparkles')
|
|
->form([
|
|
Select::make('stabile_id')
|
|
->label('Stabile')
|
|
->options(fn() => $this->getStabiliOptions())
|
|
->default(fn() => $this->getDefaultStabileId())
|
|
->searchable()
|
|
->required(),
|
|
TextInput::make('faldone')
|
|
->label('Faldone (opzionale)')
|
|
->placeholder('Es. CONTR-2024')
|
|
->maxLength(50),
|
|
])
|
|
->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);
|
|
}),
|
|
];
|
|
}
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return Documento::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
return $this->getVisibilityService()
|
|
->scopeVisibleDocumenti(Documento::query()->with('cartellaArchivio'), $user);
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->striped()
|
|
->defaultSort('created_at', 'desc')
|
|
->columns([
|
|
TextColumn::make('numero_protocollo')
|
|
->label('Protocollo')
|
|
->copyable()
|
|
->searchable(),
|
|
TextColumn::make('nome')
|
|
->label('Titolo')
|
|
->searchable()
|
|
->limit(50),
|
|
BadgeColumn::make('tipo_documento')
|
|
->label('Categoria')
|
|
->toggleable()
|
|
->formatStateUsing(fn($state, Documento $record) => $state ?: ($record->tipologia ?: '—')),
|
|
TextColumn::make('descrizione')
|
|
->label('Descrizione')
|
|
->limit(50)
|
|
->toggleable(isToggledHiddenByDefault: true)
|
|
->searchable(),
|
|
TextColumn::make('fornitore')
|
|
->label('Fornitore/Ente')
|
|
->searchable()
|
|
->limit(35)
|
|
->toggleable(),
|
|
TextColumn::make('cartellaArchivio.nome')
|
|
->label('Cartella')
|
|
->toggleable()
|
|
->wrap(),
|
|
TextColumn::make('contenuto_ocr')
|
|
->label('Testo (OCR)')
|
|
->limit(50)
|
|
->toggleable(isToggledHiddenByDefault: true)
|
|
->searchable(),
|
|
TextColumn::make('data_documento')
|
|
->label('Data')
|
|
->date('d-m-Y')
|
|
->toggleable(),
|
|
TextColumn::make('data_scadenza')
|
|
->label('Scadenza')
|
|
->date('d-m-Y')
|
|
->toggleable(),
|
|
TextColumn::make('importo_collegato')
|
|
->label('Importo')
|
|
->money('EUR')
|
|
->toggleable(),
|
|
IconColumn::make('archiviato')
|
|
->label('Arch.')
|
|
->boolean()
|
|
->toggleable(),
|
|
])
|
|
->filters([
|
|
SelectFilter::make('tipo_documento')
|
|
->label('Categoria')
|
|
->options($this->getCategorieOptions()),
|
|
])
|
|
->actions([
|
|
Action::make('visualizza_pdf')
|
|
->label('Vedi PDF')
|
|
->icon('heroicon-o-eye')
|
|
->visible(fn(Documento $record) => $this->hasPdf($record))
|
|
->modalHeading(fn(Documento $record): string => trim((string) ($record->nome ?: 'Documento PDF')))
|
|
->modalWidth('7xl')
|
|
->modalContent(function (Documento $record) {
|
|
$openUrl = route('filament.documenti.download', $record);
|
|
|
|
return view('filament.modals.pdf-viewer', [
|
|
'url' => $openUrl . '?inline=1',
|
|
'openUrl' => $openUrl,
|
|
'title' => trim((string) ($record->nome ?: 'Documento PDF')),
|
|
]);
|
|
})
|
|
->modalSubmitAction(false)
|
|
->modalCancelActionLabel('Chiudi'),
|
|
Action::make('scarica_pdf')
|
|
->label('PDF')
|
|
->icon('heroicon-o-arrow-down-tray')
|
|
->url(fn(Documento $record) => route('filament.documenti.download', $record), true)
|
|
->visible(fn(Documento $record) => $this->hasPdf($record)),
|
|
Action::make('etichetta_11354')
|
|
->label('Etichetta 11354')
|
|
->icon('heroicon-o-tag')
|
|
->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=11354&dymo_fix=0&dymo_rot=0&dymo_dx=0&dymo_dy=3&autoprint=1', false),
|
|
Action::make('etichetta_11354_driver_verticale')
|
|
->label('Etichetta 11354 (driver verticale)')
|
|
->icon('heroicon-o-tag')
|
|
->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=11354&dymo_fix=1&dymo_rot=-90&dymo_dx=0&dymo_dy=0&autoprint=1', false),
|
|
Action::make('etichetta_99014')
|
|
->label('Etichetta 99014')
|
|
->icon('heroicon-o-tag')
|
|
->url(fn(Documento $record) => route('filament.documenti.etichetta', $record) . '?format=99014&autoprint=1', false),
|
|
]);
|
|
}
|
|
|
|
private function getCategorieOptions(): array
|
|
{
|
|
return [
|
|
'contratto' => 'Contratti (CONTR)',
|
|
'assicurazione' => 'Assicurazioni (ASSIC)',
|
|
'certificato' => 'Certificati (CERT)',
|
|
'preventivo' => 'Preventivi (PREV)',
|
|
'fattura' => 'Fatture (FATT)',
|
|
'verbale' => 'Verbali (VERB)',
|
|
'altro' => 'Altri (DOC)',
|
|
];
|
|
}
|
|
|
|
public function getHubStatsProperty(): array
|
|
{
|
|
$query = $this->getTableQuery();
|
|
|
|
return [
|
|
'documenti' => (clone $query)->count(),
|
|
'archiviati' => (clone $query)->where('archiviato', true)->count(),
|
|
'in_scadenza' => (clone $query)->whereNotNull('data_scadenza')->whereDate('data_scadenza', '<=', now()->addDays(30)->toDateString())->count(),
|
|
'template_drive' => count($this->getDriveTemplateFoldersProperty()),
|
|
];
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
public function getDriveTemplateFoldersProperty(): array
|
|
{
|
|
$items = config('netgescon.google.drive_template_folders', []);
|
|
$yearRoot = trim((string) config('netgescon.google.drive_template_year_root', ''));
|
|
$yearModel = trim((string) config('netgescon.google.drive_template_year_model', ''));
|
|
|
|
$folders = is_array($items)
|
|
? array_values(array_filter(array_map('strval', $items), fn(string $item) : bool => trim($item) !== ''))
|
|
: [];
|
|
|
|
if ($yearRoot !== '' && $yearModel !== '') {
|
|
foreach ($folders as $folder) {
|
|
$folders[] = $yearRoot . '/' . $yearModel . '/' . $folder;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($folders));
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
public function getAudienceGroupsProperty(): array
|
|
{
|
|
return [
|
|
'Condomini',
|
|
'Inquilini',
|
|
'Fornitori',
|
|
'Collaboratori interni',
|
|
'Amministratore',
|
|
'Studio completo',
|
|
];
|
|
}
|
|
|
|
/** @return array<int, string> */
|
|
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();
|
|
if (! $stabile instanceof Stabile) {
|
|
return null;
|
|
}
|
|
|
|
$label = Str::upper((string) ($stabile->denominazione ?? 'STABILE'));
|
|
$letters = preg_replace('/[^A-Z]/', '', Str::ascii($label)) ?: 'STB';
|
|
$digits = preg_replace('/\D/', '', $label);
|
|
$prefix = str_pad(substr($letters, 0, 3), 3, 'X');
|
|
$suffix = str_pad(substr($digits !== '' ? $digits : (string) $stabile->id, -5), 5, '0', STR_PAD_LEFT);
|
|
|
|
return $prefix . $suffix;
|
|
}
|
|
|
|
private function getDocumentoFormSchema(): array
|
|
{
|
|
return [
|
|
Select::make('stabile_id')
|
|
->label('Stabile')
|
|
->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())
|
|
->required(),
|
|
TextInput::make('titolo')
|
|
->label('Titolo')
|
|
->required()
|
|
->maxLength(255),
|
|
Textarea::make('descrizione')
|
|
->label('Descrizione')
|
|
->rows(4),
|
|
TextInput::make('fornitore')
|
|
->label('Fornitore / Ente / Compagnia')
|
|
->maxLength(255),
|
|
DatePicker::make('data_documento')
|
|
->label('Data documento')
|
|
->default(now())
|
|
->required(),
|
|
DatePicker::make('data_scadenza')
|
|
->label('Data scadenza')
|
|
->nullable(),
|
|
TextInput::make('importo')
|
|
->label('Importo (opzionale)')
|
|
->numeric()
|
|
->step(0.01)
|
|
->minValue(0)
|
|
->nullable(),
|
|
Select::make('movimento_contabile_id')
|
|
->label('Movimento contabile (opzionale)')
|
|
->options(fn(callable $get) => $this->getMovimentiOptions((int) ($get('stabile_id') ?: 0)))
|
|
->searchable()
|
|
->nullable(),
|
|
TextInput::make('faldone')
|
|
->label('Faldone (archiviazione fisica)')
|
|
->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),
|
|
Select::make('allowed_user_ids')
|
|
->label('Utenti specifici ammessi')
|
|
->options($this->getCollaboratorUserOptions())
|
|
->multiple()
|
|
->native(false)
|
|
->searchable(),
|
|
FileUpload::make('pdf')
|
|
->label('PDF')
|
|
->disk('local')
|
|
->acceptedFileTypes(['application/pdf'])
|
|
->required(),
|
|
];
|
|
}
|
|
|
|
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();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
if ($user->hasAnyRole(['super-admin', 'admin'])) {
|
|
return Stabile::query()
|
|
->orderBy('denominazione')
|
|
->limit(500)
|
|
->get(['id', 'denominazione'])
|
|
->mapWithKeys(fn(Stabile $s) => [(string) $s->id => (($s->denominazione ?: ('Stabile #' . $s->id)))])
|
|
->all();
|
|
}
|
|
|
|
$allowed = StabileContext::accessibleStabili($user);
|
|
return $allowed
|
|
->sortBy(fn(Stabile $s) => $s->denominazione)
|
|
->take(500)
|
|
->mapWithKeys(fn(Stabile $s) => [(string) $s->id => (($s->denominazione ?: ('Stabile #' . $s->id)))])
|
|
->all();
|
|
}
|
|
|
|
private function getDefaultStabileId(): ?int
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$activeId = StabileContext::getActiveStabileId($user);
|
|
if (is_int($activeId) && $activeId > 0) {
|
|
return $activeId;
|
|
}
|
|
|
|
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'];
|
|
|
|
return in_array($tab, $allowed, true) ? $tab : 'archivio-digitale';
|
|
}
|
|
|
|
private function resolveActiveStabile(): ?Stabile
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
return StabileContext::getActiveStabile($user);
|
|
}
|
|
|
|
private function getMovimentiOptions(int $stabileId): array
|
|
{
|
|
if ($stabileId <= 0) {
|
|
return [];
|
|
}
|
|
if (! class_exists(MovimentoContabile::class)) {
|
|
return [];
|
|
}
|
|
|
|
return MovimentoContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('data_movimento')
|
|
->orderByDesc('id')
|
|
->limit(200)
|
|
->get(['id', 'descrizione', 'data_movimento', 'importo'])
|
|
->mapWithKeys(function (MovimentoContabile $m) {
|
|
$label = '#' . $m->id;
|
|
if ($m->data_movimento) {
|
|
$label .= ' ' . $m->data_movimento->format('d-m-Y');
|
|
}
|
|
if (is_string($m->descrizione) && trim($m->descrizione) !== '') {
|
|
$label .= ' — ' . trim($m->descrizione);
|
|
}
|
|
if ($m->importo !== null) {
|
|
$label .= ' — €' . number_format((float) $m->importo, 2, ',', '.');
|
|
}
|
|
return [(string) $m->id => $label];
|
|
})
|
|
->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();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$stabileId = (int) ($data['stabile_id'] ?? 0);
|
|
if ($stabileId <= 0) {
|
|
Notification::make()->title('Stabile obbligatorio')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$categoria = (string) ($data['categoria'] ?? 'altro');
|
|
$titolo = (string) ($data['titolo'] ?? '');
|
|
if (trim($titolo) === '') {
|
|
Notification::make()->title('Titolo obbligatorio')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$dataDocumento = $data['data_documento'] ?? null;
|
|
$year = null;
|
|
if ($dataDocumento instanceof \DateTimeInterface) {
|
|
$year = (int) $dataDocumento->format('Y');
|
|
} elseif (is_string($dataDocumento) && trim($dataDocumento) !== '') {
|
|
$raw = trim($dataDocumento);
|
|
foreach (['Y-m-d', 'd-m-Y', 'd/m/Y', 'Y/m/d'] as $fmt) {
|
|
try {
|
|
$dt = Carbon::createFromFormat($fmt, $raw);
|
|
if ($dt) {
|
|
$year = (int) $dt->format('Y');
|
|
break;
|
|
}
|
|
} catch (\Throwable) {
|
|
// try next
|
|
}
|
|
}
|
|
if ($year === null) {
|
|
try {
|
|
$year = (int) Carbon::parse($raw)->format('Y');
|
|
} catch (\Throwable) {
|
|
$year = null;
|
|
}
|
|
}
|
|
}
|
|
if (! is_int($year) || $year < 2000 || $year > 2100) {
|
|
$year = (int) now()->format('Y');
|
|
}
|
|
|
|
$protocollo = $this->generateProtocollo($categoria, $year);
|
|
|
|
$storedPath = (string) ($data['pdf'] ?? '');
|
|
if ($storedPath === '') {
|
|
Notification::make()->title('PDF obbligatorio')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
// Rinomina il file su storage in modo ordinato
|
|
$safeTitle = Str::slug($titolo);
|
|
$finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year;
|
|
$finalName = $protocollo . '_' . ($safeTitle !== '' ? $safeTitle : 'documento') . '.pdf';
|
|
$finalPath = $finalDir . '/' . $finalName;
|
|
|
|
try {
|
|
if (Storage::disk('local')->exists($storedPath)) {
|
|
$bytes = Storage::disk('local')->get($storedPath);
|
|
Storage::disk('local')->put($finalPath, $bytes);
|
|
Storage::disk('local')->delete($storedPath);
|
|
}
|
|
} catch (\Throwable) {
|
|
// best-effort
|
|
}
|
|
|
|
$size = null;
|
|
try {
|
|
if (Storage::disk('local')->exists($finalPath)) {
|
|
$size = Storage::disk('local')->size($finalPath);
|
|
}
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
$note = '';
|
|
$faldone = trim((string) ($data['faldone'] ?? ''));
|
|
if ($faldone !== '') {
|
|
$note = 'Faldone: ' . $faldone;
|
|
}
|
|
|
|
$tipologia = $this->mapCategoriaToTipologia($categoria);
|
|
|
|
$doc = Documento::query()->create([
|
|
'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'] ?? ''),
|
|
'fornitore' => (string) ($data['fornitore'] ?? ''),
|
|
'tipologia' => $tipologia,
|
|
'tipo_documento' => $categoria,
|
|
'data_documento' => $dataDocumento,
|
|
'data_scadenza' => $data['data_scadenza'] ?? null,
|
|
'importo_collegato' => $data['importo'] ?? null,
|
|
'movimento_contabile_id' => $data['movimento_contabile_id'] ?? null,
|
|
'numero_protocollo' => $protocollo,
|
|
'nome_file' => $finalName,
|
|
'mime_type' => 'application/pdf',
|
|
'path_file' => $finalPath,
|
|
'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'] ?? []),
|
|
'allowed_user_ids' => $this->normalizeIntArray($data['allowed_user_ids'] ?? []),
|
|
'data_upload' => now(),
|
|
'note' => $note,
|
|
'is_demo' => $isDemo,
|
|
]);
|
|
|
|
// Estrai testo per ricerca (best-effort)
|
|
try {
|
|
app(PdfTextExtractionService::class)->extractAndStore($doc, false);
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Documento registrato')
|
|
->body('Protocollo: ' . $protocollo)
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
private function createDocumentoDemoContrattoAscensori(int $stabileId, string $faldone): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$year = 2024;
|
|
$protocollo = $this->generateProtocollo('contratto', $year);
|
|
|
|
$titolo = 'Contratto Manutenzione Ascensori 2024';
|
|
$descrizione = 'Contratto annuale per manutenzione impianti ascensore con Acme Elevators Srl.';
|
|
|
|
$html = '<h1>' . e($titolo) . '</h1>';
|
|
$html .= '<p><strong>Protocollo:</strong> ' . e($protocollo) . '</p>';
|
|
$html .= '<p>' . e($descrizione) . '</p>';
|
|
$html .= '<hr>';
|
|
$html .= '<p><strong>Fornitore:</strong> Acme Elevators Srl</p>';
|
|
$html .= '<p><strong>Dal:</strong> 01/01/2024 <strong>Al:</strong> 31/12/2024</p>';
|
|
$html .= '<p><strong>Totale annuo:</strong> € 2.400,00</p>';
|
|
$html .= '<p><strong>Modalità pagamento:</strong> Trimestrale</p>';
|
|
$html .= '<p><strong>Data stipula:</strong> 15/12/2023</p>';
|
|
|
|
$dompdf = new Dompdf();
|
|
$dompdf->loadHtml('<html><body style="font-family: DejaVu Sans, Arial, sans-serif;">' . $html . '</body></html>');
|
|
$dompdf->setPaper('A4');
|
|
$dompdf->render();
|
|
$pdfBytes = $dompdf->output();
|
|
|
|
$finalDir = 'documenti/stabili/ID-' . $stabileId . '/' . $year;
|
|
$finalName = $protocollo . '_contratto_ascensori_2024.pdf';
|
|
$finalPath = $finalDir . '/' . $finalName;
|
|
|
|
Storage::disk('local')->put($finalPath, $pdfBytes);
|
|
|
|
$size = null;
|
|
try {
|
|
$size = Storage::disk('local')->size($finalPath);
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
$note = '';
|
|
$faldone = trim($faldone);
|
|
if ($faldone !== '') {
|
|
$note = 'Faldone: ' . $faldone;
|
|
} else {
|
|
$note = 'Faldone: CONTR-2024';
|
|
}
|
|
|
|
$doc = Documento::query()->create([
|
|
'stabile_id' => $stabileId,
|
|
'utente_id' => (int) $user->id,
|
|
'nome' => $titolo,
|
|
'descrizione' => $descrizione,
|
|
'fornitore' => 'Acme Elevators Srl',
|
|
'tipologia' => 'contratto',
|
|
'tipo_documento' => 'contratto',
|
|
'data_documento' => '2023-12-15',
|
|
'data_scadenza' => '2024-12-31',
|
|
'importo_collegato' => 2400.00,
|
|
'numero_protocollo' => $protocollo,
|
|
'nome_file' => $finalName,
|
|
'mime_type' => 'application/pdf',
|
|
'path_file' => $finalPath,
|
|
'percorso_file' => $finalPath,
|
|
'estensione' => 'pdf',
|
|
'dimensione_file' => $size,
|
|
'visibility_scope' => 'interno',
|
|
'data_upload' => now(),
|
|
'note' => $note,
|
|
'is_demo' => true,
|
|
]);
|
|
|
|
try {
|
|
app(PdfTextExtractionService::class)->extractAndStore($doc, true);
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
Notification::make()
|
|
->title('Demo creato')
|
|
->body('Protocollo: ' . $protocollo)
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
private function generateProtocollo(string $categoria, int $year): string
|
|
{
|
|
$categoria = strtolower(trim($categoria));
|
|
$prefissi = [
|
|
'contratto' => 'CONTR',
|
|
'assicurazione' => 'ASSIC',
|
|
'certificato' => 'CERT',
|
|
'preventivo' => 'PREV',
|
|
// Importante: i protocolli fattura devono essere per-anno.
|
|
// Usiamo FT come prefisso compatto e compatibile con l'archiviazione.
|
|
'fattura' => 'FT',
|
|
'verbale' => 'VERB',
|
|
'altro' => 'DOC',
|
|
];
|
|
|
|
$pref = $prefissi[$categoria] ?? 'DOC';
|
|
|
|
$ultimo = Documento::query()
|
|
->whereNotNull('numero_protocollo')
|
|
->where(function ($q) use ($pref, $year) {
|
|
$q->where('numero_protocollo', 'like', $pref . '-' . $year . '-%');
|
|
// Compatibilità: alcuni documenti legacy potrebbero avere prefisso FATT.
|
|
if ($pref === 'FT') {
|
|
$q->orWhere('numero_protocollo', 'like', 'FATT-' . $year . '-%');
|
|
}
|
|
})
|
|
->orderBy('numero_protocollo', 'desc')
|
|
->first(['numero_protocollo']);
|
|
|
|
$progressivo = 1;
|
|
if ($ultimo && is_string($ultimo->numero_protocollo)) {
|
|
$parts = explode('-', $ultimo->numero_protocollo);
|
|
$last = (int) (end($parts) ?: 0);
|
|
if ($last > 0) {
|
|
$progressivo = $last + 1;
|
|
}
|
|
}
|
|
|
|
return sprintf('%s-%d-%03d', $pref, $year, $progressivo);
|
|
}
|
|
|
|
private function hasPdf(Documento $doc): bool
|
|
{
|
|
$p = (string) ($doc->percorso_file ?: ($doc->path_file ?: ''));
|
|
return $p !== '' && str_ends_with(strtolower($p), '.pdf');
|
|
}
|
|
|
|
private function mapCategoriaToTipologia(string $categoria): string
|
|
{
|
|
$categoria = strtolower(trim($categoria));
|
|
|
|
// `documenti.tipologia` è un enum: manteniamoci sui valori previsti.
|
|
return match ($categoria) {
|
|
'contratto' => 'contratto',
|
|
'certificato' => 'certificato',
|
|
'preventivo' => 'preventivo',
|
|
'fattura' => 'fattura',
|
|
'verbale' => 'verbale',
|
|
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();
|
|
}
|
|
}
|