359 lines
22 KiB
PHP
359 lines
22 KiB
PHP
<div class="space-y-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">Import</h1>
|
|
<p class="text-sm text-gray-500 mt-1">Centro importazioni: PDF territori, conversione legacy SQL, import XML ed export XML.</p>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4"
|
|
x-data="{
|
|
uploadLog: '',
|
|
uploadProgress: 0,
|
|
uploading: false,
|
|
zipUploading: false,
|
|
zipProgress: 0,
|
|
selectedFiles: 0,
|
|
selectedZip: '',
|
|
append(message) {
|
|
this.uploadLog = this.uploadLog ? this.uploadLog + '\n' + message : message;
|
|
},
|
|
submitZip(event) {
|
|
const form = event.target;
|
|
const formData = new FormData(form);
|
|
|
|
this.uploadLog = '';
|
|
this.zipUploading = true;
|
|
this.zipProgress = 0;
|
|
this.append('Upload ZIP diretto al server avviato...');
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', form.action);
|
|
xhr.setRequestHeader('Accept', 'application/json');
|
|
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
xhr.upload.addEventListener('progress', (uploadEvent) => {
|
|
if (!uploadEvent.lengthComputable) {
|
|
return;
|
|
}
|
|
|
|
this.zipProgress = Math.round((uploadEvent.loaded / uploadEvent.total) * 100);
|
|
});
|
|
|
|
xhr.addEventListener('load', () => {
|
|
this.zipUploading = false;
|
|
|
|
let payload = {};
|
|
|
|
try {
|
|
payload = JSON.parse(xhr.responseText || '{}');
|
|
} catch (error) {
|
|
payload = {};
|
|
}
|
|
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
this.zipProgress = 100;
|
|
this.append('Archivio ricevuto. Reindirizzamento alla console import...');
|
|
window.location = payload.redirect_url || window.location.href;
|
|
return;
|
|
}
|
|
|
|
const message = payload.message || (payload.errors && payload.errors.pdfZip ? payload.errors.pdfZip[0] : 'Errore durante il caricamento dello ZIP.');
|
|
this.append(message);
|
|
});
|
|
|
|
xhr.addEventListener('error', () => {
|
|
this.zipUploading = false;
|
|
this.append('Errore di rete durante il caricamento dello ZIP.');
|
|
});
|
|
|
|
xhr.send(formData);
|
|
}
|
|
}"
|
|
x-on:livewire-upload-start="if ($event.detail.id === 'pdfFolder') { uploading = true; uploadProgress = 0; append('Upload cartella avviato...'); }"
|
|
x-on:livewire-upload-progress="if ($event.detail.id === 'pdfFolder') { uploadProgress = $event.detail.progress; append('Upload Livewire in corso: ' + $event.detail.progress + '%'); }"
|
|
x-on:livewire-upload-finish="if ($event.detail.id === 'pdfFolder') { uploading = false; uploadProgress = 100; append('Upload completato. Avvio preparazione import lato server...'); }"
|
|
x-on:livewire-upload-error="if ($event.detail.id === 'pdfFolder') { uploading = false; append('Errore durante l\'upload temporaneo dei file.'); }">
|
|
<h2 class="text-lg font-semibold text-gray-900">Import PDF territori</h2>
|
|
<p class="text-xs text-gray-500">Puoi importare una cartella di PDF oppure, meglio per archivi grandi, un file ZIP contenente i PDF. Il nome file puo variare: basta che contenga il numero di un territorio gia presente nell'app. I PDF verranno associati ai territori esistenti e verra generata anche la thumbnail.</p>
|
|
|
|
<form wire:submit.prevent="importTerritoryPdfFolder" class="space-y-4">
|
|
<div>
|
|
<input wire:model="pdfFolder"
|
|
x-on:change="selectedFiles = $event.target.files.length; uploadLog = ''; if (selectedFiles > 0) { append('Cartella selezionata: ' + selectedFiles + ' file.'); append('In attesa dell\'upload temporaneo Livewire...'); }"
|
|
type="file"
|
|
multiple
|
|
webkitdirectory
|
|
directory
|
|
accept=".pdf,application/pdf"
|
|
class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
|
@error('pdfFolder') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
@error('pdfFolder.*') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
|
|
<div x-show="selectedFiles > 0 || uploading" x-cloak class="rounded-lg border border-indigo-100 bg-indigo-50 px-3 py-3">
|
|
<div class="flex items-center justify-between gap-3 text-xs text-indigo-800">
|
|
<span x-text="uploading ? 'Upload file in corso...' : 'Upload file pronto'"></span>
|
|
<span x-text="selectedFiles > 0 ? selectedFiles + ' file selezionati' : ''"></span>
|
|
</div>
|
|
<div class="mt-2 h-2 overflow-hidden rounded-full bg-indigo-100">
|
|
<div class="h-full rounded-full bg-indigo-600 transition-all duration-300" :style="'width:' + uploadProgress + '%' "></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition border border-indigo-700"
|
|
style="display:inline-flex;align-items:center;justify-content:center;min-height:40px;background:#4f46e5;color:#fff;border:1px solid #3730a3;border-radius:10px;padding:10px 14px;"
|
|
wire:loading.attr="disabled"
|
|
@disabled(empty($pdfFolder))
|
|
>
|
|
Importa PDF territori
|
|
</button>
|
|
<div wire:loading wire:target="importTerritoryPdfFolder" class="text-sm text-indigo-700">Preparazione import in corso...</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="border-t border-gray-200 pt-4">
|
|
<h3 class="text-sm font-semibold text-gray-900">Import da archivio ZIP</h3>
|
|
<p class="mt-1 text-xs text-gray-500">Consigliato per grandi volumi: carichi un solo file e il server estrae automaticamente tutti i PDF.</p>
|
|
|
|
<form action="{{ route('imports.territori.pdf-zip') }}" method="POST" enctype="multipart/form-data" class="mt-3 space-y-4" @submit.prevent="submitZip">
|
|
@csrf
|
|
<div>
|
|
<input name="pdfZip"
|
|
x-on:change="selectedZip = $event.target.files[0] ? $event.target.files[0].name : ''; uploadLog = ''; zipProgress = 0; if (selectedZip) { append('Archivio ZIP selezionato: ' + selectedZip); append('Pronto per upload diretto al server.'); }"
|
|
type="file"
|
|
accept=".zip,application/zip"
|
|
class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-emerald-50 file:text-emerald-700 hover:file:bg-emerald-100">
|
|
@error('pdfZip') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
|
|
<div x-show="selectedZip || zipUploading" x-cloak class="rounded-lg border border-emerald-100 bg-emerald-50 px-3 py-3">
|
|
<div class="flex items-center justify-between gap-3 text-xs text-emerald-800">
|
|
<span x-text="zipUploading ? 'Upload ZIP in corso...' : 'ZIP pronto per l\'invio' "></span>
|
|
<span x-text="selectedZip"></span>
|
|
</div>
|
|
<div class="mt-2 h-2 overflow-hidden rounded-full bg-emerald-100">
|
|
<div class="h-full rounded-full bg-emerald-600 transition-all duration-300" :style="'width:' + zipProgress + '%' "></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg hover:bg-emerald-700 transition border border-emerald-700"
|
|
style="display:inline-flex;align-items:center;justify-content:center;min-height:40px;background:#059669;color:#fff;border:1px solid #065f46;border-radius:10px;padding:10px 14px;"
|
|
x-bind:disabled="!selectedZip || zipUploading"
|
|
>
|
|
Importa ZIP PDF
|
|
</button>
|
|
<div x-show="zipUploading" x-cloak class="text-sm text-emerald-700">Caricamento ZIP diretto in corso...</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div x-show="uploadLog || uploading || zipUploading || selectedZip" x-cloak>
|
|
<div class="mb-2 text-sm font-medium text-gray-800">Console upload</div>
|
|
<textarea readonly rows="8" x-bind:value="uploadLog" class="block w-full rounded-lg border border-gray-300 bg-gray-950 px-3 py-3 font-mono text-xs leading-5 text-gray-100 focus:border-indigo-500 focus:ring-indigo-500"></textarea>
|
|
</div>
|
|
|
|
@if($currentPdfImportId && (!empty($pdfImportStats) || !empty($pdfImportLogs)))
|
|
<div class="rounded-xl border border-indigo-200 bg-indigo-50/40 p-4" @if(in_array($pdfImportStatus, ['queued', 'running'], true)) wire:poll.1000ms="refreshPdfImportStatus" @endif>
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-800">Stato import PDF</div>
|
|
<div class="mt-1 text-xs text-gray-500">ID import: {{ $currentPdfImportId }}</div>
|
|
</div>
|
|
<span class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold
|
|
{{ match($pdfImportStatus) {
|
|
'queued' => 'bg-amber-100 text-amber-800',
|
|
'running' => 'bg-blue-100 text-blue-800',
|
|
'completed' => 'bg-green-100 text-green-800',
|
|
'failed' => 'bg-red-100 text-red-800',
|
|
default => 'bg-gray-100 text-gray-700',
|
|
} }}">
|
|
{{ match($pdfImportStatus) {
|
|
'queued' => 'In coda',
|
|
'running' => 'In esecuzione',
|
|
'completed' => 'Completato',
|
|
'failed' => 'Fallito',
|
|
default => 'Inattivo',
|
|
} }}
|
|
</span>
|
|
</div>
|
|
|
|
@if(!empty($pdfImportStats))
|
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm sm:grid-cols-4">
|
|
<div class="rounded-lg bg-white px-3 py-2 border border-gray-200">
|
|
<div class="text-xs text-gray-500">Processati</div>
|
|
<div class="font-semibold text-gray-900">{{ $pdfImportStats['processed'] ?? 0 }} / {{ $pdfImportStats['total'] ?? 0 }}</div>
|
|
</div>
|
|
<div class="rounded-lg bg-green-50 px-3 py-2 border border-green-100">
|
|
<div class="text-xs text-green-700">Aggiornati</div>
|
|
<div class="font-semibold text-green-900">{{ $pdfImportStats['updated'] ?? 0 }}</div>
|
|
</div>
|
|
<div class="rounded-lg bg-amber-50 px-3 py-2 border border-amber-100">
|
|
<div class="text-xs text-amber-700">Saltati</div>
|
|
<div class="font-semibold text-amber-900">{{ $pdfImportStats['skipped'] ?? 0 }}</div>
|
|
</div>
|
|
<div class="rounded-lg bg-red-50 px-3 py-2 border border-red-100">
|
|
<div class="text-xs text-red-700">Errori</div>
|
|
<div class="font-semibold text-red-900">{{ $pdfImportStats['errors'] ?? 0 }}</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="mt-4">
|
|
<div class="mb-2 flex items-center justify-between gap-3">
|
|
<div class="text-sm font-medium text-gray-800">Log import PDF</div>
|
|
<button type="button" wire:click="refreshPdfImportStatus" class="inline-flex items-center justify-center rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-white transition">
|
|
Aggiorna log
|
|
</button>
|
|
</div>
|
|
<textarea readonly rows="12" class="block w-full rounded-lg border border-gray-300 bg-gray-950 px-3 py-3 font-mono text-xs leading-5 text-gray-100 focus:border-indigo-500 focus:ring-indigo-500 sm:rows-14">{{ $pdfImportLogText }}</textarea>
|
|
</div>
|
|
|
|
@if(!empty($pdfImportIssues))
|
|
<div class="mt-4">
|
|
<div class="mb-2 text-sm font-medium text-gray-800">Riepilogo file non associati o problematici</div>
|
|
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">
|
|
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500">File</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500">Motivo</th>
|
|
<th class="px-3 py-2 text-left text-xs font-medium uppercase text-gray-500">Territori rilevati</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
@foreach($pdfImportIssues as $issue)
|
|
<tr>
|
|
<td class="px-3 py-2 text-gray-900">{{ $issue['file'] ?? '-' }}</td>
|
|
<td class="px-3 py-2 text-gray-600">{{ $issue['message'] ?? '-' }}</td>
|
|
<td class="px-3 py-2 text-gray-600">{{ !empty($issue['matched_numbers']) ? implode(', ', $issue['matched_numbers']) : '-' }}</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
|
<h2 class="text-lg font-semibold text-gray-900">Conversione dump SQL legacy</h2>
|
|
<p class="text-xs text-gray-500">Carica il file SQL (es. dump-termanager-202604071526.sql) e genera un XML compatibile con l'import dell'app.</p>
|
|
|
|
<form action="{{ route('xml.convert-sql') }}" method="POST" enctype="multipart/form-data">
|
|
@csrf
|
|
<div>
|
|
<input name="sqlDump" type="file" accept=".sql,.txt,.SQL,.TXT" class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
|
@error('sqlDump') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-4">
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition border border-indigo-700"
|
|
style="display:inline-flex;align-items:center;justify-content:center;min-height:40px;background:#4f46e5;color:#fff;border:1px solid #3730a3;border-radius:10px;padding:10px 14px;"
|
|
>
|
|
Converti in XML
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
|
<h2 class="text-lg font-semibold text-gray-900">Import XML nell'app</h2>
|
|
<p class="text-xs text-gray-500">Importa un XML nel formato TerManager2. L'import sostituisce i dati gestionali (zone, tipologie, proclamatori, territori, anni, campagne, assegnazioni e impostazioni).</p>
|
|
|
|
<form action="{{ route('xml.import-xml') }}" method="POST" enctype="multipart/form-data" onsubmit="return confirm('Confermi l\'import XML? I dati gestionali correnti verranno sostituiti.');">
|
|
@csrf
|
|
<div>
|
|
<input name="xmlImport" type="file" accept=".xml,.txt,.XML,.TXT" class="block w-full text-sm text-gray-600 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100">
|
|
@error('xmlImport') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-4">
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-lg hover:bg-amber-700 transition border border-amber-700"
|
|
style="display:inline-flex;align-items:center;justify-content:center;min-height:40px;background:#d97706;color:#fff;border:1px solid #92400e;border-radius:10px;padding:10px 14px;"
|
|
>
|
|
Importa XML
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
@if(!empty($importStats))
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-3">
|
|
<h2 class="text-lg font-semibold text-gray-900">Log importazione</h2>
|
|
<div class="text-sm text-gray-700" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px;">
|
|
<div>Zone importate: <strong>{{ $importStats['zone_importate'] ?? 0 }}</strong></div>
|
|
<div>Tipologie importate: <strong>{{ $importStats['tipologie_importate'] ?? 0 }}</strong></div>
|
|
<div>Proclamatori importati: <strong>{{ $importStats['proclamatori_importati'] ?? 0 }}</strong></div>
|
|
<div>Territori importati: <strong>{{ $importStats['territori_importati'] ?? 0 }}</strong></div>
|
|
<div>Anni importati: <strong>{{ $importStats['anni_importati'] ?? 0 }}</strong></div>
|
|
<div>Campagne importate: <strong>{{ $importStats['campagne_importate'] ?? 0 }}</strong></div>
|
|
<div>Assegnazioni importate: <strong>{{ $importStats['assegnazioni_importate'] ?? 0 }}</strong></div>
|
|
<div>Territori duplicati saltati: <strong>{{ $importStats['duplicate_territori'] ?? 0 }}</strong></div>
|
|
<div>Assegnazioni saltate: <strong>{{ $importStats['assegnazioni_saltate'] ?? 0 }}</strong></div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<button
|
|
wire:click="downloadImportLogPdf"
|
|
type="button"
|
|
style="display:inline-flex;align-items:center;gap:6px;background:#1d4ed8;color:#fff;border:1px solid #1e3a8a;border-radius:8px;padding:8px 14px;font-size:13px;cursor:pointer;"
|
|
>
|
|
<svg style="width:15px;height:15px;" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17v3a1 1 0 001 1h16a1 1 0 001-1v-3"/></svg>
|
|
Scarica log PDF
|
|
</button>
|
|
</div>
|
|
|
|
@if(!empty($importIssues))
|
|
<div class="mt-2" style="max-height:260px;overflow:auto;border:1px solid #e5e7eb;border-radius:10px;">
|
|
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
|
<thead style="background:#f9fafb;position:sticky;top:0;">
|
|
<tr>
|
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;">Entità</th>
|
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;">Legacy ID</th>
|
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #e5e7eb;">Motivo</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($importIssues as $issue)
|
|
<tr>
|
|
<td style="padding:8px;border-bottom:1px solid #f3f4f6;">{{ $issue['entity'] }}</td>
|
|
<td style="padding:8px;border-bottom:1px solid #f3f4f6;">{{ $issue['legacy_id'] }}</td>
|
|
<td style="padding:8px;border-bottom:1px solid #f3f4f6;">{{ $issue['reason'] }}</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
|
<h2 class="text-lg font-semibold text-gray-900">Export XML</h2>
|
|
<p class="text-xs text-gray-500">Esporta i dati correnti dell'app in XML.</p>
|
|
|
|
<div class="flex gap-2">
|
|
<button
|
|
wire:click="exportCurrentAsXml"
|
|
type="button"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg hover:bg-emerald-700 transition border border-emerald-700"
|
|
style="display:inline-flex;align-items:center;justify-content:center;min-height:40px;background:#059669;color:#fff;border:1px solid #065f46;border-radius:10px;padding:10px 14px;"
|
|
>
|
|
Esporta XML
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|