diff --git a/.gitignore b/.gitignore
index ff78a90..5de87f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/README.md b/README.md
index 185718d..eb5305d 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app/Livewire/Assegnazioni/Assegna.php b/app/Livewire/Assegnazioni/Assegna.php
index 855b677..c8ca587 100644
--- a/app/Livewire/Assegnazioni/Assegna.php
+++ b/app/Livewire/Assegnazioni/Assegna.php
@@ -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();
diff --git a/app/Livewire/Registro.php b/app/Livewire/Registro.php
index 3e5951a..fa18103 100644
--- a/app/Livewire/Registro.php
+++ b/app/Livewire/Registro.php
@@ -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(),
]);
}
}
diff --git a/app/Livewire/Settings/XmlExchange.php b/app/Livewire/Settings/XmlExchange.php
new file mode 100644
index 0000000..3d33462
--- /dev/null
+++ b/app/Livewire/Settings/XmlExchange.php
@@ -0,0 +1,714 @@
+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('
Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.
← Torna ai territori