++ fix: Rientro assegnazione e territorio
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\TerritorioPdfImportDispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
class TerritoryPdfImportController extends Controller
|
||||
{
|
||||
public function storeZip(Request $request, TerritorioPdfImportDispatcher $dispatcher): JsonResponse|RedirectResponse
|
||||
{
|
||||
$request->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.');
|
||||
}
|
||||
}
|
||||
240
app/Jobs/ImportTerritoryPdfFolder.php
Normal file
240
app/Jobs/ImportTerritoryPdfFolder.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Territorio;
|
||||
use App\Models\User;
|
||||
use App\Services\TerritorioPdfImportState;
|
||||
use App\Services\TerritorioThumbnailService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImportTerritoryPdfFolder implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public int $tries = 1;
|
||||
public int $timeout = 0;
|
||||
|
||||
public function __construct(
|
||||
public string $importId,
|
||||
public array $files,
|
||||
public int $actorId,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TerritorioPdfImportState $stateService, TerritorioThumbnailService $thumbnailService): void
|
||||
{
|
||||
$stateService->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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 !== ''
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
94
app/Services/TerritorioPdfImportDispatcher.php
Normal file
94
app/Services/TerritorioPdfImportDispatcher.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\ImportTerritoryPdfFolder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
class TerritorioPdfImportDispatcher
|
||||
{
|
||||
public function __construct(
|
||||
protected TerritorioPdfImportState $stateService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function dispatchStoredFiles(string $importId, array $storedFiles, ?int $actorId, string $initialLog): array
|
||||
{
|
||||
$state = $this->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;
|
||||
}
|
||||
}
|
||||
122
app/Services/TerritorioPdfImportState.php
Normal file
122
app/Services/TerritorioPdfImportState.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class TerritorioPdfImportState
|
||||
{
|
||||
protected int $ttlSeconds = 86400;
|
||||
|
||||
public function initialize(string $importId, int $totalFiles): array
|
||||
{
|
||||
$state = [
|
||||
'id' => $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user