+++fix: add thumbnail support for territori
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
/storage/*.key
|
||||
/storage/app/.app_key
|
||||
/storage/app/.db_seeded
|
||||
/storage/app/public/territori-pdf/*.pdf
|
||||
/storage/logs/
|
||||
/storage/framework/
|
||||
/bootstrap/cache/
|
||||
|
||||
@@ -267,6 +267,11 @@ TerManager2/
|
||||
- Soglia giorni per "da rientrare"
|
||||
- Retention giorni audit log
|
||||
|
||||
### XML Exchange
|
||||
- Conversione dump SQL legacy in XML compatibile con TerManager2
|
||||
- Import XML nell'app (sostituzione dati gestionali: impostazioni, zone, tipologie, proclamatori, territori, anni, campagne, assegnazioni)
|
||||
- Export XML dei dati correnti
|
||||
|
||||
### Zone e Tipologie
|
||||
- CRUD inline (aggiungi, rinomina, attiva/disattiva, elimina)
|
||||
- Protezione: non eliminabile se ha territori associati
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
namespace App\Livewire\Assegnazioni;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Assegna extends Component
|
||||
{
|
||||
public ?int $territorio_id = null;
|
||||
public ?int $proclamatore_id = null;
|
||||
public string $assigned_at = '';
|
||||
public string $territorioSearch = '';
|
||||
|
||||
// Optional pre-selection from parent context
|
||||
public ?int $preselectedTerritorioId = null;
|
||||
@@ -80,10 +83,32 @@ class Assegna extends Component
|
||||
return $this->redirect(route('territori.show', $territorio), navigate: true);
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function selectedThumbnailUrl(): ?string
|
||||
{
|
||||
if (!$this->territorio_id) {
|
||||
return null;
|
||||
}
|
||||
$t = Territorio::find($this->territorio_id);
|
||||
return $t?->thumbnail_path ? Storage::url($t->thumbnail_path) : null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$territoriDisponibili = Territorio::where('attivo', true)
|
||||
$territoriQuery = Territorio::where('attivo', true)
|
||||
->whereDoesntHave('assegnazioni', fn($q) => $q->aperte())
|
||||
->with(['zona', 'tipologia']);
|
||||
|
||||
if (trim($this->territorioSearch) !== '') {
|
||||
$term = trim($this->territorioSearch);
|
||||
$territoriQuery->where(function ($q) use ($term) {
|
||||
$q->where('numero', 'like', "%{$term}%")
|
||||
->orWhereHas('zona', fn($z) => $z->where('nome', 'like', "%{$term}%"))
|
||||
->orWhereHas('tipologia', fn($t) => $t->where('nome', 'like', "%{$term}%"));
|
||||
});
|
||||
}
|
||||
|
||||
$territoriDisponibili = $territoriQuery
|
||||
->orderBy('numero')
|
||||
->get();
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
@@ -21,6 +24,23 @@ class Registro extends Component
|
||||
public string $sortField = 'assigned_at';
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
// ─── Modal create/edit ──────────────────────────────────────
|
||||
public bool $showModal = false;
|
||||
public ?int $editingId = null;
|
||||
|
||||
public string $form_territorio_id = '';
|
||||
public string $form_proclamatore_id = '';
|
||||
public string $form_anno_id = '';
|
||||
public string $form_assigned_at = '';
|
||||
public string $form_returned_at = '';
|
||||
public bool $form_counted_in_campaign = false;
|
||||
public string $form_campaign_id = '';
|
||||
public string $form_note = '';
|
||||
|
||||
// ─── Delete confirm ─────────────────────────────────────────
|
||||
public bool $showDeleteConfirm = false;
|
||||
public ?int $deleteId = null;
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'filtroAnno' => ['except' => ''],
|
||||
@@ -44,6 +64,91 @@ class Registro extends Component
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Admin CRUD ─────────────────────────────────────────────
|
||||
|
||||
public function openCreate(): void
|
||||
{
|
||||
abort_if(!auth()->user()->can('settings.manage'), 403);
|
||||
$this->editingId = null;
|
||||
$this->form_territorio_id = '';
|
||||
$this->form_proclamatore_id = '';
|
||||
$this->form_anno_id = '';
|
||||
$this->form_assigned_at = now()->format('Y-m-d');
|
||||
$this->form_returned_at = '';
|
||||
$this->form_counted_in_campaign = false;
|
||||
$this->form_campaign_id = '';
|
||||
$this->form_note = '';
|
||||
$this->resetValidation();
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function openEdit(int $id): void
|
||||
{
|
||||
abort_if(!auth()->user()->can('settings.manage'), 403);
|
||||
$a = Assegnazione::findOrFail($id);
|
||||
$this->editingId = $id;
|
||||
$this->form_territorio_id = (string) $a->territorio_id;
|
||||
$this->form_proclamatore_id = (string) $a->proclamatore_id;
|
||||
$this->form_anno_id = (string) $a->anno_teocratico_id;
|
||||
$this->form_assigned_at = $a->assigned_at?->format('Y-m-d') ?? '';
|
||||
$this->form_returned_at = $a->returned_at?->format('Y-m-d') ?? '';
|
||||
$this->form_counted_in_campaign = (bool) $a->counted_in_campaign;
|
||||
$this->form_campaign_id = (string) ($a->campaign_id ?? '');
|
||||
$this->form_note = $a->note ?? '';
|
||||
$this->resetValidation();
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
abort_if(!auth()->user()->can('settings.manage'), 403);
|
||||
$this->validate([
|
||||
'form_territorio_id' => 'required|exists:territori,id',
|
||||
'form_proclamatore_id' => 'required|exists:proclamatori,id',
|
||||
'form_anno_id' => 'required|exists:anni_teocratici,id',
|
||||
'form_assigned_at' => 'required|date',
|
||||
'form_returned_at' => 'nullable|date|after_or_equal:form_assigned_at',
|
||||
'form_campaign_id' => 'nullable|exists:campagne,id',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'territorio_id' => $this->form_territorio_id,
|
||||
'proclamatore_id' => $this->form_proclamatore_id,
|
||||
'anno_teocratico_id' => $this->form_anno_id,
|
||||
'assigned_at' => $this->form_assigned_at,
|
||||
'returned_at' => $this->form_returned_at ?: null,
|
||||
'counted_in_campaign' => $this->form_counted_in_campaign,
|
||||
'campaign_id' => $this->form_campaign_id ?: null,
|
||||
'note' => $this->form_note ?: null,
|
||||
];
|
||||
|
||||
if ($this->editingId) {
|
||||
Assegnazione::findOrFail($this->editingId)->update($data);
|
||||
} else {
|
||||
$data['created_by'] = auth()->id();
|
||||
Assegnazione::create($data);
|
||||
}
|
||||
|
||||
$this->showModal = false;
|
||||
}
|
||||
|
||||
public function askDelete(int $id): void
|
||||
{
|
||||
abort_if(!auth()->user()->can('settings.manage'), 403);
|
||||
$this->deleteId = $id;
|
||||
$this->showDeleteConfirm = true;
|
||||
}
|
||||
|
||||
public function deleteConfirmed(): void
|
||||
{
|
||||
abort_if(!auth()->user()->can('settings.manage'), 403);
|
||||
if ($this->deleteId) {
|
||||
Assegnazione::findOrFail($this->deleteId)->delete();
|
||||
}
|
||||
$this->deleteId = null;
|
||||
$this->showDeleteConfirm = false;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Assegnazione::with(['territorio.zona', 'proclamatore', 'annoTeocratico', 'campagna']);
|
||||
@@ -96,6 +201,9 @@ class Registro extends Component
|
||||
'anni' => AnnoTeocratico::orderByDesc('start_date')->get(),
|
||||
'zone' => Zona::attive()->orderBy('nome')->get(),
|
||||
'tipologie' => Tipologia::orderBy('nome')->get(),
|
||||
'territori' => Territorio::attivi()->orderBy('numero')->get(),
|
||||
'proclamatori' => Proclamatore::attivi()->orderBy('cognome')->orderBy('nome')->get(),
|
||||
'campagne' => Campagna::orderByDesc('created_at')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
714
app/Livewire/Settings/XmlExchange.php
Normal file
714
app/Livewire/Settings/XmlExchange.php
Normal file
@@ -0,0 +1,714 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Tipologia;
|
||||
use App\Models\User;
|
||||
use App\Models\Zona;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class XmlExchange extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public $sqlDump;
|
||||
public $xmlImport;
|
||||
public array $importStats = [];
|
||||
public array $importIssues = [];
|
||||
|
||||
public function convertLegacySqlToXml()
|
||||
{
|
||||
$this->validate([
|
||||
'sqlDump' => ['required', 'file', 'mimes:sql,txt'],
|
||||
]);
|
||||
|
||||
$content = file_get_contents($this->sqlDump->getRealPath());
|
||||
$dataset = $this->legacySqlToDataset($content ?: '');
|
||||
$xml = $this->datasetToXml($dataset, 'legacy-sql-conversion');
|
||||
|
||||
return response()->streamDownload(function () use ($xml) {
|
||||
echo $xml;
|
||||
}, 'termanager-conversion.xml', ['Content-Type' => 'application/xml; charset=UTF-8']);
|
||||
}
|
||||
|
||||
public function importXmlIntoApp(): void
|
||||
{
|
||||
$this->importStats = [];
|
||||
$this->importIssues = [];
|
||||
|
||||
$this->validate([
|
||||
'xmlImport' => ['required', 'file', 'mimes:xml,txt'],
|
||||
]);
|
||||
|
||||
$content = file_get_contents($this->xmlImport->getRealPath());
|
||||
if (! $content) {
|
||||
$this->addError('xmlImport', 'File XML non valido.');
|
||||
return;
|
||||
}
|
||||
|
||||
$xml = @simplexml_load_string($content);
|
||||
if (! $xml) {
|
||||
$this->addError('xmlImport', 'Impossibile leggere il file XML.');
|
||||
return;
|
||||
}
|
||||
|
||||
$actorId = auth()->id() ?? User::query()->value('id');
|
||||
if (! $actorId) {
|
||||
$this->addError('xmlImport', 'Serve almeno un utente nel sistema per importare i dati.');
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'zone_importate' => 0,
|
||||
'tipologie_importate' => 0,
|
||||
'proclamatori_importati' => 0,
|
||||
'anni_importati' => 0,
|
||||
'campagne_importate' => 0,
|
||||
'territori_importati' => 0,
|
||||
'assegnazioni_importate' => 0,
|
||||
'duplicate_territori' => 0,
|
||||
'assegnazioni_saltate' => 0,
|
||||
];
|
||||
|
||||
DB::transaction(function () use ($xml, $actorId, &$stats) {
|
||||
Assegnazione::query()->delete();
|
||||
Campagna::query()->delete();
|
||||
Territorio::query()->withTrashed()->forceDelete();
|
||||
Proclamatore::query()->withTrashed()->forceDelete();
|
||||
Tipologia::query()->delete();
|
||||
Zona::query()->delete();
|
||||
AnnoTeocratico::query()->delete();
|
||||
Setting::query()->delete();
|
||||
|
||||
$settingsNode = $xml->settings;
|
||||
if ($settingsNode) {
|
||||
Setting::instance()->update([
|
||||
'congregazione_nome' => (string) ($settingsNode->congregazione_nome ?? ''),
|
||||
'giorni_giacenza_da_assegnare' => (int) ($settingsNode->giorni_giacenza_da_assegnare ?? 120),
|
||||
'giorni_giacenza_prioritari' => (int) ($settingsNode->giorni_giacenza_prioritari ?? 180),
|
||||
'giorni_per_smarrito' => (int) ($settingsNode->giorni_per_smarrito ?? 120),
|
||||
'home_limit_list' => (int) ($settingsNode->home_limit_list ?? 10),
|
||||
'audit_retention_days' => (int) ($settingsNode->audit_retention_days ?? 730),
|
||||
'setup_completed' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$zoneMap = [];
|
||||
foreach (($xml->zones->zone ?? []) as $zoneNode) {
|
||||
$z = Zona::create([
|
||||
'nome' => (string) $zoneNode->nome,
|
||||
'attivo' => ((int) ($zoneNode->attivo ?? 1)) === 1,
|
||||
]);
|
||||
$stats['zone_importate']++;
|
||||
$legacyId = (string) ($zoneNode['legacy_id'] ?? '');
|
||||
if ($legacyId !== '') {
|
||||
$zoneMap[$legacyId] = $z->id;
|
||||
}
|
||||
}
|
||||
|
||||
$tipologiaMap = [];
|
||||
foreach (($xml->tipologie->tipologia ?? []) as $tipologiaNode) {
|
||||
$t = Tipologia::create([
|
||||
'nome' => (string) $tipologiaNode->nome,
|
||||
'attivo' => ((int) ($tipologiaNode->attivo ?? 1)) === 1,
|
||||
]);
|
||||
$stats['tipologie_importate']++;
|
||||
$legacyId = (string) ($tipologiaNode['legacy_id'] ?? '');
|
||||
if ($legacyId !== '') {
|
||||
$tipologiaMap[$legacyId] = $t->id;
|
||||
}
|
||||
}
|
||||
|
||||
$proclamatoreMap = [];
|
||||
foreach (($xml->proclamatori->proclamatore ?? []) as $proclamatoreNode) {
|
||||
$p = Proclamatore::create([
|
||||
'nome' => (string) $proclamatoreNode->nome,
|
||||
'cognome' => (string) $proclamatoreNode->cognome,
|
||||
'attivo' => ((int) ($proclamatoreNode->attivo ?? 1)) === 1,
|
||||
]);
|
||||
$stats['proclamatori_importati']++;
|
||||
$legacyId = (string) ($proclamatoreNode['legacy_id'] ?? '');
|
||||
if ($legacyId !== '') {
|
||||
$proclamatoreMap[$legacyId] = $p->id;
|
||||
}
|
||||
}
|
||||
|
||||
$annoMap = [];
|
||||
foreach (($xml->anni_teocratici->anno ?? []) as $annoNode) {
|
||||
$label = (string) $annoNode->label;
|
||||
$anno = AnnoTeocratico::create([
|
||||
'label' => $label,
|
||||
'start_date' => (string) $annoNode->start_date,
|
||||
'end_date' => (string) $annoNode->end_date,
|
||||
]);
|
||||
$stats['anni_importati']++;
|
||||
$legacyId = (string) ($annoNode['legacy_id'] ?? '');
|
||||
if ($legacyId !== '') {
|
||||
$annoMap[$legacyId] = $anno->id;
|
||||
}
|
||||
}
|
||||
|
||||
$campagnaMap = [];
|
||||
foreach (($xml->campagne->campagna ?? []) as $campagnaNode) {
|
||||
$campagna = Campagna::create([
|
||||
'descrizione' => (string) $campagnaNode->descrizione,
|
||||
'start_date' => (string) $campagnaNode->start_date,
|
||||
'end_date' => (string) $campagnaNode->end_date,
|
||||
]);
|
||||
$stats['campagne_importate']++;
|
||||
$legacyId = (string) ($campagnaNode['legacy_id'] ?? '');
|
||||
if ($legacyId !== '') {
|
||||
$campagnaMap[$legacyId] = $campagna->id;
|
||||
}
|
||||
}
|
||||
|
||||
$territorioMap = [];
|
||||
$territoriNumeroVisti = [];
|
||||
foreach (($xml->territori->territorio ?? []) as $territorioNode) {
|
||||
$legacyZonaId = (string) ($territorioNode->legacy_zona_id ?? '');
|
||||
$legacyTipologiaId = (string) ($territorioNode->legacy_tipologia_id ?? '');
|
||||
$numero = trim((string) $territorioNode->numero);
|
||||
$legacyId = (string) ($territorioNode['legacy_id'] ?? '');
|
||||
|
||||
if ($numero === '' || isset($territoriNumeroVisti[$numero])) {
|
||||
$stats['duplicate_territori']++;
|
||||
$this->pushImportIssue('territorio', $legacyId, 'Territorio duplicato o numero vuoto (numero=' . ($numero !== '' ? $numero : 'vuoto') . ')');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($zoneMap[$legacyZonaId], $tipologiaMap[$legacyTipologiaId])) {
|
||||
$this->pushImportIssue('territorio', $legacyId, 'Riferimento zona/tipologia non trovato (zona=' . $legacyZonaId . ', tipologia=' . $legacyTipologiaId . ')');
|
||||
continue;
|
||||
}
|
||||
|
||||
$territorio = Territorio::create([
|
||||
'numero' => $numero,
|
||||
'zona_id' => $zoneMap[$legacyZonaId],
|
||||
'tipologia_id' => $tipologiaMap[$legacyTipologiaId],
|
||||
'confini' => (string) ($territorioNode->confini ?? ''),
|
||||
'note' => (string) ($territorioNode->note ?? ''),
|
||||
'attivo' => ((int) ($territorioNode->attivo ?? 1)) === 1,
|
||||
'prioritario' => ((int) ($territorioNode->prioritario ?? 0)) === 1,
|
||||
]);
|
||||
|
||||
$territoriNumeroVisti[$numero] = true;
|
||||
$stats['territori_importati']++;
|
||||
|
||||
if ($legacyId !== '') {
|
||||
$territorioMap[$legacyId] = $territorio->id;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (($xml->assegnazioni->assegnazione ?? []) as $assegnazioneNode) {
|
||||
$legacyTerritorio = (string) ($assegnazioneNode->legacy_territorio_id ?? '');
|
||||
$legacyProclamatore = (string) ($assegnazioneNode->legacy_proclamatore_id ?? '');
|
||||
$legacyAnno = (string) ($assegnazioneNode->legacy_anno_id ?? '');
|
||||
$legacyId = (string) ($assegnazioneNode['legacy_id'] ?? '');
|
||||
$assignedAt = $this->normalizeDateForDb((string) ($assegnazioneNode->assigned_at ?? ''), false);
|
||||
$returnedAt = $this->normalizeDateForDb((string) ($assegnazioneNode->returned_at ?? ''), true);
|
||||
|
||||
if (!isset($territorioMap[$legacyTerritorio], $proclamatoreMap[$legacyProclamatore], $annoMap[$legacyAnno])) {
|
||||
$stats['assegnazioni_saltate']++;
|
||||
$this->pushImportIssue('assegnazione', $legacyId, 'Riferimenti mancanti (territorio=' . $legacyTerritorio . ', proclamatore=' . $legacyProclamatore . ', anno=' . $legacyAnno . ')');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($assignedAt === null) {
|
||||
$stats['assegnazioni_saltate']++;
|
||||
$this->pushImportIssue('assegnazione', $legacyId, 'Data assigned_at non valida o vuota');
|
||||
continue;
|
||||
}
|
||||
|
||||
$legacyCampagna = (string) ($assegnazioneNode->legacy_campagna_id ?? '');
|
||||
|
||||
Assegnazione::create([
|
||||
'territorio_id' => $territorioMap[$legacyTerritorio],
|
||||
'proclamatore_id' => $proclamatoreMap[$legacyProclamatore],
|
||||
'anno_teocratico_id' => $annoMap[$legacyAnno],
|
||||
'assigned_at' => $assignedAt,
|
||||
'returned_at' => $returnedAt,
|
||||
'counted_in_campaign' => ((int) ($assegnazioneNode->counted_in_campaign ?? 0)) === 1,
|
||||
'campaign_id' => $legacyCampagna !== '' && isset($campagnaMap[$legacyCampagna]) ? $campagnaMap[$legacyCampagna] : null,
|
||||
'note' => (string) ($assegnazioneNode->note ?? ''),
|
||||
'created_by' => $actorId,
|
||||
'returned_by' => $returnedAt !== null ? $actorId : null,
|
||||
]);
|
||||
$stats['assegnazioni_importate']++;
|
||||
}
|
||||
});
|
||||
|
||||
$this->importStats = $stats;
|
||||
|
||||
$message = 'Import XML completato con successo.';
|
||||
if ($stats['duplicate_territori'] > 0 || $stats['assegnazioni_saltate'] > 0) {
|
||||
$message .= ' Territori duplicati saltati: ' . $stats['duplicate_territori'] . '. Assegnazioni saltate: ' . $stats['assegnazioni_saltate'] . '.';
|
||||
}
|
||||
|
||||
session()->flash('success', $message);
|
||||
}
|
||||
|
||||
public function downloadImportLogPdf()
|
||||
{
|
||||
if (empty($this->importStats)) {
|
||||
session()->flash('error', 'Nessun log da scaricare. Esegui prima un import XML.');
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $this->importStats;
|
||||
$issues = $this->importIssues;
|
||||
$generatedAt = now()->format('d/m/Y H:i:s');
|
||||
|
||||
$html = view('pdf.import-log', compact('stats', 'issues', 'generatedAt'))->render();
|
||||
|
||||
return response()->streamDownload(function () use ($html) {
|
||||
echo Pdf::loadHTML($html)
|
||||
->setPaper('a4', 'portrait')
|
||||
->output();
|
||||
}, 'import-log-' . now()->format('Ymd-His') . '.pdf', ['Content-Type' => 'application/pdf']);
|
||||
}
|
||||
|
||||
public function exportCurrentAsXml()
|
||||
{
|
||||
$dataset = $this->currentDataset();
|
||||
$xml = $this->datasetToXml($dataset, 'current-app-export');
|
||||
|
||||
return response()->streamDownload(function () use ($xml) {
|
||||
echo $xml;
|
||||
}, 'termanager-export.xml', ['Content-Type' => 'application/xml; charset=UTF-8']);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.xml-exchange');
|
||||
}
|
||||
|
||||
private function currentDataset(): array
|
||||
{
|
||||
$settings = Setting::instance();
|
||||
|
||||
return [
|
||||
'settings' => [
|
||||
'congregazione_nome' => (string) ($settings->congregazione_nome ?? ''),
|
||||
'giorni_giacenza_da_assegnare' => (int) ($settings->giorni_giacenza_da_assegnare ?? 120),
|
||||
'giorni_giacenza_prioritari' => (int) ($settings->giorni_giacenza_prioritari ?? 180),
|
||||
'giorni_per_smarrito' => (int) ($settings->giorni_per_smarrito ?? 120),
|
||||
'home_limit_list' => (int) ($settings->home_limit_list ?? 10),
|
||||
'audit_retention_days' => (int) ($settings->audit_retention_days ?? 730),
|
||||
],
|
||||
'zones' => Zona::query()->orderBy('id')->get(['id', 'nome', 'attivo'])->toArray(),
|
||||
'tipologie' => Tipologia::query()->orderBy('id')->get(['id', 'nome', 'attivo'])->toArray(),
|
||||
'proclamatori' => Proclamatore::query()->withTrashed()->orderBy('id')->get(['id', 'nome', 'cognome', 'attivo'])->toArray(),
|
||||
'territori' => Territorio::query()->withTrashed()->orderBy('id')->get(['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario'])->toArray(),
|
||||
'anni_teocratici' => AnnoTeocratico::query()->orderBy('id')->get(['id', 'label', 'start_date', 'end_date'])->toArray(),
|
||||
'campagne' => Campagna::query()->orderBy('id')->get(['id', 'descrizione', 'start_date', 'end_date'])->toArray(),
|
||||
'assegnazioni' => Assegnazione::query()->orderBy('id')->get(['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note'])->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
private function legacySqlToDataset(string $sql): array
|
||||
{
|
||||
$rows = $this->extractInsertRows($sql);
|
||||
|
||||
$congregazione = $rows['congregazione'][0][1] ?? 'Congregazione';
|
||||
$impostazioni = $rows['impostazioni'][0] ?? [1, 120, 10, 120];
|
||||
|
||||
$settings = [
|
||||
'congregazione_nome' => (string) $congregazione,
|
||||
'giorni_giacenza_da_assegnare' => (int) ($impostazioni[3] ?? 120),
|
||||
'giorni_giacenza_prioritari' => (int) ($impostazioni[1] ?? 180),
|
||||
'giorni_per_smarrito' => (int) ($impostazioni[3] ?? 120),
|
||||
'home_limit_list' => (int) ($impostazioni[2] ?? 10),
|
||||
'audit_retention_days' => 730,
|
||||
];
|
||||
|
||||
$zones = [];
|
||||
foreach (($rows['zona'] ?? []) as $r) {
|
||||
$zones[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'nome' => (string) ($r[1] ?? ''),
|
||||
'attivo' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$tipologie = [];
|
||||
foreach (($rows['tipologia'] ?? []) as $r) {
|
||||
$tipologie[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'nome' => (string) ($r[1] ?? ''),
|
||||
'attivo' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
$proclamatori = [];
|
||||
foreach (($rows['proclamatore'] ?? []) as $r) {
|
||||
$proclamatori[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'cognome' => (string) ($r[1] ?? ''),
|
||||
'nome' => (string) ($r[2] ?? ''),
|
||||
'attivo' => ((int) ($r[3] ?? 1)) === 1 ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
$territori = [];
|
||||
foreach (($rows['territorio'] ?? []) as $r) {
|
||||
$territori[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'numero' => (string) ($r[1] ?? ''),
|
||||
'legacy_tipologia_id' => (string) ($r[2] ?? ''),
|
||||
'legacy_zona_id' => (string) ($r[3] ?? ''),
|
||||
'confini' => (string) ($r[4] ?? ''),
|
||||
'note' => (string) ($r[5] ?? ''),
|
||||
'attivo' => ((int) ($r[8] ?? 1)) === 1 ? 1 : 0,
|
||||
'prioritario' => ((int) ($r[11] ?? 0)) > 0 ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
$anni = [];
|
||||
foreach (($rows['annoServizio'] ?? []) as $r) {
|
||||
$label = (string) ($r[1] ?? '');
|
||||
[$startDate, $endDate] = $this->datesFromLegacyAnnoLabel($label);
|
||||
$anni[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'label' => str_replace(' - ', '-', $label),
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
];
|
||||
}
|
||||
|
||||
$campagne = [];
|
||||
foreach (($rows['campagna'] ?? []) as $r) {
|
||||
$campagne[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'descrizione' => (string) ($r[1] ?? ''),
|
||||
'start_date' => $this->toDateOnly((string) ($r[2] ?? '')),
|
||||
'end_date' => $this->toDateOnly((string) ($r[3] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
$assegnazioni = [];
|
||||
foreach (($rows['assegnazione'] ?? []) as $r) {
|
||||
$assegnazioni[] = [
|
||||
'legacy_id' => (string) ($r[0] ?? ''),
|
||||
'legacy_territorio_id' => (string) ($r[1] ?? ''),
|
||||
'legacy_proclamatore_id' => (string) ($r[2] ?? ''),
|
||||
'legacy_anno_id' => (string) ($r[9] ?? ''),
|
||||
'legacy_campagna_id' => $r[8] === null ? '' : (string) $r[8],
|
||||
'assigned_at' => $this->toDateOnly((string) ($r[4] ?? '')),
|
||||
'returned_at' => $this->toDateOnly((string) ($r[5] ?? '')),
|
||||
'counted_in_campaign' => ((int) ($r[7] ?? 0)) === 1 ? 1 : 0,
|
||||
'note' => '',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'settings' => $settings,
|
||||
'zones' => $zones,
|
||||
'tipologie' => $tipologie,
|
||||
'proclamatori' => $proclamatori,
|
||||
'territori' => $territori,
|
||||
'anni_teocratici' => $anni,
|
||||
'campagne' => $campagne,
|
||||
'assegnazioni' => $assegnazioni,
|
||||
];
|
||||
}
|
||||
|
||||
private function extractInsertRows(string $sql): array
|
||||
{
|
||||
$result = [];
|
||||
preg_match_all('/INSERT INTO `([^`]+)` VALUES\s*(.+?);/s', $sql, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$table = $match[1];
|
||||
$valuesPayload = $match[2];
|
||||
$tuples = $this->splitSqlTuples($valuesPayload);
|
||||
|
||||
foreach ($tuples as $tuple) {
|
||||
$result[$table][] = $this->parseSqlTuple($tuple);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function splitSqlTuples(string $payload): array
|
||||
{
|
||||
$tuples = [];
|
||||
$inString = false;
|
||||
$escape = false;
|
||||
$depth = 0;
|
||||
$current = '';
|
||||
|
||||
$len = strlen($payload);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$ch = $payload[$i];
|
||||
|
||||
if ($inString) {
|
||||
$current .= $ch;
|
||||
if ($escape) {
|
||||
$escape = false;
|
||||
continue;
|
||||
}
|
||||
if ($ch === '\\\\') {
|
||||
$escape = true;
|
||||
continue;
|
||||
}
|
||||
if ($ch === "'") {
|
||||
$inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === "'") {
|
||||
$inString = true;
|
||||
$current .= $ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === '(') {
|
||||
if ($depth === 0) {
|
||||
$current = '';
|
||||
}
|
||||
$depth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === ')') {
|
||||
$depth--;
|
||||
if ($depth === 0) {
|
||||
$tuples[] = $current;
|
||||
$current = '';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($depth > 0) {
|
||||
$current .= $ch;
|
||||
}
|
||||
}
|
||||
|
||||
return $tuples;
|
||||
}
|
||||
|
||||
private function parseSqlTuple(string $tuple): array
|
||||
{
|
||||
$values = [];
|
||||
$inString = false;
|
||||
$escape = false;
|
||||
$current = '';
|
||||
|
||||
$len = strlen($tuple);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$ch = $tuple[$i];
|
||||
|
||||
if ($inString) {
|
||||
$current .= $ch;
|
||||
if ($escape) {
|
||||
$escape = false;
|
||||
continue;
|
||||
}
|
||||
if ($ch === '\\\\') {
|
||||
$escape = true;
|
||||
continue;
|
||||
}
|
||||
if ($ch === "'") {
|
||||
$inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === "'") {
|
||||
$inString = true;
|
||||
$current .= $ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === ',') {
|
||||
$values[] = $this->normalizeSqlValue($current);
|
||||
$current = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$current .= $ch;
|
||||
}
|
||||
|
||||
if ($current !== '') {
|
||||
$values[] = $this->normalizeSqlValue($current);
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function normalizeSqlValue(string $raw)
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if (strcasecmp($raw, 'NULL') === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($raw, "'") && str_ends_with($raw, "'")) {
|
||||
$v = substr($raw, 1, -1);
|
||||
$v = str_replace(["\\\\", "\\'", '\\"', '\\n', '\\r', '\\t'], ['\\', "'", '"', "\n", "\r", "\t"], $v);
|
||||
return html_entity_decode($v, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
if (is_numeric($raw)) {
|
||||
return str_contains($raw, '.') ? (float) $raw : (int) $raw;
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
private function datesFromLegacyAnnoLabel(string $label): array
|
||||
{
|
||||
if (preg_match('/(\d{4})\s*-\s*(\d{4})/', $label, $m)) {
|
||||
return [$m[1] . '-09-01', $m[2] . '-08-31'];
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
return [$now->copy()->startOfYear()->toDateString(), $now->copy()->endOfYear()->toDateString()];
|
||||
}
|
||||
|
||||
private function toDateOnly(string $dateTime): string
|
||||
{
|
||||
if ($dateTime === '') {
|
||||
return '';
|
||||
}
|
||||
return substr($dateTime, 0, 10);
|
||||
}
|
||||
|
||||
private function normalizeDateForDb(string $raw, bool $nullable): ?string
|
||||
{
|
||||
$raw = trim($raw);
|
||||
if ($raw === '' || $raw === '0000-00-00' || $raw === '0000-00-00 00:00:00') {
|
||||
return $nullable ? null : null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($raw)->toDateString();
|
||||
} catch (\Throwable $e) {
|
||||
return $nullable ? null : null;
|
||||
}
|
||||
}
|
||||
|
||||
private function pushImportIssue(string $entity, string $legacyId, string $reason): void
|
||||
{
|
||||
if (count($this->importIssues) >= 300) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->importIssues[] = [
|
||||
'entity' => $entity,
|
||||
'legacy_id' => $legacyId !== '' ? $legacyId : '-',
|
||||
'reason' => $reason,
|
||||
];
|
||||
}
|
||||
|
||||
private function datasetToXml(array $dataset, string $source): string
|
||||
{
|
||||
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><termanager2/>');
|
||||
$xml->addAttribute('version', '1.0');
|
||||
$xml->addAttribute('source', $source);
|
||||
$xml->addAttribute('generated_at', now()->toIso8601String());
|
||||
|
||||
$settings = $xml->addChild('settings');
|
||||
foreach ($dataset['settings'] as $key => $value) {
|
||||
$settings->addChild($key, htmlspecialchars((string) $value, ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
|
||||
$zonesNode = $xml->addChild('zones');
|
||||
foreach ($dataset['zones'] as $zone) {
|
||||
$node = $zonesNode->addChild('zone');
|
||||
if (isset($zone['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $zone['legacy_id']);
|
||||
}
|
||||
$node->addChild('nome', htmlspecialchars((string) ($zone['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('attivo', (string) ((int) ($zone['attivo'] ?? 1)));
|
||||
}
|
||||
|
||||
$tipologieNode = $xml->addChild('tipologie');
|
||||
foreach ($dataset['tipologie'] as $tipologia) {
|
||||
$node = $tipologieNode->addChild('tipologia');
|
||||
if (isset($tipologia['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $tipologia['legacy_id']);
|
||||
}
|
||||
$node->addChild('nome', htmlspecialchars((string) ($tipologia['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('attivo', (string) ((int) ($tipologia['attivo'] ?? 1)));
|
||||
}
|
||||
|
||||
$proclamatoriNode = $xml->addChild('proclamatori');
|
||||
foreach ($dataset['proclamatori'] as $proclamatore) {
|
||||
$node = $proclamatoriNode->addChild('proclamatore');
|
||||
if (isset($proclamatore['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $proclamatore['legacy_id']);
|
||||
}
|
||||
$node->addChild('nome', htmlspecialchars((string) ($proclamatore['nome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('cognome', htmlspecialchars((string) ($proclamatore['cognome'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('attivo', (string) ((int) ($proclamatore['attivo'] ?? 1)));
|
||||
}
|
||||
|
||||
$territoriNode = $xml->addChild('territori');
|
||||
foreach ($dataset['territori'] as $territorio) {
|
||||
$node = $territoriNode->addChild('territorio');
|
||||
if (isset($territorio['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $territorio['legacy_id']);
|
||||
}
|
||||
$node->addChild('numero', htmlspecialchars((string) ($territorio['numero'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('legacy_zona_id', (string) ($territorio['legacy_zona_id'] ?? ($territorio['zona_id'] ?? '')));
|
||||
$node->addChild('legacy_tipologia_id', (string) ($territorio['legacy_tipologia_id'] ?? ($territorio['tipologia_id'] ?? '')));
|
||||
$node->addChild('confini', htmlspecialchars((string) ($territorio['confini'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('note', htmlspecialchars((string) ($territorio['note'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('attivo', (string) ((int) ($territorio['attivo'] ?? 1)));
|
||||
$node->addChild('prioritario', (string) ((int) ($territorio['prioritario'] ?? 0)));
|
||||
}
|
||||
|
||||
$anniNode = $xml->addChild('anni_teocratici');
|
||||
foreach ($dataset['anni_teocratici'] as $anno) {
|
||||
$node = $anniNode->addChild('anno');
|
||||
if (isset($anno['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $anno['legacy_id']);
|
||||
}
|
||||
$node->addChild('label', htmlspecialchars((string) ($anno['label'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('start_date', (string) ($anno['start_date'] ?? ''));
|
||||
$node->addChild('end_date', (string) ($anno['end_date'] ?? ''));
|
||||
}
|
||||
|
||||
$campagneNode = $xml->addChild('campagne');
|
||||
foreach ($dataset['campagne'] as $campagna) {
|
||||
$node = $campagneNode->addChild('campagna');
|
||||
if (isset($campagna['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $campagna['legacy_id']);
|
||||
}
|
||||
$node->addChild('descrizione', htmlspecialchars((string) ($campagna['descrizione'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
$node->addChild('start_date', (string) ($campagna['start_date'] ?? ''));
|
||||
$node->addChild('end_date', (string) ($campagna['end_date'] ?? ''));
|
||||
}
|
||||
|
||||
$assegnazioniNode = $xml->addChild('assegnazioni');
|
||||
foreach ($dataset['assegnazioni'] as $assegnazione) {
|
||||
$node = $assegnazioniNode->addChild('assegnazione');
|
||||
if (isset($assegnazione['legacy_id'])) {
|
||||
$node->addAttribute('legacy_id', (string) $assegnazione['legacy_id']);
|
||||
}
|
||||
$node->addChild('legacy_territorio_id', (string) ($assegnazione['legacy_territorio_id'] ?? ($assegnazione['territorio_id'] ?? '')));
|
||||
$node->addChild('legacy_proclamatore_id', (string) ($assegnazione['legacy_proclamatore_id'] ?? ($assegnazione['proclamatore_id'] ?? '')));
|
||||
$node->addChild('legacy_anno_id', (string) ($assegnazione['legacy_anno_id'] ?? ($assegnazione['anno_teocratico_id'] ?? '')));
|
||||
$node->addChild('legacy_campagna_id', (string) ($assegnazione['legacy_campagna_id'] ?? ($assegnazione['campaign_id'] ?? '')));
|
||||
$node->addChild('assigned_at', (string) ($assegnazione['assigned_at'] ?? ''));
|
||||
$node->addChild('returned_at', (string) ($assegnazione['returned_at'] ?? ''));
|
||||
$node->addChild('counted_in_campaign', (string) ((int) ($assegnazione['counted_in_campaign'] ?? 0)));
|
||||
$node->addChild('note', htmlspecialchars((string) ($assegnazione['note'] ?? ''), ENT_QUOTES | ENT_XML1, 'UTF-8'));
|
||||
}
|
||||
|
||||
return $xml->asXML() ?: '';
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Livewire\WithFileUploads;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
use App\Services\TerritorioThumbnailService;
|
||||
|
||||
class TerritorioCreate extends Component
|
||||
{
|
||||
@@ -48,7 +49,9 @@ class TerritorioCreate extends Component
|
||||
];
|
||||
|
||||
if ($this->pdf) {
|
||||
$data['pdf_path'] = $this->pdf->store('territori-pdf', 'public');
|
||||
$pdfPath = $this->pdf->store('territori-pdf', 'public');
|
||||
$data['pdf_path'] = $pdfPath;
|
||||
$data['thumbnail_path'] = app(TerritorioThumbnailService::class)->generate($pdfPath);
|
||||
}
|
||||
|
||||
$territorio = Territorio::create($data);
|
||||
|
||||
@@ -7,6 +7,7 @@ use Livewire\WithFileUploads;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
use App\Services\TerritorioThumbnailService;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class TerritorioEdit extends Component
|
||||
@@ -60,11 +61,17 @@ class TerritorioEdit extends Component
|
||||
];
|
||||
|
||||
if ($this->pdf) {
|
||||
// Remove old PDF
|
||||
$thumbService = app(TerritorioThumbnailService::class);
|
||||
// Remove old PDF and thumbnail
|
||||
if ($this->territorio->pdf_path) {
|
||||
Storage::disk('public')->delete($this->territorio->pdf_path);
|
||||
}
|
||||
$data['pdf_path'] = $this->pdf->store('territori-pdf', 'public');
|
||||
if ($this->territorio->thumbnail_path) {
|
||||
$thumbService->delete($this->territorio->thumbnail_path);
|
||||
}
|
||||
$pdfPath = $this->pdf->store('territori-pdf', 'public');
|
||||
$data['pdf_path'] = $pdfPath;
|
||||
$data['thumbnail_path'] = $thumbService->generate($pdfPath);
|
||||
}
|
||||
|
||||
$this->territorio->update($data);
|
||||
@@ -77,7 +84,10 @@ class TerritorioEdit extends Component
|
||||
{
|
||||
if ($this->territorio->pdf_path) {
|
||||
Storage::disk('public')->delete($this->territorio->pdf_path);
|
||||
$this->territorio->update(['pdf_path' => null]);
|
||||
if ($this->territorio->thumbnail_path) {
|
||||
app(TerritorioThumbnailService::class)->delete($this->territorio->thumbnail_path);
|
||||
}
|
||||
$this->territorio->update(['pdf_path' => null, 'thumbnail_path' => null]);
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($this->territorio)
|
||||
->log('removed_pdf');
|
||||
|
||||
@@ -21,6 +21,7 @@ class Territorio extends Model
|
||||
'note',
|
||||
'confini',
|
||||
'pdf_path',
|
||||
'thumbnail_path',
|
||||
'attivo',
|
||||
'prioritario',
|
||||
];
|
||||
|
||||
61
app/Services/TerritorioThumbnailService.php
Normal file
61
app/Services/TerritorioThumbnailService.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class TerritorioThumbnailService
|
||||
{
|
||||
/**
|
||||
* Generate a PNG thumbnail of the first page of a PDF stored on the public disk.
|
||||
*
|
||||
* @param string $pdfStoragePath Relative path within the public disk (e.g. "territori-pdf/abc.pdf")
|
||||
* @return string|null Relative path of the saved thumbnail, or null on failure
|
||||
*/
|
||||
public function generate(string $pdfStoragePath): ?string
|
||||
{
|
||||
$pdfAbsPath = Storage::disk('public')->path($pdfStoragePath);
|
||||
|
||||
if (!file_exists($pdfAbsPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tempPrefix = sys_get_temp_dir() . '/terr_thumb_' . uniqid();
|
||||
|
||||
exec(
|
||||
'pdftoppm -r 72 -png -l 1 ' . escapeshellarg($pdfAbsPath) . ' ' . escapeshellarg($tempPrefix),
|
||||
$output,
|
||||
$exitCode
|
||||
);
|
||||
|
||||
// pdftoppm may produce -1.png, -01.png or even -001.png depending on page count
|
||||
$generated = null;
|
||||
foreach (['-1.png', '-01.png', '-001.png'] as $suffix) {
|
||||
$candidate = $tempPrefix . $suffix;
|
||||
if (file_exists($candidate)) {
|
||||
$generated = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$generated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$thumbRelPath = 'territori-thumbnails/' . basename($pdfStoragePath, '.pdf') . '.png';
|
||||
Storage::disk('public')->put($thumbRelPath, file_get_contents($generated));
|
||||
unlink($generated);
|
||||
|
||||
return $thumbRelPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing thumbnail from the public disk.
|
||||
*/
|
||||
public function delete(string $thumbnailPath): void
|
||||
{
|
||||
if ($thumbnailPath) {
|
||||
Storage::disk('public')->delete($thumbnailPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,12 @@
|
||||
"license": "proprietary",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"barryvdh/laravel-dompdf": "^3.1",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.5",
|
||||
"spatie/laravel-permission": "^6.4",
|
||||
"spatie/laravel-activitylog": "^4.8"
|
||||
"spatie/laravel-activitylog": "^4.8",
|
||||
"spatie/laravel-permission": "^6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
524
composer.lock
generated
524
composer.lock
generated
@@ -4,8 +4,85 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f5778263babc2e758e3225284a64e9b3",
|
||||
"content-hash": "2501abb81bccf7d5db1e152bde41bd5b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-dompdf",
|
||||
"version": "v3.1.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-dompdf.git",
|
||||
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||
"reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"illuminate/support": "^9|^10|^11|^12|^13.0",
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"larastan/larastan": "^2.7|^3.0",
|
||||
"orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
|
||||
"phpro/grumphp": "^2.5",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||
},
|
||||
"providers": [
|
||||
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\DomPDF\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A DOMPDF Wrapper for Laravel",
|
||||
"keywords": [
|
||||
"dompdf",
|
||||
"laravel",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
|
||||
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://fruitcake.nl",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/barryvdh",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-21T08:51:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.14.8",
|
||||
@@ -377,6 +454,161 @@
|
||||
],
|
||||
"time": "2024-02-05T11:56:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v3.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/dompdf.git",
|
||||
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||
"reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dompdf/php-font-lib": "^1.0.0",
|
||||
"dompdf/php-svg-lib": "^1.0.0",
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||
"source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
|
||||
},
|
||||
"time": "2026-03-03T13:54:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-font-lib",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FontLib\\": "src/FontLib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The FontLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||
"homepage": "https://github.com/dompdf/php-font-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-01-20T14:10:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dompdf/php-svg-lib",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabberworm/php-css-parser": "^8.4 || ^9.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Svg\\": "src/Svg"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The SvgLib Community",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse and export to PDF SVG files.",
|
||||
"homepage": "https://github.com/dompdf/php-svg-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
|
||||
},
|
||||
"time": "2026-01-02T16:01:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dragonmantank/cron-expression",
|
||||
"version": "v3.6.0",
|
||||
@@ -2089,6 +2321,73 @@
|
||||
],
|
||||
"time": "2026-04-03T13:08:41+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.10.0",
|
||||
@@ -3364,6 +3663,86 @@
|
||||
},
|
||||
"time": "2025-12-14T04:43:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v9.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
|
||||
"reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||
"phpstan/extension-installer": "1.4.3",
|
||||
"phpstan/phpstan": "1.12.32 || 2.1.32",
|
||||
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
|
||||
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
|
||||
"phpunit/phpunit": "8.5.52",
|
||||
"rawr/phpunit-data-provider": "3.3.1",
|
||||
"rector/rector": "1.2.10 || 2.2.8",
|
||||
"rector/type-perfect": "1.0.0 || 2.1.0",
|
||||
"squizlabs/php_codesniffer": "4.0.1",
|
||||
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.4.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Rule/Rule.php",
|
||||
"src/RuleSet/RuleContainer.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
},
|
||||
{
|
||||
"name": "Oliver Klee",
|
||||
"email": "github@oliverklee.de"
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"keywords": [
|
||||
"css",
|
||||
"parser",
|
||||
"stylesheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
|
||||
},
|
||||
"time": "2026-03-03T17:31:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/laravel-activitylog",
|
||||
"version": "4.12.3",
|
||||
@@ -6021,6 +6400,149 @@
|
||||
],
|
||||
"time": "2026-03-30T13:44:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "thecodingmachine/safe",
|
||||
"version": "v3.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thecodingmachine/safe.git",
|
||||
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||
"reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||
"phpstan/phpstan": "^2",
|
||||
"phpunit/phpunit": "^10",
|
||||
"squizlabs/php_codesniffer": "^3.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/special_cases.php",
|
||||
"generated/apache.php",
|
||||
"generated/apcu.php",
|
||||
"generated/array.php",
|
||||
"generated/bzip2.php",
|
||||
"generated/calendar.php",
|
||||
"generated/classobj.php",
|
||||
"generated/com.php",
|
||||
"generated/cubrid.php",
|
||||
"generated/curl.php",
|
||||
"generated/datetime.php",
|
||||
"generated/dir.php",
|
||||
"generated/eio.php",
|
||||
"generated/errorfunc.php",
|
||||
"generated/exec.php",
|
||||
"generated/fileinfo.php",
|
||||
"generated/filesystem.php",
|
||||
"generated/filter.php",
|
||||
"generated/fpm.php",
|
||||
"generated/ftp.php",
|
||||
"generated/funchand.php",
|
||||
"generated/gettext.php",
|
||||
"generated/gmp.php",
|
||||
"generated/gnupg.php",
|
||||
"generated/hash.php",
|
||||
"generated/ibase.php",
|
||||
"generated/ibmDb2.php",
|
||||
"generated/iconv.php",
|
||||
"generated/image.php",
|
||||
"generated/imap.php",
|
||||
"generated/info.php",
|
||||
"generated/inotify.php",
|
||||
"generated/json.php",
|
||||
"generated/ldap.php",
|
||||
"generated/libxml.php",
|
||||
"generated/lzf.php",
|
||||
"generated/mailparse.php",
|
||||
"generated/mbstring.php",
|
||||
"generated/misc.php",
|
||||
"generated/mysql.php",
|
||||
"generated/mysqli.php",
|
||||
"generated/network.php",
|
||||
"generated/oci8.php",
|
||||
"generated/opcache.php",
|
||||
"generated/openssl.php",
|
||||
"generated/outcontrol.php",
|
||||
"generated/pcntl.php",
|
||||
"generated/pcre.php",
|
||||
"generated/pgsql.php",
|
||||
"generated/posix.php",
|
||||
"generated/ps.php",
|
||||
"generated/pspell.php",
|
||||
"generated/readline.php",
|
||||
"generated/rnp.php",
|
||||
"generated/rpminfo.php",
|
||||
"generated/rrd.php",
|
||||
"generated/sem.php",
|
||||
"generated/session.php",
|
||||
"generated/shmop.php",
|
||||
"generated/sockets.php",
|
||||
"generated/sodium.php",
|
||||
"generated/solr.php",
|
||||
"generated/spl.php",
|
||||
"generated/sqlsrv.php",
|
||||
"generated/ssdeep.php",
|
||||
"generated/ssh2.php",
|
||||
"generated/stream.php",
|
||||
"generated/strings.php",
|
||||
"generated/swoole.php",
|
||||
"generated/uodbc.php",
|
||||
"generated/uopz.php",
|
||||
"generated/url.php",
|
||||
"generated/var.php",
|
||||
"generated/xdiff.php",
|
||||
"generated/xml.php",
|
||||
"generated/xmlrpc.php",
|
||||
"generated/yaml.php",
|
||||
"generated/yaz.php",
|
||||
"generated/zip.php",
|
||||
"generated/zlib.php"
|
||||
],
|
||||
"classmap": [
|
||||
"lib/DateTime.php",
|
||||
"lib/DateTimeImmutable.php",
|
||||
"lib/Exceptions/",
|
||||
"generated/Exceptions/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
||||
"support": {
|
||||
"issues": "https://github.com/thecodingmachine/safe/issues",
|
||||
"source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/OskarStark",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/shish",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/silasjoisten",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/staabm",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-02-04T18:08:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tijsverkoyen/css-to-inline-styles",
|
||||
"version": "v2.4.0",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->string('thumbnail_path')->nullable()->after('pdf_path');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->dropColumn('thumbnail_path');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('territori', 'thumbnail_path')) {
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->string('thumbnail_path')->nullable()->after('pdf_path');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('territori', 'thumbnail_path')) {
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->dropColumn('thumbnail_path');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('territori', 'thumbnail_path')) {
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->string('thumbnail_path')->nullable()->after('pdf_path');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('territori', 'thumbnail_path')) {
|
||||
Schema::table('territori', function (Blueprint $table) {
|
||||
$table->dropColumn('thumbnail_path');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ services:
|
||||
dockerfile: docker/php/Dockerfile
|
||||
container_name: termanager2_app
|
||||
restart: unless-stopped
|
||||
user: "1000:1000"
|
||||
working_dir: /var/www/html
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
|
||||
@@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \
|
||||
zip \
|
||||
unzip \
|
||||
supervisor \
|
||||
poppler-utils \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install \
|
||||
pdo_mysql \
|
||||
|
||||
@@ -70,7 +70,9 @@ mkdir -p storage/framework/{cache,sessions,views}
|
||||
mkdir -p storage/logs
|
||||
mkdir -p storage/app
|
||||
mkdir -p bootstrap/cache
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
fi
|
||||
|
||||
# -----------------------------------------------
|
||||
# 1. .env file (must exist before composer/artisan)
|
||||
|
||||
@@ -82,6 +82,14 @@
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
@can('territori.assign')
|
||||
<a href="{{ route('assegnazioni.assegna') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('assegnazioni.assegna') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
Assegnazioni
|
||||
</a>
|
||||
@endcan
|
||||
|
||||
@can('campagne.manage')
|
||||
<a href="{{ route('campagne.index') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('campagne.*') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}">
|
||||
@@ -129,6 +137,11 @@
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A2 2 0 013 12V7a4 4 0 014-4z"/></svg>
|
||||
Tipologie
|
||||
</a>
|
||||
<a href="{{ route('xml.exchange') }}"
|
||||
class="flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-lg {{ request()->routeIs('xml.*') ? 'bg-indigo-50 text-indigo-700' : 'text-gray-700 hover:bg-gray-100' }}">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v4m0 0l-3-3m3 3l3-3M4 12a8 8 0 1116 0v1a3 3 0 01-3 3h-1M7 16H6a2 2 0 01-2-2v-2"/></svg>
|
||||
XML Exchange
|
||||
</a>
|
||||
</div>
|
||||
@endcan
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Assegna Territorio</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Assegnazioni</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
|
||||
<a href="{{ route('territori.index') }}" class="text-sm text-indigo-600 hover:text-indigo-800">← Torna ai territori</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 max-w-lg">
|
||||
<form wire:submit="save" class="space-y-4">
|
||||
<div>
|
||||
@if(!$preselectedTerritorioId)
|
||||
<label for="territorio_search" class="block text-sm font-medium text-gray-700">Filtra territori</label>
|
||||
<input wire:model.live.debounce.300ms="territorioSearch"
|
||||
type="text"
|
||||
id="territorio_search"
|
||||
placeholder="Cerca per numero, zona o tipologia"
|
||||
class="mt-1 mb-2 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
@endif
|
||||
|
||||
<label for="territorio_id" class="block text-sm font-medium text-gray-700">Territorio *</label>
|
||||
<select wire:model="territorio_id" id="territorio_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" @if($preselectedTerritorioId) disabled @endif>
|
||||
<select wire:model.live="territorio_id" id="territorio_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" @if($preselectedTerritorioId) disabled @endif>
|
||||
<option value="">Seleziona un territorio</option>
|
||||
@foreach($territoriDisponibili as $t)
|
||||
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }} ({{ $t->tipologia?->nome }})</option>
|
||||
@@ -18,6 +28,21 @@
|
||||
<input type="hidden" wire:model="territorio_id" value="{{ $preselectedTerritorioId }}">
|
||||
@endif
|
||||
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||
|
||||
@if($territorio_id)
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-gray-500 mb-1">Anteprima territorio</p>
|
||||
@if($this->selectedThumbnailUrl)
|
||||
<img src="{{ $this->selectedThumbnailUrl }}"
|
||||
alt="Anteprima territorio"
|
||||
class="rounded-lg border border-gray-200 shadow-sm max-h-64 w-auto">
|
||||
@else
|
||||
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||
Nessun PDF disponibile per questo territorio.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Registro Assegnazioni</h1>
|
||||
@can('settings.manage')
|
||||
<button wire:click="openCreate"
|
||||
style="background:#4f46e5;color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:14px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="width:16px;height:16px;" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||
Nuova voce
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
{{-- Filters --}}
|
||||
@@ -50,6 +57,9 @@
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase">Giorni</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase">Anno</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase">Campagna</th>
|
||||
@can('settings.manage')
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase">Azioni</th>
|
||||
@endcan
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@@ -77,6 +87,18 @@
|
||||
<span class="text-gray-300">-</span>
|
||||
@endif
|
||||
</td>
|
||||
@can('settings.manage')
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<button wire:click="openEdit({{ $a->id }})"
|
||||
style="background:#e0e7ff;color:#4338ca;border:none;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;margin-right:4px;">
|
||||
Modifica
|
||||
</button>
|
||||
<button wire:click="askDelete({{ $a->id }})"
|
||||
style="background:#fee2e2;color:#b91c1c;border:none;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;">
|
||||
Elimina
|
||||
</button>
|
||||
</td>
|
||||
@endcan
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
@@ -90,4 +112,124 @@
|
||||
{{ $assegnazioni->links() }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ─── Modal Crea / Modifica (solo admin) ────────────────── --}}
|
||||
@can('settings.manage')
|
||||
@if($showModal)
|
||||
<div style="position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:50;display:flex;align-items:center;justify-content:center;padding:16px;">
|
||||
<div style="background:#fff;border-radius:12px;width:100%;max-width:560px;max-height:90vh;overflow-y:auto;padding:24px;">
|
||||
<h2 style="font-size:18px;font-weight:700;color:#111827;margin-bottom:20px;">
|
||||
{{ $editingId ? 'Modifica voce' : 'Nuova voce' }}
|
||||
</h2>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
|
||||
{{-- Territorio --}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Territorio *</label>
|
||||
<select wire:model="form_territorio_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">-- seleziona --</option>
|
||||
@foreach($territori as $t)
|
||||
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form_territorio_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Proclamatore --}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Proclamatore *</label>
|
||||
<select wire:model="form_proclamatore_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">-- seleziona --</option>
|
||||
@foreach($proclamatori as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->cognome }} {{ $p->nome }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form_proclamatore_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Anno teocratico --}}
|
||||
<div>
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Anno teocratico *</label>
|
||||
<select wire:model="form_anno_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">-- seleziona --</option>
|
||||
@foreach($anni as $anno)
|
||||
<option value="{{ $anno->id }}">{{ $anno->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form_anno_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Campagna --}}
|
||||
<div>
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Campagna</label>
|
||||
<select wire:model="form_campaign_id" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
<option value="">Nessuna</option>
|
||||
@foreach($campagne as $c)
|
||||
<option value="{{ $c->id }}">{{ $c->descrizione }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('form_campaign_id') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Data assegnazione --}}
|
||||
<div>
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Data assegnazione *</label>
|
||||
<input type="date" wire:model="form_assigned_at" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
@error('form_assigned_at') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Data rientro --}}
|
||||
<div>
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Data rientro</label>
|
||||
<input type="date" wire:model="form_returned_at" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;">
|
||||
@error('form_returned_at') <span style="color:#dc2626;font-size:12px;">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Contata in campagna --}}
|
||||
<div style="grid-column:1/-1;display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" wire:model="form_counted_in_campaign" id="ccb" style="width:16px;height:16px;cursor:pointer;">
|
||||
<label for="ccb" style="font-size:14px;color:#374151;cursor:pointer;">Contata in campagna</label>
|
||||
</div>
|
||||
|
||||
{{-- Note --}}
|
||||
<div style="grid-column:1/-1;">
|
||||
<label style="font-size:13px;font-weight:600;color:#374151;display:block;margin-bottom:4px;">Note</label>
|
||||
<textarea wire:model="form_note" rows="2" style="width:100%;border:1px solid #d1d5db;border-radius:8px;padding:8px 10px;font-size:14px;resize:vertical;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;margin-top:20px;">
|
||||
<button wire:click="$set('showModal', false)"
|
||||
style="background:#f3f4f6;color:#374151;border:none;border-radius:8px;padding:8px 18px;font-size:14px;cursor:pointer;">
|
||||
Annulla
|
||||
</button>
|
||||
<button wire:click="save"
|
||||
style="background:#4f46e5;color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:14px;cursor:pointer;">
|
||||
{{ $editingId ? 'Salva modifiche' : 'Crea voce' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ─── Conferma eliminazione (solo admin) ─────────────────── --}}
|
||||
@if($showDeleteConfirm)
|
||||
<div style="position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:50;display:flex;align-items:center;justify-content:center;padding:16px;">
|
||||
<div style="background:#fff;border-radius:12px;width:100%;max-width:360px;padding:24px;">
|
||||
<h2 style="font-size:17px;font-weight:700;color:#111827;margin-bottom:10px;">Elimina voce</h2>
|
||||
<p style="font-size:14px;color:#6b7280;margin-bottom:20px;">Sei sicuro di voler eliminare questa assegnazione? L'operazione non può essere annullata.</p>
|
||||
<div style="display:flex;justify-content:flex-end;gap:10px;">
|
||||
<button wire:click="$set('showDeleteConfirm', false)"
|
||||
style="background:#f3f4f6;color:#374151;border:none;border-radius:8px;padding:8px 18px;font-size:14px;cursor:pointer;">
|
||||
Annulla
|
||||
</button>
|
||||
<button wire:click="deleteConfirmed"
|
||||
style="background:#dc2626;color:#fff;border:none;border-radius:8px;padding:8px 18px;font-size:14px;cursor:pointer;">
|
||||
Elimina
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
<h1 class="text-2xl font-bold text-gray-900">Impostazioni</h1>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 bg-white rounded-xl shadow-sm border border-gray-200 p-4 max-w-lg">
|
||||
<p class="text-sm font-medium text-gray-700 mb-3">Import / Export dati XML</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('xml.exchange') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-lg hover:bg-amber-700 transition">
|
||||
Import XML
|
||||
</a>
|
||||
<a href="{{ route('xml.exchange') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg hover:bg-emerald-700 transition">
|
||||
Export XML
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">I pulsanti aprono la sezione XML Exchange con gli strumenti di conversione, import e export.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 max-w-lg">
|
||||
@if (session()->has('success'))
|
||||
<div x-data="{ show: true }" x-show="show" x-init="setTimeout(() => show = false, 3000)" x-transition.duration.500ms
|
||||
|
||||
121
resources/views/livewire/settings/xml-exchange.blade.php
Normal file
121
resources/views/livewire/settings/xml-exchange.blade.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Import/Export XML</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Converti dump SQL legacy in XML, importa XML nell'app ed esporta i dati correnti in XML.</p>
|
||||
</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>
|
||||
|
||||
<div>
|
||||
<input wire:model="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">
|
||||
<button
|
||||
wire:click="convertLegacySqlToXml"
|
||||
type="button"
|
||||
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>
|
||||
</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>
|
||||
|
||||
<div wire:loading wire:target="importXmlIntoApp" style="padding:10px 12px;border-radius:10px;background:#fffbeb;border:1px solid #f59e0b;color:#92400e;font-size:13px;">
|
||||
Importazione in corso... attendi il completamento.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input wire:model="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">
|
||||
<button
|
||||
wire:click="importXmlIntoApp"
|
||||
type="button"
|
||||
onclick="if(!confirm('Confermi l\'import XML? I dati gestionali correnti verranno sostituiti.')) event.stopImmediatePropagation();"
|
||||
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>
|
||||
</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>
|
||||
66
resources/views/pdf/import-log.blade.php
Normal file
66
resources/views/pdf/import-log.blade.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Log Import XML</title>
|
||||
<style>
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; color: #111; margin: 20px; }
|
||||
h1 { font-size: 17px; margin-bottom: 4px; }
|
||||
.meta { font-size: 11px; color: #555; margin-bottom: 16px; }
|
||||
.stats-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
||||
.stat { background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 6px; padding: 8px 12px; min-width: 180px; }
|
||||
.stat-label { font-size: 11px; color: #6b7280; }
|
||||
.stat-value { font-size: 14px; font-weight: bold; color: #1f2937; }
|
||||
h2 { font-size: 14px; margin-top: 20px; margin-bottom: 8px; border-bottom: 1px solid #e5e7eb; padding-bottom: 4px; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 11px; }
|
||||
thead tr { background: #f9fafb; }
|
||||
th { text-align: left; padding: 7px 8px; border-bottom: 1px solid #d1d5db; font-size: 11px; color: #374151; }
|
||||
td { padding: 6px 8px; border-bottom: 1px solid #f3f4f6; vertical-align: top; }
|
||||
tr:nth-child(even) td { background: #f9fafb; }
|
||||
.badge-err { color: #b91c1c; font-weight: bold; }
|
||||
.footer { margin-top: 30px; font-size: 10px; color: #9ca3af; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Log Import XML — TerManager2</h1>
|
||||
<div class="meta">Generato il {{ $generatedAt }}</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat"><div class="stat-label">Zone importate</div><div class="stat-value">{{ $stats['zone_importate'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Tipologie importate</div><div class="stat-value">{{ $stats['tipologie_importate'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Proclamatori importati</div><div class="stat-value">{{ $stats['proclamatori_importati'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Territori importati</div><div class="stat-value">{{ $stats['territori_importati'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Anni teocratici</div><div class="stat-value">{{ $stats['anni_importati'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Campagne importate</div><div class="stat-value">{{ $stats['campagne_importate'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Assegnazioni importate</div><div class="stat-value">{{ $stats['assegnazioni_importate'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Territori duplicati saltati</div><div class="stat-value badge-err">{{ $stats['duplicate_territori'] ?? 0 }}</div></div>
|
||||
<div class="stat"><div class="stat-label">Assegnazioni saltate</div><div class="stat-value badge-err">{{ $stats['assegnazioni_saltate'] ?? 0 }}</div></div>
|
||||
</div>
|
||||
|
||||
@if(!empty($issues))
|
||||
<h2>Dettaglio righe non importate</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:18%">Entità</th>
|
||||
<th style="width:14%">Legacy ID</th>
|
||||
<th>Motivo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($issues as $issue)
|
||||
<tr>
|
||||
<td>{{ $issue['entity'] }}</td>
|
||||
<td>{{ $issue['legacy_id'] }}</td>
|
||||
<td>{{ $issue['reason'] }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<p style="color:#16a34a;margin-top:12px;">✓ Nessun elemento saltato durante l'importazione.</p>
|
||||
@endif
|
||||
|
||||
<div class="footer">TerManager2 — Export generato automaticamente</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,6 +25,7 @@ use App\Livewire\Settings\SettingsEdit;
|
||||
use App\Livewire\Settings\ZoneIndex;
|
||||
use App\Livewire\Settings\TipologieIndex;
|
||||
use App\Livewire\Settings\UsersIndex;
|
||||
use App\Livewire\Settings\XmlExchange;
|
||||
use App\Livewire\Privacy;
|
||||
|
||||
/*
|
||||
@@ -105,6 +106,7 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('utenti', UsersIndex::class)->name('users.index');
|
||||
Route::get('zone', ZoneIndex::class)->name('zone.index');
|
||||
Route::get('tipologie', TipologieIndex::class)->name('tipologie.index');
|
||||
Route::get('xml-exchange', XmlExchange::class)->name('xml.exchange');
|
||||
});
|
||||
|
||||
// Privacy / Informativa GDPR
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Reference in New Issue
Block a user