diff --git a/.gitignore b/.gitignore index 5de87f6..e99ba81 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ /storage/*.key /storage/app/.app_key /storage/app/.db_seeded -/storage/app/public/territori-pdf/*.pdf +/storage/app/public/territori-pdf/*.* +/storage/app/public/territori-thumbnails/*.* +/storage/app/livewire-tmp/*.* /storage/logs/ /storage/framework/ /bootstrap/cache/ diff --git a/app/Http/Controllers/Settings/TerritoryPdfImportController.php b/app/Http/Controllers/Settings/TerritoryPdfImportController.php new file mode 100644 index 0000000..1010907 --- /dev/null +++ b/app/Http/Controllers/Settings/TerritoryPdfImportController.php @@ -0,0 +1,46 @@ +validate([ + 'pdfZip' => ['required', 'file', 'mimes:zip', 'max:256000'], + ]); + + try { + $importId = $dispatcher->dispatchUploadedZip($request->file('pdfZip'), auth()->id()); + } catch (RuntimeException $exception) { + if ($request->expectsJson()) { + return response()->json([ + 'message' => $exception->getMessage(), + 'errors' => ['pdfZip' => [$exception->getMessage()]], + ], 422); + } + + return back()->withErrors(['pdfZip' => $exception->getMessage()]); + } + + $redirectUrl = route('xml.exchange', ['pdf-import' => $importId]); + + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Import PDF avviato in background.', + 'import_id' => $importId, + 'redirect_url' => $redirectUrl, + ]); + } + + return redirect($redirectUrl) + ->with('success', 'Import PDF avviato in background. I log si aggiorneranno automaticamente.'); + } +} \ No newline at end of file diff --git a/app/Jobs/ImportTerritoryPdfFolder.php b/app/Jobs/ImportTerritoryPdfFolder.php new file mode 100644 index 0000000..9fe6547 --- /dev/null +++ b/app/Jobs/ImportTerritoryPdfFolder.php @@ -0,0 +1,240 @@ +markRunning($this->importId); + $stateService->appendLog($this->importId, 'Worker avviato. Inizio elaborazione dei PDF.'); + + $territoriMap = []; + foreach (Territorio::withTrashed()->get() as $territorio) { + $territoriMap[$this->normalizeTerritoryNumber($territorio->numero)] = $territorio; + } + + $actor = User::find($this->actorId); + $seenNumbers = []; + + try { + foreach ($this->files as $file) { + $originalName = $file['original_name'] ?? 'file-sconosciuto.pdf'; + $stateService->increment($this->importId, 'processed'); + + $territoryMatch = $this->resolveTerritoryFromFilename($originalName, $territoriMap); + + if ($territoryMatch === null) { + $stateService->increment($this->importId, 'skipped'); + $stateService->appendLog($this->importId, '[SKIP] ' . $originalName . ' - nessun numero territorio riconosciuto nel nome file.'); + $stateService->addIssue($this->importId, [ + 'file' => $originalName, + 'type' => 'no-match', + 'message' => 'Nessun numero territorio riconosciuto nel nome file.', + ]); + continue; + } + + if (($territoryMatch['type'] ?? 'single') === 'ambiguous') { + $stateService->increment($this->importId, 'skipped'); + $stateService->appendLog($this->importId, '[SKIP] ' . $originalName . ' - nome ambiguo, possibili territori: ' . implode(', ', $territoryMatch['matched_numbers']) . '.'); + $stateService->addIssue($this->importId, [ + 'file' => $originalName, + 'type' => 'ambiguous', + 'message' => 'Nome ambiguo.', + 'matched_numbers' => $territoryMatch['matched_numbers'], + ]); + continue; + } + + $normalizedNumber = $territoryMatch['normalized_number']; + $matchedNumber = $territoryMatch['matched_number']; + $territorio = $territoryMatch['territorio']; + + if (isset($seenNumbers[$normalizedNumber])) { + $stateService->increment($this->importId, 'skipped'); + $stateService->appendLog($this->importId, '[SKIP] ' . $originalName . ' - territorio ' . $matchedNumber . ' presente piu volte nella stessa importazione.'); + $stateService->addIssue($this->importId, [ + 'file' => $originalName, + 'type' => 'duplicate-in-batch', + 'message' => 'Territorio presente piu volte nella stessa importazione.', + 'matched_numbers' => [$matchedNumber], + ]); + continue; + } + + $seenNumbers[$normalizedNumber] = true; + + $sourcePath = Storage::disk('local')->path($file['stored_path']); + + if (! is_file($sourcePath)) { + $stateService->increment($this->importId, 'errors'); + $stateService->appendLog($this->importId, '[ERR] ' . $originalName . ' - file temporaneo non trovato.'); + $stateService->addIssue($this->importId, [ + 'file' => $originalName, + 'type' => 'missing-temp-file', + 'message' => 'File temporaneo non trovato.', + 'matched_numbers' => [$matchedNumber], + ]); + continue; + } + + try { + if ($territorio->pdf_path) { + Storage::disk('public')->delete($territorio->pdf_path); + } + + if ($territorio->thumbnail_path) { + $thumbnailService->delete($territorio->thumbnail_path); + } + + $storedFilename = Str::slug('territorio-' . $territorio->numero) . '.pdf'; + $publicPath = 'territori-pdf/' . $storedFilename; + Storage::disk('public')->put($publicPath, file_get_contents($sourcePath)); + $thumbnailPath = $thumbnailService->generate($publicPath); + + $territorio->update([ + 'pdf_path' => $publicPath, + 'thumbnail_path' => $thumbnailPath, + ]); + + if ($actor) { + activity()->causedBy($actor) + ->performedOn($territorio) + ->withProperties([ + 'numero' => $territorio->numero, + 'pdf' => $originalName, + 'bulk_import' => true, + ]) + ->log('bulk_uploaded_pdf'); + } + + $stateService->increment($this->importId, 'updated'); + $stateService->appendLog( + $this->importId, + '[OK] ' . $originalName . ' - aggiornato territorio ' . $territorio->numero . ($thumbnailPath ? ' con thumbnail generata.' : ' ma la thumbnail non e stata generata.') + ); + } catch (\Throwable $exception) { + $stateService->increment($this->importId, 'errors'); + $stateService->appendLog($this->importId, '[ERR] ' . $originalName . ' - ' . $exception->getMessage()); + $stateService->addIssue($this->importId, [ + 'file' => $originalName, + 'type' => 'processing-error', + 'message' => $exception->getMessage(), + 'matched_numbers' => [$matchedNumber], + ]); + } + } + + $stateService->appendLog($this->importId, 'Import completato.'); + $stateService->markCompleted($this->importId); + } catch (\Throwable $exception) { + $stateService->increment($this->importId, 'errors'); + $stateService->appendLog($this->importId, '[ERR] Errore fatale del job: ' . $exception->getMessage()); + $stateService->markFailed($this->importId); + + throw $exception; + } finally { + Storage::disk('local')->deleteDirectory('bulk-territori-imports/' . $this->importId); + } + } + + protected function resolveTerritoryFromFilename(string $filename, array $territoriMap): ?array + { + if ($territoriMap === []) { + return null; + } + + $basename = pathinfo($filename, PATHINFO_FILENAME); + $normalizedBasename = mb_strtoupper($basename); + $normalizedBasename = preg_replace('/[^A-Z0-9]+/u', ' ', $normalizedBasename); + $normalizedBasename = trim(preg_replace('/\s+/', ' ', $normalizedBasename)); + + $territoryKeys = array_keys($territoriMap); + usort($territoryKeys, function (string $left, string $right) { + $lengthComparison = mb_strlen($right) <=> mb_strlen($left); + + if ($lengthComparison !== 0) { + return $lengthComparison; + } + + return strnatcasecmp($left, $right); + }); + + $matches = []; + + foreach ($territoryKeys as $normalizedNumber) { + if (! $this->filenameContainsTerritoryNumber($normalizedBasename, $normalizedNumber)) { + continue; + } + + $matches[] = [ + 'normalized_number' => $normalizedNumber, + 'matched_number' => $territoriMap[$normalizedNumber]->numero, + 'territorio' => $territoriMap[$normalizedNumber], + ]; + } + + if ($matches === []) { + return null; + } + + if (count($matches) > 1) { + return [ + 'type' => 'ambiguous', + 'matched_numbers' => array_values(array_map(fn(array $match) => $match['matched_number'], $matches)), + ]; + } + + return $matches[0]; + } + + protected function filenameContainsTerritoryNumber(string $normalizedBasename, string $normalizedNumber): bool + { + $escapedNumber = preg_quote($normalizedNumber, '/'); + + if (preg_match('/^\d+$/', $normalizedNumber)) { + $escapedNumber = '0*' . preg_quote(ltrim($normalizedNumber, '0') ?: '0', '/'); + } + + return (bool) preg_match('/(^| )' . $escapedNumber . '(?= |$)/', $normalizedBasename); + } + + protected function normalizeTerritoryNumber(string $number): string + { + $normalized = preg_replace('/\s+/', ' ', trim(mb_strtoupper($number))); + + if ($normalized !== '' && preg_match('/^\d+$/', $normalized)) { + return ltrim($normalized, '0') ?: '0'; + } + + return $normalized; + } +} \ No newline at end of file diff --git a/app/Livewire/Home.php b/app/Livewire/Home.php index 657492f..dd2cac9 100644 --- a/app/Livewire/Home.php +++ b/app/Livewire/Home.php @@ -49,15 +49,26 @@ class Home extends Component } // Quick lists - $daAssegnare = Territorio::daAssegnare() + $territoriDaAssegnare = Territorio::inReparto() ->with('zona', 'tipologia', 'ultimaAssegnazione') - ->take(10) - ->get(); + ->get() + ->sort(function (Territorio $left, Territorio $right) { + $priorityComparison = (int) $right->is_prioritario <=> (int) $left->is_prioritario; - $prioritari = Territorio::prioritari() - ->with('zona', 'tipologia', 'ultimaAssegnazione') + if ($priorityComparison !== 0) { + return $priorityComparison; + } + + $giacenzaComparison = $right->giorni_giacenza <=> $left->giorni_giacenza; + + if ($giacenzaComparison !== 0) { + return $giacenzaComparison; + } + + return strnatcasecmp((string) $left->numero, (string) $right->numero); + }) ->take(10) - ->get(); + ->values(); $daRientrare = Territorio::daRientrare() ->with(['zona', 'assegnazioneCorrente.proclamatore']) @@ -73,8 +84,7 @@ class Home extends Component 'territoriPercorsi' => $territoriPercorsi, 'mediaPercorrenzaMensile' => $mediaPercorrenzaMensile, 'campagnaStats' => $campagnaStats, - 'daAssegnare' => $daAssegnare, - 'prioritari' => $prioritari, + 'territoriDaAssegnare' => $territoriDaAssegnare, 'daRientrare' => $daRientrare, ]); } diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php index 3d33462..f8241c9 100644 --- a/app/Livewire/Settings/XmlExchange.php +++ b/app/Livewire/Settings/XmlExchange.php @@ -2,6 +2,7 @@ namespace App\Livewire\Settings; +use App\Jobs\ImportTerritoryPdfFolder; use App\Models\AnnoTeocratico; use App\Models\Assegnazione; use App\Models\Campagna; @@ -11,9 +12,12 @@ use App\Models\Territorio; use App\Models\Tipologia; use App\Models\User; use App\Models\Zona; +use App\Services\TerritorioPdfImportDispatcher; +use App\Services\TerritorioPdfImportState; use Barryvdh\DomPDF\Facade\Pdf; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; use Livewire\Component; use Livewire\WithFileUploads; @@ -25,6 +29,22 @@ class XmlExchange extends Component public $xmlImport; public array $importStats = []; public array $importIssues = []; + public array $pdfFolder = []; + public array $pdfImportLogs = []; + public array $pdfImportStats = []; + public array $pdfImportIssues = []; + public ?string $currentPdfImportId = null; + public string $pdfImportStatus = 'idle'; + public string $pdfImportLogText = ''; + + public function mount(): void + { + $this->currentPdfImportId = request()->query('pdf-import'); + + if ($this->currentPdfImportId) { + $this->refreshPdfImportStatus(); + } + } public function convertLegacySqlToXml() { @@ -257,6 +277,70 @@ class XmlExchange extends Component session()->flash('success', $message); } + public function importTerritoryPdfFolder(): void + { + $this->validate([ + 'pdfFolder' => ['required', 'array', 'min:1'], + 'pdfFolder.*' => ['file', 'mimes:pdf', 'max:10240'], + ]); + + $importId = (string) Str::uuid(); + $storedFiles = []; + + foreach ($this->pdfFolder as $index => $file) { + $originalName = $file->getClientOriginalName(); + $safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME)); + $extension = strtolower($file->getClientOriginalExtension() ?: 'pdf'); + $storedPath = $file->storeAs( + 'bulk-territori-imports/' . $importId, + str_pad((string) $index, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.' . $extension, + 'local' + ); + + $storedFiles[] = [ + 'original_name' => $originalName, + 'stored_path' => $storedPath, + ]; + } + + $this->pdfFolder = []; + + } + + public function refreshPdfImportStatus(): void + { + if (! $this->currentPdfImportId) { + return; + } + + $state = app(TerritorioPdfImportState::class)->get($this->currentPdfImportId); + + if (! $state) { + return; + } + + $this->pdfImportStatus = $state['status'] ?? 'idle'; + $this->pdfImportStats = $state['stats'] ?? []; + $this->pdfImportLogs = $state['logs'] ?? []; + $this->pdfImportIssues = $state['issues'] ?? []; + $this->pdfImportLogText = implode(PHP_EOL, $this->pdfImportLogs); + } + + protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog): void + { + $state = app(TerritorioPdfImportDispatcher::class) + ->dispatchStoredFiles($importId, $storedFiles, auth()->id(), $initialLog); + + $this->currentPdfImportId = $importId; + $this->pdfImportStatus = $state['status'] ?? 'queued'; + $this->pdfImportStats = $state['stats'] ?? []; + $this->pdfImportLogs = $state['logs'] ?? []; + $this->pdfImportIssues = $state['issues'] ?? []; + $this->refreshPdfImportStatus(); + + session()->flash('success', 'Import PDF avviato in background. I log si aggiorneranno automaticamente.'); + } + public function downloadImportLogPdf() { if (empty($this->importStats)) { diff --git a/app/Livewire/Territori/TerritorioIndex.php b/app/Livewire/Territori/TerritorioIndex.php index 1c2ace1..be892a6 100644 --- a/app/Livewire/Territori/TerritorioIndex.php +++ b/app/Livewire/Territori/TerritorioIndex.php @@ -2,6 +2,7 @@ namespace App\Livewire\Territori; +use Illuminate\Pagination\LengthAwarePaginator; use Livewire\Component; use Livewire\WithPagination; use App\Models\Territorio; @@ -16,6 +17,9 @@ class TerritorioIndex extends Component public string $filterZona = ''; public string $filterTipologia = ''; public string $filterStato = ''; + public string $filterPriorita = ''; + public string $filterPdf = ''; + public string $filterContenuti = ''; public string $sortField = 'numero'; public string $sortDirection = 'asc'; @@ -24,6 +28,9 @@ class TerritorioIndex extends Component 'filterZona' => ['except' => ''], 'filterTipologia' => ['except' => ''], 'filterStato' => ['except' => ''], + 'filterPriorita' => ['except' => ''], + 'filterPdf' => ['except' => ''], + 'filterContenuti' => ['except' => ''], ]; public function updatingSearch() @@ -31,6 +38,36 @@ class TerritorioIndex extends Component $this->resetPage(); } + public function updatingFilterZona() + { + $this->resetPage(); + } + + public function updatingFilterTipologia() + { + $this->resetPage(); + } + + public function updatingFilterStato() + { + $this->resetPage(); + } + + public function updatingFilterPriorita() + { + $this->resetPage(); + } + + public function updatingFilterPdf() + { + $this->resetPage(); + } + + public function updatingFilterContenuti() + { + $this->resetPage(); + } + public function sortBy(string $field) { if ($this->sortField === $field) { @@ -41,6 +78,22 @@ class TerritorioIndex extends Component } } + public function clearFilters(): void + { + $this->reset([ + 'search', + 'filterZona', + 'filterTipologia', + 'filterStato', + 'filterPriorita', + 'filterPdf', + 'filterContenuti', + ]); + $this->sortField = 'numero'; + $this->sortDirection = 'asc'; + $this->resetPage(); + } + public function toggleActive(Territorio $territorio) { $territorio->update(['attivo' => !$territorio->attivo]); @@ -73,7 +126,14 @@ class TerritorioIndex extends Component $query = Territorio::with(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore']); if ($this->search) { - $query->where('numero', 'like', "%{$this->search}%"); + $search = $this->search; + $query->where(function ($subQuery) use ($search) { + $subQuery->where('numero', 'like', "%{$search}%") + ->orWhere('note', 'like', "%{$search}%") + ->orWhere('confini', 'like', "%{$search}%") + ->orWhereHas('zona', fn($zonaQuery) => $zonaQuery->where('nome', 'like', "%{$search}%")) + ->orWhereHas('tipologia', fn($tipologiaQuery) => $tipologiaQuery->where('nome', 'like', "%{$search}%")); + }); } if ($this->filterZona) { @@ -90,16 +150,104 @@ class TerritorioIndex extends Component 'assegnato' => $query->assegnato(), 'da_rientrare' => $query->daRientrare(), 'inattivo' => $query->where('attivo', false), + 'prioritari' => $query->inReparto(), default => null, }; } - $query->orderBy($this->sortField, $this->sortDirection); + if ($this->filterPdf) { + match ($this->filterPdf) { + 'con_pdf' => $query->whereNotNull('pdf_path'), + 'senza_pdf' => $query->whereNull('pdf_path'), + 'con_thumbnail' => $query->whereNotNull('thumbnail_path'), + 'senza_thumbnail' => $query->whereNull('thumbnail_path'), + default => null, + }; + } + + if ($this->filterContenuti) { + match ($this->filterContenuti) { + 'con_note' => $query->whereNotNull('note')->where('note', '!=', ''), + 'senza_note' => $query->where(function ($subQuery) { + $subQuery->whereNull('note')->orWhere('note', ''); + }), + 'con_confini' => $query->whereNotNull('confini')->where('confini', '!=', ''), + 'senza_confini' => $query->where(function ($subQuery) { + $subQuery->whereNull('confini')->orWhere('confini', ''); + }), + default => null, + }; + } + + $territori = $query->get(); + + if ($this->filterStato === 'prioritari') { + $territori = $territori->filter(fn(Territorio $territorio) => $territorio->is_prioritario)->values(); + } + + if ($this->filterPriorita) { + $territori = $territori->filter(function (Territorio $territorio) { + return match ($this->filterPriorita) { + 'prioritari' => $territorio->is_prioritario, + 'manuali' => $territorio->prioritario, + 'automatici' => $territorio->is_prioritario && !$territorio->prioritario, + 'non_prioritari' => !$territorio->is_prioritario, + default => true, + }; + })->values(); + } + + if ($this->usesPriorityOrdering()) { + $territori = $territori->sort(function (Territorio $left, Territorio $right) { + $priorityComparison = (int) $right->is_prioritario <=> (int) $left->is_prioritario; + + if ($priorityComparison !== 0) { + return $priorityComparison; + } + + $giacenzaComparison = $right->giorni_giacenza <=> $left->giorni_giacenza; + + if ($giacenzaComparison !== 0) { + return $giacenzaComparison; + } + + return strnatcasecmp((string) $left->numero, (string) $right->numero); + })->values(); + } else { + $territori = $territori->sort(function (Territorio $left, Territorio $right) { + $result = strnatcasecmp((string) data_get($left, $this->sortField), (string) data_get($right, $this->sortField)); + + return $this->sortDirection === 'asc' ? $result : -$result; + })->values(); + } + + $perPage = 20; + $page = $this->getPage(); + $items = $territori->slice(($page - 1) * $perPage, $perPage)->values(); + $paginatedTerritori = new LengthAwarePaginator( + $items, + $territori->count(), + $perPage, + $page, + ['path' => request()->url(), 'query' => request()->query()] + ); return view('livewire.territori.territorio-index', [ - 'territori' => $query->paginate(20), + 'territori' => $paginatedTerritori, 'zone' => Zona::attive()->get(), 'tipologie' => Tipologia::attive()->get(), + 'usesPriorityOrdering' => $this->usesPriorityOrdering(), ]); } + + protected function usesPriorityOrdering(): bool + { + return $this->sortField === 'numero' + && $this->sortDirection === 'asc' + && ( + in_array($this->filterStato, ['in_reparto', 'prioritari'], true) + || $this->filterPriorita !== '' + ); + } + } diff --git a/app/Services/TerritorioPdfImportDispatcher.php b/app/Services/TerritorioPdfImportDispatcher.php new file mode 100644 index 0000000..3813822 --- /dev/null +++ b/app/Services/TerritorioPdfImportDispatcher.php @@ -0,0 +1,94 @@ +stateService->initialize($importId, count($storedFiles)); + $this->stateService->appendLog($importId, $initialLog); + + ImportTerritoryPdfFolder::dispatch($importId, $storedFiles, $actorId); + + return $this->stateService->get($importId) ?? $state; + } + + public function dispatchUploadedZip(UploadedFile $zipFile, ?int $actorId): string + { + $importId = (string) Str::uuid(); + $zipStoredPath = $zipFile->storeAs( + 'bulk-territori-imports/' . $importId, + 'archivio-' . $importId . '.zip', + 'local' + ); + + $zipAbsolutePath = storage_path('app/' . $zipStoredPath); + $zip = new ZipArchive(); + + if ($zip->open($zipAbsolutePath) !== true) { + throw new RuntimeException('Impossibile aprire il file ZIP.'); + } + + $storedFiles = []; + $entryIndex = 0; + + try { + for ($index = 0; $index < $zip->numFiles; $index++) { + $entryName = $zip->getNameIndex($index); + + if (! $entryName || str_ends_with($entryName, '/')) { + continue; + } + + if (strtolower(pathinfo($entryName, PATHINFO_EXTENSION)) !== 'pdf') { + continue; + } + + $content = $zip->getFromIndex($index); + if ($content === false) { + continue; + } + + $originalName = basename($entryName); + $safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME)); + $storedPath = 'bulk-territori-imports/' . $importId . '/zip-' . str_pad((string) $entryIndex, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.pdf'; + + file_put_contents(storage_path('app/' . $storedPath), $content); + + $storedFiles[] = [ + 'original_name' => $originalName, + 'stored_path' => $storedPath, + ]; + + $entryIndex++; + } + } finally { + $zip->close(); + } + + if ($storedFiles === []) { + throw new RuntimeException('Lo ZIP non contiene file PDF validi.'); + } + + $this->dispatchStoredFiles( + $importId, + $storedFiles, + $actorId, + 'Archivio ZIP ricevuto: ' . count($storedFiles) . ' PDF estratti e messi in coda per l\'elaborazione.' + ); + + return $importId; + } +} \ No newline at end of file diff --git a/app/Services/TerritorioPdfImportState.php b/app/Services/TerritorioPdfImportState.php new file mode 100644 index 0000000..3d2f009 --- /dev/null +++ b/app/Services/TerritorioPdfImportState.php @@ -0,0 +1,122 @@ + $importId, + 'status' => 'queued', + 'stats' => [ + 'total' => $totalFiles, + 'processed' => 0, + 'updated' => 0, + 'skipped' => 0, + 'errors' => 0, + ], + 'logs' => [ + 'Import creato. In attesa del worker di coda.', + ], + 'issues' => [], + 'started_at' => null, + 'finished_at' => null, + ]; + + $this->put($importId, $state); + + return $state; + } + + public function get(string $importId): ?array + { + return Cache::get($this->key($importId)); + } + + public function put(string $importId, array $state): void + { + Cache::put($this->key($importId), $state, $this->ttlSeconds); + } + + public function update(string $importId, callable $callback): ?array + { + $state = $this->get($importId); + + if (! $state) { + return null; + } + + $updatedState = $callback($state) ?? $state; + $this->put($importId, $updatedState); + + return $updatedState; + } + + public function appendLog(string $importId, string $message): void + { + $this->update($importId, function (array $state) use ($message) { + $timestamp = now()->format('H:i:s'); + $state['logs'][] = '[' . $timestamp . '] ' . $message; + + return $state; + }); + } + + public function increment(string $importId, string $metric, int $amount = 1): void + { + $this->update($importId, function (array $state) use ($metric, $amount) { + $state['stats'][$metric] = ($state['stats'][$metric] ?? 0) + $amount; + + return $state; + }); + } + + public function addIssue(string $importId, array $issue): void + { + $this->update($importId, function (array $state) use ($issue) { + $state['issues'][] = $issue; + + return $state; + }); + } + + public function markRunning(string $importId): void + { + $this->update($importId, function (array $state) { + $state['status'] = 'running'; + $state['started_at'] = now()->toDateTimeString(); + + return $state; + }); + } + + public function markCompleted(string $importId): void + { + $this->update($importId, function (array $state) { + $state['status'] = 'completed'; + $state['finished_at'] = now()->toDateTimeString(); + + return $state; + }); + } + + public function markFailed(string $importId): void + { + $this->update($importId, function (array $state) { + $state['status'] = 'failed'; + $state['finished_at'] = now()->toDateTimeString(); + + return $state; + }); + } + + protected function key(string $importId): string + { + return 'territori-pdf-import:' . $importId; + } +} \ No newline at end of file diff --git a/config/livewire.php b/config/livewire.php new file mode 100644 index 0000000..46e4cd1 --- /dev/null +++ b/config/livewire.php @@ -0,0 +1,47 @@ + 'App\\Livewire', + + 'view_path' => resource_path('views/livewire'), + + 'layout' => 'components.layouts.app', + + 'lazy_placeholder' => null, + + 'temporary_file_upload' => [ + 'disk' => null, + 'rules' => ['required', 'file', 'max:256000'], + 'directory' => null, + 'middleware' => null, + 'preview_mimes' => [ + 'png', 'gif', 'bmp', 'svg', 'wav', 'mp4', + 'mov', 'avi', 'wmv', 'mp3', 'm4a', + 'jpg', 'jpeg', 'mpga', 'webp', 'wma', + ], + 'max_upload_time' => 15, + 'cleanup' => true, + ], + + 'render_on_redirect' => false, + + 'legacy_model_binding' => false, + + 'inject_assets' => true, + + 'navigate' => [ + 'show_progress_bar' => true, + 'progress_bar_color' => '#2299dd', + ], + + 'inject_morph_markers' => true, + + 'smart_wire_keys' => false, + + 'pagination_theme' => 'tailwind', + + 'release_token' => env('APP_VERSION'), + + 'inject_assets_after_styles' => false, +]; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ab235e4..b4968a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,6 +47,33 @@ services: app: condition: service_healthy + queue-worker: + build: + context: . + dockerfile: docker/php/Dockerfile + container_name: termanager2_queue + restart: unless-stopped + user: "1000:1000" + working_dir: /var/www/html + volumes: + - ./:/var/www/html + networks: + - termanager2 + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + command: ["php", "artisan", "queue:work", "redis", "--queue=default", "--sleep=1", "--tries=1", "--timeout=0"] + environment: + - PHP_OPCACHE_VALIDATE_TIMESTAMPS=1 + - SEED_DEV_DATA=${SEED_DEV_DATA:-false} + - RUN_DB_SEED_ON_FIRST_START=${RUN_DB_SEED_ON_FIRST_START:-true} + - ENSURE_INITIAL_ADMIN_ON_EMPTY_DB=${ENSURE_INITIAL_ADMIN_ON_EMPTY_DB:-true} + - INITIAL_ADMIN_NAME=${INITIAL_ADMIN_NAME:-} + - INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-} + - INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:-} + mariadb: image: mariadb:11 container_name: termanager2_db diff --git a/docker/php/php.ini b/docker/php/php.ini index 9a5fb52..b7f7253 100644 --- a/docker/php/php.ini +++ b/docker/php/php.ini @@ -1,6 +1,6 @@ [PHP] -upload_max_filesize = 64M -post_max_size = 64M +upload_max_filesize = 256M +post_max_size = 256M memory_limit = 256M max_execution_time = 120 max_input_vars = 3000 diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index c1bb68e..5d149f5 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -140,7 +140,7 @@ - XML Exchange + Import @endcan diff --git a/resources/views/livewire/assegnazioni/assegna.blade.php b/resources/views/livewire/assegnazioni/assegna.blade.php index 4c3ed12..c6e7460 100644 --- a/resources/views/livewire/assegnazioni/assegna.blade.php +++ b/resources/views/livewire/assegnazioni/assegna.blade.php @@ -5,7 +5,7 @@ ← Torna ai territori -