netgescon-day0/app/Filament/Pages/Strumenti/DocumentiArchivio.php

620 lines
22 KiB
PHP

<?php
namespace App\Filament\Pages\Strumenti;
use Carbon\Carbon;
use App\Models\Documento;
use App\Models\MovimentoContabile;
use App\Models\Stabile;
use App\Models\User;
use App\Services\Documenti\PdfTextExtractionService;
use App\Support\StabileContext;
use BackedEnum;
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\TextInput;
use Filament\Forms\Components\Textarea;
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 UnitEnum;
class DocumentiArchivio extends Page implements HasTable
{
use InteractsWithTable;
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.gescon.table';
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$this->mountInteractsWithTable();
}
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'] ?? ''));
}),
];
}
protected function getTableQuery(): Builder
{
$user = Auth::user();
if (! $user instanceof User) {
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);
}
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('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('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)',
];
}
private function getDocumentoFormSchema(): array
{
return [
Select::make('stabile_id')
->label('Stabile')
->options(fn () => $this->getStabiliOptions())
->default(fn () => $this->getDefaultStabileId())
->searchable()
->required(),
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(),
FileUpload::make('pdf')
->label('PDF')
->disk('local')
->acceptedFileTypes(['application/pdf'])
->required(),
];
}
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 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 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,
'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,
'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,
'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',
};
}
}