++ Richieste dal modulo pubblico, territori cartacei, S-13 mensile per email
- /richiesta: modulo pubblico (nuovo territorio o riconsegna) con campo trappola, tempo minimo e limite orario; nome e note cifrati, avviso per email a chi ha richieste.gestisci (amministratore, assistente) - Home: riquadro Richieste con i proclamatori dal nome simile; porta su Assegna/Rientra già esistenti, che chiudono la richiesta; Scarta - Cartaceo sull'assegnazione (Assegna e Il mio gruppo), etichetta nell'elenco degli assegnati, pagina Cartacei, passa per l'XML - registro:invia-s13 agli amministratori l'ultima domenica del mese; richieste:pulizia dopo 90 giorni dalla gestione - Privacy, README e HANDOFF aggiornati; test in RichiesteTest Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,9 @@ use App\Models\Territorio;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Richiesta;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Locked;
|
||||
|
||||
class Assegna extends Component
|
||||
{
|
||||
@@ -20,6 +22,17 @@ class Assegna extends Component
|
||||
public ?int $proclamatore_id = null;
|
||||
public string $assigned_at = '';
|
||||
|
||||
/** Il proclamatore ha chiesto la cartolina cartacea (elenco Cartacei). */
|
||||
public bool $cartaceo = false;
|
||||
|
||||
/**
|
||||
* La richiesta dal modulo pubblico da cui si è partiti (?richiesta=,
|
||||
* dalla Home): si mostra sopra il modulo e, fatta l'assegnazione, risulta
|
||||
* evasa. Locked: la imposta solo mount().
|
||||
*/
|
||||
#[Locked]
|
||||
public ?int $richiestaId = null;
|
||||
|
||||
// Optional pre-selection from parent context
|
||||
public ?int $preselectedTerritorioId = null;
|
||||
|
||||
@@ -37,6 +50,17 @@ class Assegna extends Component
|
||||
$this->assigned_at = now()->format('Y-m-d');
|
||||
$this->elencoNascosto = request()->boolean('dalla_home');
|
||||
|
||||
// Dalla Home, da una richiesta: il proclamatore scelto fra i nomi
|
||||
// simili arriva già impostato (?proclamatore=), il resto del modulo è
|
||||
// quello di sempre.
|
||||
if (($id = request()->integer('proclamatore')) && Proclamatore::attivi()->whereKey($id)->exists()) {
|
||||
$this->proclamatore_id = $id;
|
||||
}
|
||||
if (($id = request()->integer('richiesta')) && auth()->user()->can('richieste.gestisci')
|
||||
&& Richiesta::aperte()->whereKey($id)->exists()) {
|
||||
$this->richiestaId = $id;
|
||||
}
|
||||
|
||||
// ?invia=<id>: riprende l'invio di un link rimasto in sospeso (dalla
|
||||
// scheda del territorio).
|
||||
if ($invia = request()->integer('invia')) {
|
||||
@@ -109,6 +133,7 @@ class Assegna extends Component
|
||||
'created_by' => auth()->id(),
|
||||
// C'è un link da mandare solo se il territorio ha il PDF.
|
||||
'link_da_inviare' => (bool) $territorio->pdf_path,
|
||||
'cartaceo' => $this->cartaceo,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -131,6 +156,13 @@ class Assegna extends Component
|
||||
])
|
||||
->log('assigned');
|
||||
|
||||
// Partita da una richiesta del modulo pubblico: ora è evasa.
|
||||
if ($this->richiestaId) {
|
||||
Richiesta::find($this->richiestaId)?->evadi($assegnazione, auth()->user());
|
||||
$this->richiestaId = null;
|
||||
}
|
||||
$this->cartaceo = false;
|
||||
|
||||
// Non si va avanti: si apre l'invio del link (vedi in cima).
|
||||
$this->apriInvio($assegnazione->id);
|
||||
}
|
||||
@@ -177,6 +209,7 @@ class Assegna extends Component
|
||||
'opzioniTerritori' => $opzioniTerritori,
|
||||
'opzioniProclamatori' => $opzioniProclamatori,
|
||||
'anteprimaIniziale' => $this->territorio_id ? $territori->firstWhere('id', $this->territorio_id)?->miniaturaUrl() : null,
|
||||
'richiesta' => $this->richiestaId ? Richiesta::find($this->richiestaId) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
25
app/Livewire/Assegnazioni/Cartacei.php
Normal file
25
app/Livewire/Assegnazioni/Cartacei.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Assegnazioni;
|
||||
|
||||
use App\Models\Assegnazione;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* I territori fuori in cartaceo, e a chi: le assegnazioni aperte per cui il
|
||||
* proclamatore ha chiesto la cartolina (spunta "cartaceo" su Assegna o in "Il
|
||||
* mio gruppo"). Al rientro escono dall'elenco.
|
||||
*/
|
||||
class Cartacei extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.assegnazioni.cartacei', [
|
||||
'assegnazioni' => Assegnazione::aperte()
|
||||
->where('cartaceo', true)
|
||||
->with(['territorio.zona', 'proclamatore'])
|
||||
->orderBy('assigned_at')
|
||||
->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use App\Livewire\Concerns\RicordaRitorno;
|
||||
use Livewire\Component;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Richiesta;
|
||||
|
||||
class Rientra extends Component
|
||||
{
|
||||
@@ -25,6 +26,13 @@ class Rientra extends Component
|
||||
|
||||
public bool $counted_in_campaign = false;
|
||||
|
||||
/**
|
||||
* La richiesta di riconsegna dal modulo pubblico da cui si è partiti
|
||||
* (?richiesta=, dalla Home): registrato il rientro, risulta evasa.
|
||||
*/
|
||||
#[Locked]
|
||||
public ?int $richiestaId = null;
|
||||
|
||||
public function mount(Assegnazione $assegnazione)
|
||||
{
|
||||
$this->autorizza($assegnazione);
|
||||
@@ -34,6 +42,11 @@ class Rientra extends Component
|
||||
abort(404, 'Assegnazione già rientrata.');
|
||||
}
|
||||
|
||||
if (($id = request()->integer('richiesta')) && auth()->user()->can('richieste.gestisci')
|
||||
&& Richiesta::aperte()->whereKey($id)->exists()) {
|
||||
$this->richiestaId = $id;
|
||||
}
|
||||
|
||||
$this->returned_at = now()->format('Y-m-d');
|
||||
$this->checkCampaign();
|
||||
}
|
||||
@@ -132,6 +145,11 @@ class Rientra extends Component
|
||||
);
|
||||
}
|
||||
|
||||
// Partito da una richiesta di riconsegna (Home): ora è evasa.
|
||||
if ($this->richiestaId) {
|
||||
Richiesta::find($this->richiestaId)?->evadi($this->assegnazione, auth()->user());
|
||||
}
|
||||
|
||||
session()->flash('success', "Territorio {$territorio->numero} rientrato da {$proclamatore->nome_completo} dopo {$this->assegnazione->giorni} giorni.");
|
||||
return $this->redirect($this->destinazione($this->paginaPredefinita()), navigate: true);
|
||||
}
|
||||
@@ -164,6 +182,7 @@ class Rientra extends Component
|
||||
'campagna' => $campagna,
|
||||
'giorniPercorrenza' => $giorniPercorrenza,
|
||||
'sogliaPercorrenza' => $sogliaPercorrenza,
|
||||
'richiesta' => $this->richiestaId ? Richiesta::find($this->richiestaId) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ class MioGruppo extends Component
|
||||
|
||||
public string $dataAssegnazione = '';
|
||||
|
||||
/** Il membro ha chiesto la cartolina cartacea (elenco Cartacei). */
|
||||
public bool $cartaceo = false;
|
||||
|
||||
// Filtri del pacchetto, nell'indirizzo: tornando dal rientro si ritrovano.
|
||||
#[Url(except: '')]
|
||||
public string $filtroZona = '';
|
||||
@@ -84,13 +87,14 @@ class MioGruppo extends Component
|
||||
$this->territorioDelPacchetto($territorioId);
|
||||
$this->territorioDaAssegnare = $territorioId;
|
||||
$this->membroId = null;
|
||||
$this->cartaceo = false;
|
||||
$this->dataAssegnazione = now()->format('Y-m-d');
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
public function annullaAssegnazione(): void
|
||||
{
|
||||
$this->reset(['territorioDaAssegnare', 'membroId']);
|
||||
$this->reset(['territorioDaAssegnare', 'membroId', 'cartaceo']);
|
||||
$this->resetValidation();
|
||||
}
|
||||
|
||||
@@ -115,14 +119,14 @@ class MioGruppo extends Component
|
||||
}
|
||||
|
||||
try {
|
||||
$assegnazione = app(GruppiServizio::class)->assegna($gruppo, $territorio, $membro, Carbon::parse($this->dataAssegnazione));
|
||||
$assegnazione = app(GruppiServizio::class)->assegna($gruppo, $territorio, $membro, Carbon::parse($this->dataAssegnazione), $this->cartaceo);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->addError('membroId', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->reset(['territorioDaAssegnare', 'membroId']);
|
||||
$this->reset(['territorioDaAssegnare', 'membroId', 'cartaceo']);
|
||||
|
||||
// Come dalla pagina Assegna: si apre l'invio del link.
|
||||
$this->apriInvio($assegnazione->id);
|
||||
|
||||
@@ -3,16 +3,58 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Richiesta;
|
||||
use App\Models\Setting;
|
||||
use App\Services\RichiesteServizio;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class Home extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
|
||||
/**
|
||||
* Le richieste dal modulo pubblico si confermano portandole su Assegna o
|
||||
* Rientra (link nel riquadro); qui si possono solo scartare.
|
||||
*/
|
||||
public function scartaRichiesta(int $id): void
|
||||
{
|
||||
$this->authorize('richieste.gestisci');
|
||||
Richiesta::findOrFail($id)->scarta(auth()->user());
|
||||
$this->avvisa('Richiesta scartata.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Le richieste aperte, con quello che serve a confermarle: per un
|
||||
* territorio nuovo i proclamatori dal nome simile a quello scritto, per
|
||||
* una riconsegna l'assegnazione aperta di quel territorio.
|
||||
*/
|
||||
private function richiesteAperte(): \Illuminate\Support\Collection
|
||||
{
|
||||
if (! auth()->user()->can('richieste.gestisci')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$aperte = Richiesta::aperte()->orderBy('created_at')->get();
|
||||
if ($aperte->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$servizio = app(RichiesteServizio::class);
|
||||
$proclamatori = Proclamatore::attivi()->get();
|
||||
|
||||
return $aperte->map(fn (Richiesta $r) => [
|
||||
'richiesta' => $r,
|
||||
'simili' => $r->tipo === Richiesta::NUOVO ? $servizio->nomiSimili($r->nome, $proclamatori) : collect(),
|
||||
'assegnazione' => $r->tipo === Richiesta::RICONSEGNA ? $servizio->assegnazioneAperta($r->numero_territorio) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadPdfDaAssegnare()
|
||||
{
|
||||
$this->authorize('territori.assign');
|
||||
@@ -147,6 +189,7 @@ class Home extends Component
|
||||
'homeLimit' => $homeLimit,
|
||||
'territoriDaAssegnare' => $territoriDaAssegnare,
|
||||
'daRientrare' => $daRientrare,
|
||||
'richieste' => $this->richiesteAperte(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Livewire;
|
||||
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Richiesta;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\ConfigurazionePosta;
|
||||
@@ -23,7 +24,7 @@ use Spatie\Permission\Models\Role;
|
||||
class Privacy extends Component
|
||||
{
|
||||
/** Data dell'ultima revisione del testo dell'informativa. */
|
||||
public const REVISIONE = '19 ottobre 2026';
|
||||
public const REVISIONE = '21 ottobre 2026';
|
||||
|
||||
/** I permessi spiegati a chi legge l'informativa. */
|
||||
public const PERMESSI = [
|
||||
@@ -36,6 +37,7 @@ class Privacy extends Component
|
||||
'registro.export' => 'esportazione del registro',
|
||||
'territori.assign' => 'assegnazione dei territori',
|
||||
'territori.return' => 'rientro dei territori',
|
||||
'richieste.gestisci' => 'richieste arrivate dal modulo pubblico (nuovo territorio o riconsegna)',
|
||||
'audit.view' => 'consultazione del registro delle attività',
|
||||
'audit.export' => 'esportazione del registro delle attività',
|
||||
];
|
||||
@@ -78,6 +80,8 @@ class Privacy extends Component
|
||||
'utenti' => User::count(),
|
||||
'assegnazioni' => Assegnazione::count(),
|
||||
'assegnazioniAperte' => Assegnazione::aperte()->count(),
|
||||
'richieste' => Richiesta::count(),
|
||||
'richiesteAperte' => Richiesta::aperte()->count(),
|
||||
'attivita' => Activity::count(),
|
||||
'attivitaDal' => Activity::min('created_at'),
|
||||
],
|
||||
|
||||
97
app/Livewire/Richieste/ModuloRichiesta.php
Normal file
97
app/Livewire/Richieste/ModuloRichiesta.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Richieste;
|
||||
|
||||
use App\Models\Richiesta;
|
||||
use App\Models\Setting;
|
||||
use App\Services\RichiesteServizio;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Il modulo pubblico (/richiesta, senza login): un proclamatore chiede un
|
||||
* territorio nuovo o annuncia una riconsegna. Non mostra nessun dato già
|
||||
* presente: chi lo apre vede solo i campi da compilare.
|
||||
*
|
||||
* Contro lo spam, tre difese che a chi compila davvero non costano niente:
|
||||
* - un campo trappola ($sito) nascosto alle persone e riempito dai programmi;
|
||||
* - un tempo minimo fra apertura e invio (i programmi inviano subito);
|
||||
* - al massimo MASSIMO_ORA richieste all'ora per indirizzo.
|
||||
* Nei primi due casi si risponde come se fosse andata bene, senza salvare:
|
||||
* chi prova non impara cosa l'ha fermato.
|
||||
*/
|
||||
class ModuloRichiesta extends Component
|
||||
{
|
||||
public const MASSIMO_ORA = 5;
|
||||
|
||||
public const SECONDI_MINIMI = 3;
|
||||
|
||||
public string $tipo = Richiesta::NUOVO;
|
||||
|
||||
public string $nome = '';
|
||||
|
||||
public string $numero = '';
|
||||
|
||||
public string $note = '';
|
||||
|
||||
/** Trappola: nascosto alle persone, i programmi lo riempiono. */
|
||||
public string $sito = '';
|
||||
|
||||
#[Locked]
|
||||
public int $apertoAlle = 0;
|
||||
|
||||
#[Locked]
|
||||
public bool $inviata = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->apertoAlle = now()->timestamp;
|
||||
}
|
||||
|
||||
public function invia(RichiesteServizio $richieste): void
|
||||
{
|
||||
if ($this->sito !== '' || now()->timestamp - $this->apertoAlle < self::SECONDI_MINIMI) {
|
||||
$this->inviata = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$chiave = 'richiesta-pubblica:' . sha1((string) request()->ip());
|
||||
if (RateLimiter::tooManyAttempts($chiave, self::MASSIMO_ORA)) {
|
||||
$this->addError('nome', "Hai già mandato diverse richieste: riprova fra un po'.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'tipo' => 'required|in:' . Richiesta::NUOVO . ',' . Richiesta::RICONSEGNA,
|
||||
'nome' => 'required|string|min:3|max:100',
|
||||
'numero' => 'nullable|required_if:tipo,' . Richiesta::RICONSEGNA . '|string|max:20',
|
||||
'note' => 'nullable|string|max:1000',
|
||||
], [
|
||||
'nome.required' => 'Scrivi nome e cognome.',
|
||||
'nome.min' => 'Scrivi nome e cognome.',
|
||||
'numero.required_if' => 'Scrivi il numero del territorio che riconsegni.',
|
||||
]);
|
||||
|
||||
RateLimiter::hit($chiave, 3600);
|
||||
$richieste->registra($this->tipo, $this->nome, $this->numero, $this->note);
|
||||
|
||||
$this->reset(['nome', 'numero', 'note']);
|
||||
$this->inviata = true;
|
||||
}
|
||||
|
||||
public function nuova(): void
|
||||
{
|
||||
$this->reset(['tipo', 'nome', 'numero', 'note', 'inviata']);
|
||||
$this->apertoAlle = now()->timestamp;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.richieste.modulo-richiesta', [
|
||||
'congregazione' => Setting::instance()->congregazione_nome ?: 'TerManager2',
|
||||
])->layout('components.layouts.guest', ['title' => 'Richiesta territorio']);
|
||||
}
|
||||
}
|
||||
@@ -817,6 +817,7 @@ class XmlExchange extends Component
|
||||
'campaign_id' => $legacyCampagna !== '' && isset($campagnaMap[$legacyCampagna]) ? $campagnaMap[$legacyCampagna] : null,
|
||||
'link_sent' => ((int) ($assegnazioneNode->link_sent ?? 0)) === 1,
|
||||
'link_da_inviare' => ((int) ($assegnazioneNode->link_da_inviare ?? 0)) === 1,
|
||||
'cartaceo' => ((int) ($assegnazioneNode->cartaceo ?? 0)) === 1,
|
||||
'gruppo_servizio_id' => $gruppoMap[(string) ($assegnazioneNode->legacy_gruppo_id ?? '')] ?? null,
|
||||
'gruppo_nome' => trim((string) ($assegnazioneNode->gruppo_nome ?? '')) ?: null,
|
||||
'link_canale' => in_array((string) ($assegnazioneNode->link_canale ?? ''), array_keys(Assegnazione::CANALI_LINK), true) ? (string) $assegnazioneNode->link_canale : null,
|
||||
@@ -887,7 +888,7 @@ class XmlExchange extends Component
|
||||
'territori' => $this->righeEsportate(Territorio::query()->withTrashed()->orderBy('id'), ['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', 'gruppo_servizio_id', 'pdf_path', 'thumbnail_path', 'deleted_at', 'created_at', 'updated_at']),
|
||||
'anni_teocratici' => $this->righeEsportate(AnnoTeocratico::query()->orderBy('id'), ['id', 'label', 'start_date', 'end_date', 'created_at', 'updated_at']),
|
||||
'campagne' => $this->righeEsportate(Campagna::query()->orderBy('id'), ['id', 'descrizione', 'start_date', 'end_date', 'created_at', 'updated_at']),
|
||||
'assegnazioni' => $this->righeEsportate(Assegnazione::query()->orderBy('id'), ['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'link_da_inviare', 'link_canale', 'gruppo_servizio_id', 'gruppo_nome', 'pdf_access_code', 'created_at', 'updated_at']),
|
||||
'assegnazioni' => $this->righeEsportate(Assegnazione::query()->orderBy('id'), ['id', 'territorio_id', 'proclamatore_id', 'anno_teocratico_id', 'campaign_id', 'assigned_at', 'returned_at', 'counted_in_campaign', 'note', 'created_by', 'returned_by', 'link_sent', 'link_da_inviare', 'link_canale', 'cartaceo', 'gruppo_servizio_id', 'gruppo_nome', 'pdf_access_code', 'created_at', 'updated_at']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1388,6 +1389,7 @@ class XmlExchange extends Component
|
||||
$node->addChild('counted_in_campaign', (string) ((int) ($assegnazione['counted_in_campaign'] ?? 0)));
|
||||
$node->addChild('link_sent', (string) ((int) ($assegnazione['link_sent'] ?? 0)));
|
||||
$node->addChild('link_da_inviare', (string) ((int) ($assegnazione['link_da_inviare'] ?? 0)));
|
||||
$node->addChild('cartaceo', (string) ((int) ($assegnazione['cartaceo'] ?? 0)));
|
||||
$node->addChild('legacy_gruppo_id', (string) ($assegnazione['gruppo_servizio_id'] ?? ''));
|
||||
$this->addXmlText($node, 'gruppo_nome', (string) ($assegnazione['gruppo_nome'] ?? ''));
|
||||
$this->addXmlText($node, 'link_canale', (string) ($assegnazione['link_canale'] ?? ''));
|
||||
|
||||
Reference in New Issue
Block a user