Files
termanager2/routes/web.php
Francesco Picone 9c37c1ad7b Piantine staccate dai territori, editor a schermo intero, restyle
Le piantine non dipendono piu' da un territorio esistente: territorio_id e'
nullable, la chiave esterna e' nullOnDelete invece di cascadeOnDelete, e `nome`
fa da etichetta finche' il collegamento manca. Si disegna guardando le strade e
si decide dopo di chi e' il disegno; se il territorio non c'e', lo si crea
dall'editor con numero, zona, tipologia e confini — zona e tipologia comprese,
create sul posto quando mancano.

Il cambio di cascata chiude anche una perdita di dati silenziosa: ogni import
XML svuota `territori` in blocco e finora si portava via tutte le piantine.

Il disegno passa su /piantine/editor, a schermo intero con telaio proprio: su
una mappa larga un terzo di schermo un confine si sbaglia, e finisce stampato.
/piantine resta la gestione — area, generazione, collegamenti, storico.

Cartolina S-12 reimpaginata: pagina 1 il modulo con la Localita' (che ora e' il
nome della zona, non un campo duplicato) e i confini scritti nello spazio
centrale, con il corpo del testo che si adatta alla lunghezza; pagina 2 la
piantina sola al 98%. Aggiunta l'anteprima, che compone il PDF al volo senza
toccare pdf_path ne' lo storico.

Immagine: JPEG invece di PNG (0,9 MB contro 5 per territorio), composta alle
tessere dello zoom piu' alto e ridotta con Lanczos, nelle proporzioni della
pagina cosi' riempie il foglio invece di lasciare fasce bianche. Il tratto del
confine e' espresso in pixel dell'immagine finale: si vede il confine e si legge
la via che ci passa sotto. L'attribuzione OSM si dimensiona con l'immagine,
come la ODbL richiede.

Rimossa la rigenerazione massiva: job, stato in cache e interfaccia.

Sistema visivo ripassato su tutta l'applicazione: le scale gray e indigo di
Tailwind sono ridefinite nei token, cosi' le 37 viste si riallineano insieme
invece di essere riscritte una per una. Grigi caldi, un accento solo con quattro
significati, carattere servito dall'applicazione e non da Google Fonts, via il
gradiente della testata. Aggiunto il link "salta al contenuto", che mancava.

Corretto un guasto d'ambiente che bloccava TUTTI gli upload in sviluppo: dalla
1.26 il fastcgi_params di nginx passa HTTP_HOST come $host, che perde la porta,
e ogni URL firmato smetteva di validare — l'endpoint di upload di Livewire
rispondeva 401.

294 test verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 16:41:22 +02:00

198 lines
9.9 KiB
PHP

