Revisione blocchi 2 e 3: file protetti, ripristino atomico, import sicuri
Blocco 2 - Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash che comparivano alla pagina dopo. - PDF, miniature e versioni dei territori non più serviti da nginx su /storage: TerritorioFileController con login, permessi e cache privata. - Ripristino tutto o niente: verifica dell'archivio, una transazione con import, piantine, blocco account e scambio dei file annullabile. - Backup formato 2 con piantine, storico PDF e area KML. - Posto unico preso in un passo (Cache::lock) e rispettato anche da import XML e PDF. - Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui), caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico. - L'import XML elimina i file dei territori che non usa più; comando territori:file-orfani per quelli accumulati prima. Blocco 3 - Password attuale richiesta al cambio volontario; AuthenticateSession chiude le sessioni aperte altrove. - Niente nomi dei proclamatori in chiaro nel registro attività. - Export XML senza remember_token; chi riceve l'hash dal file deve cambiare password. - Campagne: ultimo giorno incluso, percentuale sui territori distinti. - Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru, impostazioni rilette a ogni job. - Azzeramento che cancella anche piantine, area KML e anteprime. - Pulsanti profilo e menu che non si nascondevano; icona del sito. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,11 @@ class BackupManager extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_BACKUP);
|
||||
$operationId = $this->nuovaOperazione(BackupOperationState::TIPO_BACKUP);
|
||||
if ($operationId === null) {
|
||||
return;
|
||||
}
|
||||
$this->operationId = $operationId;
|
||||
|
||||
CreateBackupArchive::dispatch($this->operationId, false);
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use Livewire\Component;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SettingsEdit extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
public string $congregazione_nome = '';
|
||||
public int $giorni_giacenza_da_assegnare = 120;
|
||||
public int $giorni_giacenza_prioritari = 180;
|
||||
@@ -55,7 +57,7 @@ class SettingsEdit extends Component
|
||||
'audit_retention_days' => $this->audit_retention_days,
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Impostazioni aggiornate.');
|
||||
$this->avvisa('Impostazioni aggiornate.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use Livewire\Component;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
class TipologieIndex extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
public string $nuovaTipologia = '';
|
||||
public ?int $editingId = null;
|
||||
public string $editingNome = '';
|
||||
@@ -16,7 +18,7 @@ class TipologieIndex extends Component
|
||||
$this->validate(['nuovaTipologia' => 'required|string|max:100|unique:tipologie,nome']);
|
||||
Tipologia::create(['nome' => $this->nuovaTipologia, 'attivo' => true]);
|
||||
$this->nuovaTipologia = '';
|
||||
session()->flash('success', 'Tipologia aggiunta.');
|
||||
$this->avvisa('Tipologia aggiunta.');
|
||||
}
|
||||
|
||||
public function startEdit(int $id)
|
||||
@@ -33,7 +35,7 @@ class TipologieIndex extends Component
|
||||
$tipo->update(['nome' => $this->editingNome]);
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
session()->flash('success', 'Tipologia aggiornata.');
|
||||
$this->avvisa('Tipologia aggiornata.');
|
||||
}
|
||||
|
||||
public function cancelEdit()
|
||||
@@ -52,11 +54,11 @@ class TipologieIndex extends Component
|
||||
{
|
||||
$tipo = Tipologia::findOrFail($id);
|
||||
if ($tipo->territori()->exists()) {
|
||||
session()->flash('error', "Impossibile eliminare: la tipologia '{$tipo->nome}' ha territori associati.");
|
||||
$this->avvisaErrore("Impossibile eliminare: la tipologia '{$tipo->nome}' ha territori associati.");
|
||||
return;
|
||||
}
|
||||
$tipo->delete();
|
||||
session()->flash('success', "Tipologia '{$tipo->nome}' eliminata.");
|
||||
$this->avvisa("Tipologia '{$tipo->nome}' eliminata.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -13,6 +14,7 @@ use Spatie\Permission\Models\Role;
|
||||
|
||||
class UsersIndex extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
@@ -83,7 +85,7 @@ class UsersIndex extends Component
|
||||
if (! empty($this->availableRoles)) {
|
||||
$this->selectedRole = $this->availableRoles[0];
|
||||
}
|
||||
session()->flash('success', 'Utente creato con successo.');
|
||||
$this->avvisa('Utente creato con successo.');
|
||||
}
|
||||
|
||||
public function startEdit(int $userId): void
|
||||
@@ -147,7 +149,7 @@ class UsersIndex extends Component
|
||||
$user->syncRoles([$validated['editSelectedRole']]);
|
||||
|
||||
$this->cancelEdit();
|
||||
session()->flash('success', 'Utente aggiornato con successo.');
|
||||
$this->avvisa('Utente aggiornato con successo.');
|
||||
}
|
||||
|
||||
public function deleteUser(int $userId): void
|
||||
@@ -156,12 +158,12 @@ class UsersIndex extends Component
|
||||
$user = User::query()->with('roles')->findOrFail($userId);
|
||||
|
||||
if (! $currentUser || $currentUser->id === $user->id) {
|
||||
session()->flash('error', 'Non puoi cancellare il tuo utente.');
|
||||
$this->avvisaErrore('Non puoi cancellare il tuo utente.');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->hasRole('amministratore') && User::role('amministratore')->count() <= 1) {
|
||||
session()->flash('error', 'Non puoi cancellare l\'ultimo amministratore.');
|
||||
$this->avvisaErrore('Non puoi cancellare l\'ultimo amministratore.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,7 +203,7 @@ class UsersIndex extends Component
|
||||
$this->cancelEdit();
|
||||
}
|
||||
|
||||
session()->flash('success', 'Utente cancellato. I log sono stati preservati.');
|
||||
$this->avvisa('Utente cancellato. I log sono stati preservati.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Jobs\ImportTerritoryPdfFolder;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Assegnazione;
|
||||
@@ -9,20 +10,27 @@ use App\Models\Campagna;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\TerritorioMappa;
|
||||
use App\Models\TerritorioPdfVersione;
|
||||
use App\Models\Tipologia;
|
||||
use App\Models\User;
|
||||
use App\Models\Zona;
|
||||
use App\Services\BackupOperationState;
|
||||
use App\Services\TerritorioPdfImportDispatcher;
|
||||
use App\Services\TerritorioPdfImportState;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class XmlExchange extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
use WithFileUploads;
|
||||
|
||||
public array $importStats = [];
|
||||
@@ -52,39 +60,149 @@ class XmlExchange extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function importTerritoryPdfFolder(): void
|
||||
/**
|
||||
* Quanti file viaggiano in una singola richiesta di caricamento della
|
||||
* cartella.
|
||||
*
|
||||
* Il browser non manda la cartella tutta insieme: la spezza in lotti e li
|
||||
* aggiunge uno dopo l'altro a $pdfFolder. Un'unica richiesta con tutti i file
|
||||
* si scontrava con `max_file_uploads` di PHP (20 per default), che non
|
||||
* rifiuta: scarta in silenzio i file in piu', e l'import partiva con i primi
|
||||
* venti di una cartella da trecento. Alzare il limite non basterebbe: la
|
||||
* richiesta unica resterebbe soggetta al tetto di dimensione e al timeout di
|
||||
* 100 secondi di Cloudflare, e un errore a meta' farebbe ricominciare da capo.
|
||||
* A lotti, ogni richiesta e' piccola e un errore ne ferma una sola.
|
||||
*
|
||||
* Si legge il valore vero di PHP, perche' un'installazione con un limite piu'
|
||||
* basso non perda file in silenzio come prima.
|
||||
*/
|
||||
public function getFilePerLottoProperty(): int
|
||||
{
|
||||
$this->validate([
|
||||
'pdfFolder' => ['required', 'array', 'min:1'],
|
||||
'pdfFolder.*' => ['file', 'mimes:pdf', 'max:10240'],
|
||||
]);
|
||||
return max(1, min(20, (int) ini_get('max_file_uploads') ?: 20));
|
||||
}
|
||||
|
||||
$importId = (string) Str::uuid();
|
||||
$storedFiles = [];
|
||||
/**
|
||||
* Controllo rapido prima di caricare la cartella: senza, si caricherebbero
|
||||
* centinaia di file per poi sentirsi dire che c'e' un ripristino in corso.
|
||||
* Non prende il posto — lo prende importTerritoryPdfFolder(), a caricamento
|
||||
* finito — perche' una scheda chiusa a meta' caricamento lo terrebbe
|
||||
* occupato per un'ora.
|
||||
*/
|
||||
public function cartellaPuoPartire(): bool
|
||||
{
|
||||
$inCorso = app(BackupOperationState::class)->corrente();
|
||||
|
||||
foreach ($this->pdfFolder as $index => $file) {
|
||||
$originalName = $file->getClientOriginalName();
|
||||
$safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME));
|
||||
$extension = strtolower($file->getClientOriginalExtension() ?: 'pdf');
|
||||
$storedPath = $file->storeAs(
|
||||
'bulk-territori-imports/' . $importId,
|
||||
str_pad((string) $index, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.' . $extension,
|
||||
'local'
|
||||
);
|
||||
if ($inCorso === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$storedFiles[] = [
|
||||
'original_name' => $originalName,
|
||||
'stored_path' => $storedPath,
|
||||
];
|
||||
$this->avvisaErrore(app(BackupOperationState::class)->messaggioOccupato($inCorso));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Il browser azzera i lotti di un caricamento precedente prima di iniziarne uno nuovo. */
|
||||
public function svuotaCartella(): void
|
||||
{
|
||||
foreach ($this->pdfFolder as $file) {
|
||||
if ($file instanceof TemporaryUploadedFile) {
|
||||
$file->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$this->pdfFolder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mette in coda l'import dei lotti gia' caricati. Restituisce true se
|
||||
* l'import e' partito: il browser tiene la cartella scelta finche' non lo e',
|
||||
* cosi' dopo un rifiuto basta ripremere.
|
||||
*/
|
||||
public function importTerritoryPdfFolder(): bool
|
||||
{
|
||||
$this->validate([
|
||||
'pdfFolder' => ['required', 'array', 'min:1'],
|
||||
]);
|
||||
|
||||
$importId = (string) Str::uuid();
|
||||
$dispatcher = app(TerritorioPdfImportDispatcher::class);
|
||||
|
||||
try {
|
||||
$dispatcher->occupaPosto($importId);
|
||||
} catch (\RuntimeException $e) {
|
||||
// Il posto l'ha preso qualcun altro fra il controllo iniziale e la
|
||||
// fine del caricamento: si avvisa, e non si tocca niente.
|
||||
$this->avvisaErrore($e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$storedFiles = [];
|
||||
$ignorati = [];
|
||||
$scartati = [];
|
||||
|
||||
try {
|
||||
foreach ($this->pdfFolder as $index => $file) {
|
||||
$originalName = $file->getClientOriginalName();
|
||||
|
||||
// Thumbs.db, .DS_Store, desktop.ini e simili: prima uno solo di
|
||||
// questi faceva fallire la validazione e con lei l'intero import.
|
||||
if (TerritorioPdfImportDispatcher::daIgnorare($originalName)) {
|
||||
$ignorati[] = $originalName;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Un PDF rotto o troppo grande si scarta da solo, con il suo
|
||||
// motivo nell'esito: non e' una ragione per buttare gli altri.
|
||||
$verifica = Validator::make(['file' => $file], ['file' => ['file', 'mimes:pdf', 'max:10240']]);
|
||||
if ($verifica->fails()) {
|
||||
$scartati[] = [
|
||||
'file' => $originalName,
|
||||
'message' => 'File non valido: ' . $verifica->errors()->first('file'),
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$safeName = Str::slug(pathinfo($originalName, PATHINFO_FILENAME));
|
||||
$storedPath = $file->storeAs(
|
||||
'bulk-territori-imports/' . $importId,
|
||||
str_pad((string) $index, 4, '0', STR_PAD_LEFT) . '-' . $safeName . '.pdf',
|
||||
'local'
|
||||
);
|
||||
|
||||
$storedFiles[] = [
|
||||
'original_name' => $originalName,
|
||||
'stored_path' => $storedPath,
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$dispatcher->liberaPosto($importId, 'Import PDF non avviato: ' . $e->getMessage());
|
||||
Storage::disk('local')->deleteDirectory('bulk-territori-imports/' . $importId);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->svuotaCartella();
|
||||
|
||||
if ($storedFiles === []) {
|
||||
$dispatcher->liberaPosto($importId, 'Nessun PDF da importare.');
|
||||
$this->avvisaErrore('Nella cartella non c\'è nessun PDF da importare'
|
||||
. ($ignorati !== [] ? ' (ignorati ' . count($ignorati) . ' file di sistema o di altro tipo)' : '')
|
||||
. ($scartati !== [] ? ' (' . count($scartati) . ' PDF non validi: ' . implode(', ', array_column($scartati, 'file')) . ')' : '')
|
||||
. '.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->dispatchPdfImport(
|
||||
$importId,
|
||||
$storedFiles,
|
||||
'Cartella ricevuta: ' . count($storedFiles) . ' PDF in coda.'
|
||||
'Cartella ricevuta: ' . count($storedFiles) . ' PDF in coda.',
|
||||
$ignorati,
|
||||
$scartati
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function refreshPdfImportStatus(): void
|
||||
@@ -114,8 +232,9 @@ class XmlExchange extends Component
|
||||
* contenuti non possono essere ri-renderizzati dal server senza cancellare
|
||||
* le righe scritte dal browser, quindi si comunica a eventi. E' lo stesso
|
||||
* meccanismo di SegueOperazioneBackup, da cui questo componente non eredita
|
||||
* perche' l'import PDF ha uno stato tutto suo (TerritorioPdfImportState) e
|
||||
* non partecipa al posto unico: puo' girare mentre un backup lavora.
|
||||
* perche' l'import PDF ha uno stato tutto suo (TerritorioPdfImportState).
|
||||
* Del posto unico usa solo l'occupazione (BackupOperationState::occupa()):
|
||||
* non gira mai insieme a un backup, un ripristino o un azzeramento.
|
||||
*/
|
||||
protected function spedisciImportAllaConsole(): void
|
||||
{
|
||||
@@ -156,10 +275,10 @@ class XmlExchange extends Component
|
||||
return in_array($this->pdfImportStatus, ['queued', 'running'], true);
|
||||
}
|
||||
|
||||
protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog): void
|
||||
protected function dispatchPdfImport(string $importId, array $storedFiles, string $initialLog, array $ignorati = [], array $scartati = []): void
|
||||
{
|
||||
$state = app(TerritorioPdfImportDispatcher::class)
|
||||
->dispatchStoredFiles($importId, $storedFiles, auth()->id(), $initialLog);
|
||||
->dispatchStoredFiles($importId, $storedFiles, auth()->id(), $initialLog, $ignorati, $scartati);
|
||||
|
||||
$this->currentPdfImportId = $importId;
|
||||
$this->pdfImportStatus = $state['status'] ?? 'queued';
|
||||
@@ -172,13 +291,13 @@ class XmlExchange extends Component
|
||||
$this->logPdfSpediti = 0;
|
||||
$this->refreshPdfImportStatus();
|
||||
|
||||
session()->flash('success', 'Import PDF avviato in background. L\'avanzamento è nella console in fondo alla pagina.');
|
||||
$this->avvisa('Import PDF avviato in background. L\'avanzamento è nella console in fondo alla pagina.');
|
||||
}
|
||||
|
||||
public function downloadImportLogPdf()
|
||||
{
|
||||
if (empty($this->importStats)) {
|
||||
session()->flash('error', 'Nessun log da scaricare. Esegui prima un import XML.');
|
||||
$this->avvisaErrore('Nessun log da scaricare. Esegui prima un import XML.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,6 +355,67 @@ class XmlExchange extends Component
|
||||
return $this->datasetToXml($dataset, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* I file sul disco `public` a cui puntano i territori e il loro storico PDF.
|
||||
*
|
||||
* Serve all'import XML per sapere, dopo, quali file ha reso orfani. L'import
|
||||
* cancella tutti i territori e li ricrea con id nuovi; i PDF portano l'id nel
|
||||
* nome (territorio-142-141.pdf), quindi un file che arriva da un'altra
|
||||
* installazione, o da un dump della vecchia applicazione senza PDF, lascia i
|
||||
* file dei territori cancellati sul disco: nessuna schermata li mostra,
|
||||
* nessuna cancellazione li raggiunge, e finiscono dentro ogni backup
|
||||
* successivo. E' cosi' che la cartella territori-pdf si era trovata con due
|
||||
* PDF per territorio.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function fileDeiTerritori(): array
|
||||
{
|
||||
return Territorio::withTrashed()->get(['pdf_path', 'thumbnail_path'])
|
||||
->concat(TerritorioPdfVersione::query()->get(['pdf_path', 'thumbnail_path']))
|
||||
->flatMap(fn ($riga) => [$riga->pdf_path, $riga->thumbnail_path])
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina i file che prima dell'import appartenevano a un territorio e che
|
||||
* adesso nessuna riga richiama piu'.
|
||||
*
|
||||
* Si toccano solo i file elencati in $fileDiPrima: un file sul disco che
|
||||
* nessuno conosceva non e' affare dell'import. E prima di cancellare si
|
||||
* ricontrolla ogni riferimento possibile — anche logo e piantine — cosi' un
|
||||
* XML che riporta gli stessi percorsi (un export di questa stessa
|
||||
* installazione) non perde niente.
|
||||
*
|
||||
* @param array<int, string> $fileDiPrima
|
||||
*/
|
||||
public static function eliminaFileNonPiuUsati(array $fileDiPrima): int
|
||||
{
|
||||
$usati = array_flip(array_merge(
|
||||
self::fileDeiTerritori(),
|
||||
array_filter([Setting::instance()->logo_path]),
|
||||
TerritorioMappa::query()->whereNotNull('image_path')->pluck('image_path')->all(),
|
||||
));
|
||||
|
||||
$disco = Storage::disk('public');
|
||||
$eliminati = 0;
|
||||
|
||||
foreach ($fileDiPrima as $percorso) {
|
||||
if (isset($usati[$percorso]) || ! $disco->exists($percorso)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($disco->delete($percorso)) {
|
||||
$eliminati++;
|
||||
}
|
||||
}
|
||||
|
||||
return $eliminati;
|
||||
}
|
||||
|
||||
public function importXmlFromContent(string $content): array
|
||||
{
|
||||
$this->importStats = [];
|
||||
@@ -370,14 +550,20 @@ class XmlExchange extends Component
|
||||
// BCRYPT_ROUNDS), and Eloquent does not stamp its own timestamps.
|
||||
// Per chi sta importando password e token restano i suoi:
|
||||
// quelli del file possono essere di mesi fa.
|
||||
//
|
||||
// Chi riceve l'hash dal file deve scegliere una password nuova,
|
||||
// come dopo un ripristino: chi ha avuto il file fra le mani ha
|
||||
// potuto tentarne il crack con calma. Il "ricordami" si azzera
|
||||
// e basta: non si esporta più, e quello di un file vecchio
|
||||
// sarebbe una credenziale persistente regalata a chi lo tiene.
|
||||
$meta = [];
|
||||
$passwordHash = (string) ($userNode->password ?? '');
|
||||
if ($passwordHash !== '' && ! $eAttore) {
|
||||
$meta['password'] = $passwordHash;
|
||||
$meta['password_change_required'] = true;
|
||||
}
|
||||
if (! $eAttore) {
|
||||
$rememberToken = (string) ($userNode->remember_token ?? '');
|
||||
$meta['remember_token'] = $rememberToken !== '' ? $rememberToken : null;
|
||||
$meta['remember_token'] = null;
|
||||
}
|
||||
$meta['email_verified_at'] = $this->normalizeDateTimeForDb((string) ($userNode->email_verified_at ?? ''));
|
||||
|
||||
@@ -620,7 +806,8 @@ class XmlExchange extends Component
|
||||
'email' => (string) $u->email,
|
||||
'password' => (string) $u->getAuthPassword(),
|
||||
'email_verified_at' => optional($u->email_verified_at)->toIso8601String() ?? '',
|
||||
'remember_token' => (string) ($u->getRememberToken() ?? ''),
|
||||
// remember_token no: all'import si azzera comunque, e nel file
|
||||
// sarebbe solo una credenziale in più per chi lo tiene.
|
||||
'roles' => $u->roles->pluck('name')->all(),
|
||||
'created_at' => optional($u->created_at)->toIso8601String() ?? '',
|
||||
'updated_at' => optional($u->updated_at)->toIso8601String() ?? '',
|
||||
@@ -1030,7 +1217,6 @@ class XmlExchange extends Component
|
||||
$this->addXmlText($node, 'email', (string) ($user['email'] ?? ''));
|
||||
$this->addXmlText($node, 'password', (string) ($user['password'] ?? ''));
|
||||
$node->addChild('email_verified_at', (string) ($user['email_verified_at'] ?? ''));
|
||||
$this->addXmlText($node, 'remember_token', (string) ($user['remember_token'] ?? ''));
|
||||
$node->addChild('created_at', (string) ($user['created_at'] ?? ''));
|
||||
$node->addChild('updated_at', (string) ($user['updated_at'] ?? ''));
|
||||
$rolesNode = $node->addChild('roles');
|
||||
|
||||
@@ -98,8 +98,13 @@ class ZonaPericolosa extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
|
||||
if ($operationId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->confermaReset = '';
|
||||
$this->operationId = $this->nuovaOperazione(BackupOperationState::TIPO_RESET);
|
||||
$this->operationId = $operationId;
|
||||
|
||||
CreateBackupArchive::dispatch($this->operationId, true);
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use Livewire\Component;
|
||||
use App\Models\Zona;
|
||||
|
||||
class ZoneIndex extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
public string $nuovaZona = '';
|
||||
public ?int $editingId = null;
|
||||
public string $editingNome = '';
|
||||
@@ -16,7 +18,7 @@ class ZoneIndex extends Component
|
||||
$this->validate(['nuovaZona' => 'required|string|max:100|unique:zone,nome']);
|
||||
Zona::create(['nome' => $this->nuovaZona, 'attivo' => true]);
|
||||
$this->nuovaZona = '';
|
||||
session()->flash('success', 'Zona aggiunta.');
|
||||
$this->avvisa('Zona aggiunta.');
|
||||
}
|
||||
|
||||
public function startEdit(int $id)
|
||||
@@ -33,7 +35,7 @@ class ZoneIndex extends Component
|
||||
$zona->update(['nome' => $this->editingNome]);
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
session()->flash('success', 'Zona aggiornata.');
|
||||
$this->avvisa('Zona aggiornata.');
|
||||
}
|
||||
|
||||
public function cancelEdit()
|
||||
@@ -52,11 +54,11 @@ class ZoneIndex extends Component
|
||||
{
|
||||
$zona = Zona::findOrFail($id);
|
||||
if ($zona->territori()->exists()) {
|
||||
session()->flash('error', "Impossibile eliminare: la zona '{$zona->nome}' ha territori associati.");
|
||||
$this->avvisaErrore("Impossibile eliminare: la zona '{$zona->nome}' ha territori associati.");
|
||||
return;
|
||||
}
|
||||
$zona->delete();
|
||||
session()->flash('success', "Zona '{$zona->nome}' eliminata.");
|
||||
$this->avvisa("Zona '{$zona->nome}' eliminata.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
Reference in New Issue
Block a user