<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Settings\BackupController;
use App\Http\Controllers\Settings\TerritoryPdfImportController;
use App\Http\Controllers\Settings\XmlExchangeUploadController;
use App\Http\Controllers\AssignmentPdfController;
use App\Http\Controllers\ShortPdfLinkController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\PasswordChangeController;
use App\Livewire\Home;
use App\Livewire\Territori\TerritorioIndex;
use App\Livewire\Territori\TerritorioCreate;
use App\Livewire\Territori\TerritorioShow;
use App\Livewire\Territori\TerritorioEdit;
use App\Livewire\Territori\TerritorioCestino;
use App\Livewire\Proclamatori\ProclamatoreIndex;
use App\Livewire\Proclamatori\ProclamatoreCreate;
use App\Livewire\Proclamatori\ProclamatoreShow;
use App\Livewire\Proclamatori\ProclamatoreEdit;
use App\Livewire\Proclamatori\ProclamatoreCestino;
use App\Livewire\Assegnazioni\Assegna;
use App\Livewire\Assegnazioni\Rientra;
use App\Livewire\Campagne\CampagnaIndex;
use App\Livewire\Campagne\CampagnaCreate;
use App\Livewire\Campagne\CampagnaShow;
use App\Livewire\Campagne\CampagnaEdit;
use App\Livewire\Registro;
use App\Livewire\AuditLog;
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\Http\Controllers\Settings\PiantinaAnteprimaController;
use App\Livewire\Piantine\PiantineEditor;
use App\Livewire\Piantine\PiantineManager;
use App\Livewire\Privacy;
use App\Livewire\Profile;
/*
|--------------------------------------------------------------------------
| Authentication Routes
|--------------------------------------------------------------------------
*/
Route::middleware('guest')->group(function () {
Route::get('login', App\Livewire\Auth\Login::class)->name('login');
Route::post('login', LoginController::class)->name('login.store');
});
Route::post('logout', function () {
activity()->causedBy(auth()->user())->log('logout');
auth()->logout();
request()->session()->invalidate();
request()->session()->regenerateToken();
return redirect('/login');
})->middleware('auth')->name('logout');
Route::get('p/{code}', ShortPdfLinkController::class)->name('assignments.pdf.short');
Route::get('assegnazioni/pdf/{assignment}/{code}', [AssignmentPdfController::class, 'viewer'])
->name('assignments.pdf.viewer');
Route::get('assegnazioni/pdf/{assignment}/{code}/file', [AssignmentPdfController::class, 'file'])
->name('assignments.pdf.file');
/*
|--------------------------------------------------------------------------
| Authenticated Routes
|--------------------------------------------------------------------------
*/
Route::middleware('auth')->group(function () {
// Home Dashboard
Route::get('/', Home::class)->name('dashboard');
// Profilo personale (ogni utente loggato)
Route::get('profilo', Profile::class)->name('profile');
// Cambio password: raggiungibile anche quando RequirePasswordChange
// blocca tutto il resto (vedi l'elenco di rotte ammesse nel middleware).
Route::get('cambia-password', [PasswordChangeController::class, 'show'])->name('password.change');
Route::post('cambia-password', [PasswordChangeController::class, 'store'])->name('password.change.store');
// Territori — visualizzazione (lista + dettaglio) per tutti gli utenti
// operativi, cioè chi può assegnare i territori.
Route::middleware('permission:territori.assign')->group(function () {
Route::get('territori', TerritorioIndex::class)->name('territori.index');
});
// Gestione territori (crea/modifica/cestino): solo amministratore
// (territori.manage). Le rotte LETTERALI (create/cestino) devono essere
// registrate PRIMA del wildcard {territorio}, altrimenti "create"/"cestino"
// verrebbero catturate dalla rotta di dettaglio.
Route::middleware('permission:territori.manage')->group(function () {
Route::get('territori/create', TerritorioCreate::class)->name('territori.create');
Route::get('territori/cestino', TerritorioCestino::class)->name('territori.cestino');
});
Route::middleware('permission:territori.assign')->group(function () {
Route::get('territori/{territorio}', TerritorioShow::class)->name('territori.show');
});
Route::middleware('permission:territori.manage')->group(function () {
Route::get('territori/{territorio}/edit', TerritorioEdit::class)->name('territori.edit');
});
// Proclamatori — visualizzazione (lista + dettaglio) per chi ha
// proclamatori.manage; sola lettura per i non-admin.
Route::middleware('permission:proclamatori.manage')->group(function () {
Route::get('proclamatori', ProclamatoreIndex::class)->name('proclamatori.index');
});
// Gestione proclamatori (crea/modifica/cestino): solo amministratore. Le
// rotte letterali (create/cestino) devono precedere il wildcard {proclamatore}.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('proclamatori/create', ProclamatoreCreate::class)->name('proclamatori.create');
Route::get('proclamatori/cestino', ProclamatoreCestino::class)->name('proclamatori.cestino');
});
Route::middleware('permission:proclamatori.manage')->group(function () {
Route::get('proclamatori/{proclamatore}', ProclamatoreShow::class)->name('proclamatori.show');
});
Route::middleware('permission:settings.manage')->group(function () {
Route::get('proclamatori/{proclamatore}/edit', ProclamatoreEdit::class)->name('proclamatori.edit');
});
// Assegnazioni
Route::middleware('permission:territori.assign')->group(function () {
Route::get('assegnazioni/assegna/{territorioId?}', Assegna::class)->name('assegnazioni.assegna');
});
Route::middleware('permission:territori.return')->group(function () {
Route::get('assegnazioni/{assegnazione}/rientra', Rientra::class)->name('assegnazioni.rientra');
});
// Campagne — visualizzazione per chi ha campagne.manage; sola lettura per i
// non-admin.
Route::middleware('permission:campagne.manage')->group(function () {
Route::get('campagne', CampagnaIndex::class)->name('campagne.index');
});
// Creazione campagne: solo amministratore. La rotta letterale create deve
// precedere il wildcard {campagna}.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('campagne/create', CampagnaCreate::class)->name('campagne.create');
});
Route::middleware('permission:campagne.manage')->group(function () {
Route::get('campagne/{campagna}', CampagnaShow::class)->name('campagne.show');
});
// Modifica campagne: solo amministratore (settings.manage). L'eliminazione
// vive in CampagnaIndex::deleteCampagna(), protetta lì con lo stesso gate.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('campagne/{campagna}/edit', CampagnaEdit::class)->name('campagne.edit');
});
// Registro
Route::middleware('permission:registro.view')->group(function () {
Route::get('registro', Registro::class)->name('registro.index');
});
// Audit Log
Route::middleware('permission:audit.view')->group(function () {
Route::get('audit', AuditLog::class)->name('audit.index');
});
// Settings (admin)
Route::middleware('permission:settings.manage')->group(function () {
Route::get('impostazioni', SettingsEdit::class)->name('settings.edit');
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');
// Piantine: sotto-app per generare mappe HD e PDF S-12 dei territori.
// Il rendering vive nel container map-renderer; qui c'è solo l'editor.
//
// Due pagine, non una: la gestione (area, immagini, PDF, storico) sta
// nel telaio normale dell'applicazione, il disegno ha una pagina sua a
// schermo intero perché la mappa vuole tutto lo spazio che c'è.
Route::get('piantine', PiantineManager::class)->name('piantine.index');
Route::get('piantine/editor/{mappa?}', PiantineEditor::class)->name('piantine.editor');
Route::get('piantine/{mappa}/anteprima', PiantinaAnteprimaController::class)->name('piantine.anteprima');
Route::post('xml-exchange/convert-sql', [XmlExchangeUploadController::class, 'convertSqlToXml'])->name('xml.convert-sql');
Route::post('xml-exchange/import-xml', [XmlExchangeUploadController::class, 'importXml'])->name('xml.import-xml');
Route::post('imports/territori/pdf-zip', [TerritoryPdfImportController::class, 'storeZip'])->name('imports.territori.pdf-zip');
// Backup completo (.zip): dati, utenti e file caricati. L'avvio delle
// operazioni e la barra di avanzamento stanno in Livewire
// (Settings\BackupManager); qui passa solo il trasferimento dei file.
// whereUuid non e' decorazione: l'id finisce in
// CreateBackupArchive::percorso(), che lo concatena a un percorso su
// disco. Oggi il controller si ferma prima, perche' un id inventato non
// ha uno stato in cache, ma vincolare il formato toglie di mezzo la
// classe di problema invece di dipendere da quell'ordine di controlli.
Route::get('backup/file/{operationId}', [BackupController::class, 'file'])
->whereUuid('operationId')
->name('backup.file');
Route::post('backup/restore', [BackupController::class, 'upload'])->name('backup.restore');
});
// Privacy / Informativa GDPR — visibile solo all'amministratore.
Route::middleware('permission:settings.manage')->group(function () {
Route::get('privacy', Privacy::class)->name('privacy');
});
});