Primo commit
This commit is contained in:
24
app/Console/Commands/AuditCleanup.php
Normal file
24
app/Console/Commands/AuditCleanup.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use App\Models\Setting;
|
||||
|
||||
class AuditCleanup extends Command
|
||||
{
|
||||
protected $signature = 'audit:cleanup';
|
||||
protected $description = 'Delete audit log entries older than the configured retention period';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$retentionDays = Setting::instance()->audit_retention_days ?? 365;
|
||||
$cutoff = now()->subDays($retentionDays);
|
||||
|
||||
$deleted = Activity::where('created_at', '<', $cutoff)->delete();
|
||||
|
||||
$this->info("Deleted {$deleted} audit entries older than {$retentionDays} days.");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
24
app/Http/Middleware/SetupRequired.php
Normal file
24
app/Http/Middleware/SetupRequired.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SetupRequired
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if ($request->is('setup*') || $request->is('login') || $request->is('logout')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (!Setting::isSetupComplete()) {
|
||||
return redirect()->route('setup.index');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
99
app/Livewire/Assegnazioni/Assegna.php
Normal file
99
app/Livewire/Assegnazioni/Assegna.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Assegnazioni;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
|
||||
class Assegna extends Component
|
||||
{
|
||||
public ?int $territorio_id = null;
|
||||
public ?int $proclamatore_id = null;
|
||||
public string $assigned_at = '';
|
||||
|
||||
// Optional pre-selection from parent context
|
||||
public ?int $preselectedTerritorioId = null;
|
||||
|
||||
public function mount(?int $territorioId = null)
|
||||
{
|
||||
$this->preselectedTerritorioId = $territorioId;
|
||||
$this->territorio_id = $territorioId;
|
||||
$this->assigned_at = now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'territorio_id' => 'required|exists:territori,id',
|
||||
'proclamatore_id' => 'required|exists:proclamatori,id',
|
||||
'assigned_at' => 'required|date|before_or_equal:today',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$territorio = Territorio::findOrFail($this->territorio_id);
|
||||
|
||||
// Check territory is available (not currently assigned)
|
||||
if ($territorio->assegnazioneCorrente) {
|
||||
session()->flash('error', "Il territorio {$territorio->numero} è già assegnato a {$territorio->assegnazioneCorrente->proclamatore->nome_completo}.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check territory is active
|
||||
if (!$territorio->attivo) {
|
||||
session()->flash('error', "Il territorio {$territorio->numero} è inattivo.");
|
||||
return;
|
||||
}
|
||||
|
||||
$proclamatore = Proclamatore::findOrFail($this->proclamatore_id);
|
||||
if (!$proclamatore->attivo) {
|
||||
session()->flash('error', "Il proclamatore {$proclamatore->nome_completo} è inattivo.");
|
||||
return;
|
||||
}
|
||||
|
||||
$assignedDate = \Carbon\Carbon::parse($this->assigned_at);
|
||||
$annoTeocratico = AnnoTeocratico::perData($assignedDate);
|
||||
|
||||
$assegnazione = Assegnazione::create([
|
||||
'territorio_id' => $this->territorio_id,
|
||||
'proclamatore_id' => $this->proclamatore_id,
|
||||
'anno_teocratico_id' => $annoTeocratico->id,
|
||||
'assigned_at' => $assignedDate,
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($assegnazione)
|
||||
->withProperties([
|
||||
'territorio' => $territorio->numero,
|
||||
'proclamatore' => $proclamatore->nome_completo,
|
||||
])
|
||||
->log('assigned');
|
||||
|
||||
session()->flash('success', "Territorio {$territorio->numero} assegnato a {$proclamatore->nome_completo}.");
|
||||
return $this->redirect(route('territori.show', $territorio), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$territoriDisponibili = Territorio::where('attivo', true)
|
||||
->whereDoesntHave('assegnazioni', fn($q) => $q->aperte())
|
||||
->orderBy('numero')
|
||||
->get();
|
||||
|
||||
$proclamatoriAttivi = Proclamatore::attivi()
|
||||
->get()
|
||||
->sortBy(fn($p) => mb_strtolower($p->cognome . ' ' . $p->nome));
|
||||
|
||||
return view('livewire.assegnazioni.assegna', [
|
||||
'territoriDisponibili' => $territoriDisponibili,
|
||||
'proclamatoriAttivi' => $proclamatoriAttivi,
|
||||
]);
|
||||
}
|
||||
}
|
||||
99
app/Livewire/Assegnazioni/Rientra.php
Normal file
99
app/Livewire/Assegnazioni/Rientra.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Assegnazioni;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Campagna;
|
||||
|
||||
class Rientra extends Component
|
||||
{
|
||||
public Assegnazione $assegnazione;
|
||||
public string $returned_at = '';
|
||||
public bool $showCampaignPrompt = false;
|
||||
public ?int $campagna_id = null;
|
||||
public bool $counted_in_campaign = false;
|
||||
|
||||
public function mount(Assegnazione $assegnazione)
|
||||
{
|
||||
$this->assegnazione = $assegnazione->load(['territorio', 'proclamatore']);
|
||||
|
||||
if ($assegnazione->returned_at) {
|
||||
abort(404, 'Assegnazione già rientrata.');
|
||||
}
|
||||
|
||||
$this->returned_at = now()->format('Y-m-d');
|
||||
$this->checkCampaign();
|
||||
}
|
||||
|
||||
public function updatedReturnedAt()
|
||||
{
|
||||
$this->checkCampaign();
|
||||
}
|
||||
|
||||
protected function checkCampaign()
|
||||
{
|
||||
if (!$this->returned_at) {
|
||||
$this->showCampaignPrompt = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$returnDate = \Carbon\Carbon::parse($this->returned_at);
|
||||
$campagna = $this->assegnazione->campagnaApplicabile($returnDate);
|
||||
|
||||
if ($campagna) {
|
||||
$this->showCampaignPrompt = true;
|
||||
$this->campagna_id = $campagna->id;
|
||||
} else {
|
||||
$this->showCampaignPrompt = false;
|
||||
$this->campagna_id = null;
|
||||
$this->counted_in_campaign = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'returned_at' => 'required|date|after_or_equal:' . $this->assegnazione->assigned_at->format('Y-m-d') . '|before_or_equal:today',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$returnDate = \Carbon\Carbon::parse($this->returned_at);
|
||||
|
||||
$this->assegnazione->update([
|
||||
'returned_at' => $returnDate,
|
||||
'returned_by' => auth()->id(),
|
||||
'campaign_id' => $this->counted_in_campaign ? $this->campagna_id : null,
|
||||
'counted_in_campaign' => $this->counted_in_campaign,
|
||||
]);
|
||||
|
||||
$territorio = $this->assegnazione->territorio;
|
||||
$proclamatore = $this->assegnazione->proclamatore;
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($this->assegnazione)
|
||||
->withProperties([
|
||||
'territorio' => $territorio->numero,
|
||||
'proclamatore' => $proclamatore->nome_completo,
|
||||
'giorni' => $this->assegnazione->giorni,
|
||||
'campagna' => $this->counted_in_campaign,
|
||||
])
|
||||
->log('returned');
|
||||
|
||||
session()->flash('success', "Territorio {$territorio->numero} rientrato da {$proclamatore->nome_completo} dopo {$this->assegnazione->giorni} giorni.");
|
||||
return $this->redirect(route('territori.show', $territorio), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$campagna = $this->campagna_id ? Campagna::find($this->campagna_id) : null;
|
||||
|
||||
return view('livewire.assegnazioni.rientra', [
|
||||
'campagna' => $campagna,
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
app/Livewire/AuditLog.php
Normal file
57
app/Livewire/AuditLog.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
class AuditLog extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $filterEvent = '';
|
||||
public string $filterCauser = '';
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'filterEvent' => ['except' => ''],
|
||||
'filterCauser' => ['except' => ''],
|
||||
];
|
||||
|
||||
public function updatingSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Activity::with('causer')->latest();
|
||||
|
||||
if ($this->search) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('description', 'like', "%{$this->search}%")
|
||||
->orWhere('subject_type', 'like', "%{$this->search}%")
|
||||
->orWhere('properties', 'like', "%{$this->search}%");
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->filterEvent) {
|
||||
$query->where('description', $this->filterEvent);
|
||||
}
|
||||
|
||||
if ($this->filterCauser) {
|
||||
$query->where('causer_id', $this->filterCauser);
|
||||
}
|
||||
|
||||
$events = Activity::select('description')->distinct()->pluck('description');
|
||||
$users = \App\Models\User::orderBy('name')->get();
|
||||
|
||||
return view('livewire.audit-log', [
|
||||
'activities' => $query->paginate(30),
|
||||
'events' => $events,
|
||||
'users' => $users,
|
||||
]);
|
||||
}
|
||||
}
|
||||
54
app/Livewire/Auth/Login.php
Normal file
54
app/Livewire/Auth/Login.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Auth;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Login extends Component
|
||||
{
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public bool $remember = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:6',
|
||||
];
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$throttleKey = Str::transliterate(Str::lower($this->email) . '|' . request()->ip());
|
||||
|
||||
if (RateLimiter::tooManyAttempts($throttleKey, 5)) {
|
||||
$seconds = RateLimiter::availableIn($throttleKey);
|
||||
$this->addError('email', "Troppi tentativi. Riprova tra {$seconds} secondi.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) {
|
||||
RateLimiter::hit($throttleKey);
|
||||
$this->addError('email', 'Credenziali non valide.');
|
||||
return;
|
||||
}
|
||||
|
||||
RateLimiter::clear($throttleKey);
|
||||
session()->regenerate();
|
||||
activity()->causedBy(auth()->user())->log('login');
|
||||
|
||||
return redirect()->intended(route('dashboard'));
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.auth.login')
|
||||
->layout('components.layouts.guest');
|
||||
}
|
||||
}
|
||||
44
app/Livewire/Campagne/CampagnaCreate.php
Normal file
44
app/Livewire/Campagne/CampagnaCreate.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Campagne;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Campagna;
|
||||
|
||||
class CampagnaCreate extends Component
|
||||
{
|
||||
public string $descrizione = '';
|
||||
public string $start_date = '';
|
||||
public string $end_date = '';
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'descrizione' => 'required|string|max:255',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after:start_date',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$campagna = Campagna::create([
|
||||
'descrizione' => $this->descrizione,
|
||||
'start_date' => $this->start_date,
|
||||
'end_date' => $this->end_date,
|
||||
]);
|
||||
|
||||
session()->flash('success', "Campagna '{$campagna->descrizione}' creata.");
|
||||
return $this->redirect(route('campagne.index'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.campagne.campagna-form', [
|
||||
'titolo' => 'Nuova Campagna',
|
||||
'btnLabel' => 'Crea Campagna',
|
||||
]);
|
||||
}
|
||||
}
|
||||
53
app/Livewire/Campagne/CampagnaEdit.php
Normal file
53
app/Livewire/Campagne/CampagnaEdit.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Campagne;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Campagna;
|
||||
|
||||
class CampagnaEdit extends Component
|
||||
{
|
||||
public Campagna $campagna;
|
||||
public string $descrizione = '';
|
||||
public string $start_date = '';
|
||||
public string $end_date = '';
|
||||
|
||||
public function mount(Campagna $campagna)
|
||||
{
|
||||
$this->campagna = $campagna;
|
||||
$this->descrizione = $campagna->descrizione;
|
||||
$this->start_date = $campagna->start_date->format('Y-m-d');
|
||||
$this->end_date = $campagna->end_date->format('Y-m-d');
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'descrizione' => 'required|string|max:255',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after:start_date',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->campagna->update([
|
||||
'descrizione' => $this->descrizione,
|
||||
'start_date' => $this->start_date,
|
||||
'end_date' => $this->end_date,
|
||||
]);
|
||||
|
||||
session()->flash('success', "Campagna aggiornata.");
|
||||
return $this->redirect(route('campagne.index'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.campagne.campagna-form', [
|
||||
'titolo' => "Modifica: {$this->campagna->descrizione}",
|
||||
'btnLabel' => 'Salva Modifiche',
|
||||
]);
|
||||
}
|
||||
}
|
||||
26
app/Livewire/Campagne/CampagnaIndex.php
Normal file
26
app/Livewire/Campagne/CampagnaIndex.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Campagne;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Campagna;
|
||||
|
||||
class CampagnaIndex extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function deleteCampagna(int $id)
|
||||
{
|
||||
$campagna = Campagna::findOrFail($id);
|
||||
$campagna->delete();
|
||||
session()->flash('success', "Campagna '{$campagna->descrizione}' eliminata.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.campagne.campagna-index', [
|
||||
'campagne' => Campagna::orderByDesc('start_date')->paginate(15),
|
||||
]);
|
||||
}
|
||||
}
|
||||
41
app/Livewire/Campagne/CampagnaShow.php
Normal file
41
app/Livewire/Campagne/CampagnaShow.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Campagne;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\Territorio;
|
||||
|
||||
class CampagnaShow extends Component
|
||||
{
|
||||
public Campagna $campagna;
|
||||
|
||||
public function mount(Campagna $campagna)
|
||||
{
|
||||
$this->campagna = $campagna;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
// All assignments with returned_at in campaign range that were counted
|
||||
$conteggiate = Assegnazione::where('campagna_id', $this->campagna->id)
|
||||
->where('counted_in_campaign', true)
|
||||
->with(['territorio', 'proclamatore'])
|
||||
->orderBy('returned_at')
|
||||
->get();
|
||||
|
||||
// All assignments that were active during this campaign range
|
||||
$assegnateNelRange = Assegnazione::where('assigned_at', '<=', $this->campagna->end_date)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('returned_at')
|
||||
->orWhere('returned_at', '>=', $this->campagna->start_date);
|
||||
})
|
||||
->count();
|
||||
|
||||
return view('livewire.campagne.campagna-show', [
|
||||
'conteggiate' => $conteggiate,
|
||||
'assegnateNelRange' => $assegnateNelRange,
|
||||
]);
|
||||
}
|
||||
}
|
||||
81
app/Livewire/Home.php
Normal file
81
app/Livewire/Home.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Campagna;
|
||||
use App\Models\Setting;
|
||||
|
||||
class Home extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$settings = Setting::instance();
|
||||
$annoCorrente = AnnoTeocratico::corrente();
|
||||
$campagnaAttiva = Campagna::attiva();
|
||||
|
||||
// Territory counts
|
||||
$totTerritoriAttivi = Territorio::where('attivo', true)->count();
|
||||
$totAssegnati = Territorio::assegnato()->count();
|
||||
$totInReparto = Territorio::inReparto()->count();
|
||||
|
||||
// Coverage: returned territories per current theocratic year
|
||||
$territoriPercorsi = 0;
|
||||
if ($annoCorrente) {
|
||||
$territoriPercorsi = Assegnazione::where('anno_teocratico_id', $annoCorrente->id)
|
||||
->whereNotNull('returned_at')
|
||||
->distinct('territorio_id')
|
||||
->count('territorio_id');
|
||||
}
|
||||
|
||||
// Monthly average
|
||||
$mediaPercorrenzaMensile = 0;
|
||||
if ($annoCorrente && $annoCorrente->mesi_trascorsi > 0) {
|
||||
$mediaPercorrenzaMensile = round($territoriPercorsi / $annoCorrente->mesi_trascorsi, 1);
|
||||
}
|
||||
|
||||
// Campaign stats
|
||||
$campagnaStats = null;
|
||||
if ($campagnaAttiva) {
|
||||
$campagnaStats = [
|
||||
'descrizione' => $campagnaAttiva->descrizione,
|
||||
'percentuale' => $campagnaAttiva->percentuale_percorrenza,
|
||||
'fine' => $campagnaAttiva->end_date->format('d/m/Y'),
|
||||
];
|
||||
}
|
||||
|
||||
// Quick lists
|
||||
$daAssegnare = Territorio::daAssegnare()
|
||||
->with('zona', 'tipologia', 'ultimaAssegnazione')
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
$prioritari = Territorio::prioritari()
|
||||
->with('zona', 'tipologia', 'ultimaAssegnazione')
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
$daRientrare = Territorio::daRientrare()
|
||||
->with(['zona', 'assegnazioneCorrente.proclamatore'])
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
return view('livewire.home', [
|
||||
'congregazione' => $settings->congregazione_nome ?? 'TerManager2',
|
||||
'annoCorrente' => $annoCorrente,
|
||||
'totTerritoriAttivi' => $totTerritoriAttivi,
|
||||
'totAssegnati' => $totAssegnati,
|
||||
'totInReparto' => $totInReparto,
|
||||
'territoriPercorsi' => $territoriPercorsi,
|
||||
'mediaPercorrenzaMensile' => $mediaPercorrenzaMensile,
|
||||
'campagnaStats' => $campagnaStats,
|
||||
'daAssegnare' => $daAssegnare,
|
||||
'prioritari' => $prioritari,
|
||||
'daRientrare' => $daRientrare,
|
||||
]);
|
||||
}
|
||||
}
|
||||
19
app/Livewire/Privacy.php
Normal file
19
app/Livewire/Privacy.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Setting;
|
||||
|
||||
class Privacy extends Component
|
||||
{
|
||||
public function render()
|
||||
{
|
||||
$settings = Setting::instance();
|
||||
|
||||
return view('livewire.privacy', [
|
||||
'congregazione' => $settings->congregazione_nome ?? 'Congregazione',
|
||||
'auditRetention' => $settings->audit_retention_days ?? 365,
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
app/Livewire/Proclamatori/ProclamatoreCestino.php
Normal file
44
app/Livewire/Proclamatori/ProclamatoreCestino.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Proclamatori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Proclamatore;
|
||||
|
||||
class ProclamatoreCestino extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function restore(int $id)
|
||||
{
|
||||
$proclamatore = Proclamatore::onlyTrashed()->findOrFail($id);
|
||||
$proclamatore->restore();
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($proclamatore)
|
||||
->log('restored');
|
||||
session()->flash('success', "Proclamatore ripristinato.");
|
||||
}
|
||||
|
||||
public function forceDelete(int $id)
|
||||
{
|
||||
$proclamatore = Proclamatore::onlyTrashed()->findOrFail($id);
|
||||
|
||||
if ($proclamatore->assegnazioni()->exists()) {
|
||||
session()->flash('error', 'Impossibile eliminare definitivamente: il proclamatore ha assegnazioni nello storico.');
|
||||
return;
|
||||
}
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->log('force_deleted_proclamatore');
|
||||
$proclamatore->forceDelete();
|
||||
session()->flash('success', 'Proclamatore eliminato definitivamente.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.proclamatori.proclamatore-cestino', [
|
||||
'proclamatori' => Proclamatore::onlyTrashed()->orderByDesc('deleted_at')->paginate(20),
|
||||
]);
|
||||
}
|
||||
}
|
||||
48
app/Livewire/Proclamatori/ProclamatoreCreate.php
Normal file
48
app/Livewire/Proclamatori/ProclamatoreCreate.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Proclamatori;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Proclamatore;
|
||||
|
||||
class ProclamatoreCreate extends Component
|
||||
{
|
||||
public string $nome = '';
|
||||
public string $cognome = '';
|
||||
public bool $attivo = true;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'nome' => 'required|string|max:100',
|
||||
'cognome' => 'required|string|max:100',
|
||||
'attivo' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$proclamatore = Proclamatore::create([
|
||||
'nome' => $this->nome,
|
||||
'cognome' => $this->cognome,
|
||||
'attivo' => $this->attivo,
|
||||
]);
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($proclamatore)
|
||||
->log('created');
|
||||
|
||||
session()->flash('success', "Proclamatore {$proclamatore->nome_completo} creato.");
|
||||
return $this->redirect(route('proclamatori.index'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.proclamatori.proclamatore-form', [
|
||||
'titolo' => 'Nuovo Proclamatore',
|
||||
'btnLabel' => 'Crea Proclamatore',
|
||||
]);
|
||||
}
|
||||
}
|
||||
53
app/Livewire/Proclamatori/ProclamatoreEdit.php
Normal file
53
app/Livewire/Proclamatori/ProclamatoreEdit.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Proclamatori;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Proclamatore;
|
||||
|
||||
class ProclamatoreEdit extends Component
|
||||
{
|
||||
public Proclamatore $proclamatore;
|
||||
public string $nome = '';
|
||||
public string $cognome = '';
|
||||
public bool $attivo = true;
|
||||
|
||||
public function mount(Proclamatore $proclamatore)
|
||||
{
|
||||
$this->proclamatore = $proclamatore;
|
||||
$this->nome = $proclamatore->nome;
|
||||
$this->cognome = $proclamatore->cognome;
|
||||
$this->attivo = $proclamatore->attivo;
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'nome' => 'required|string|max:100',
|
||||
'cognome' => 'required|string|max:100',
|
||||
'attivo' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->proclamatore->update([
|
||||
'nome' => $this->nome,
|
||||
'cognome' => $this->cognome,
|
||||
'attivo' => $this->attivo,
|
||||
]);
|
||||
|
||||
session()->flash('success', "Proclamatore {$this->proclamatore->nome_completo} aggiornato.");
|
||||
return $this->redirect(route('proclamatori.index'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.proclamatori.proclamatore-form', [
|
||||
'titolo' => "Modifica {$this->proclamatore->nome_completo}",
|
||||
'btnLabel' => 'Salva Modifiche',
|
||||
]);
|
||||
}
|
||||
}
|
||||
135
app/Livewire/Proclamatori/ProclamatoreIndex.php
Normal file
135
app/Livewire/Proclamatori/ProclamatoreIndex.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Proclamatori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Proclamatore;
|
||||
|
||||
class ProclamatoreIndex extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $filtroStato = '';
|
||||
public string $sortField = 'cognome';
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'filtroStato' => ['except' => ''],
|
||||
];
|
||||
|
||||
public function updatingSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingFiltroStato()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function sortBy(string $field)
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortField = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleActive(int $id)
|
||||
{
|
||||
$proclamatore = Proclamatore::findOrFail($id);
|
||||
$proclamatore->update(['attivo' => !$proclamatore->attivo]);
|
||||
}
|
||||
|
||||
public function deleteProclamatore(int $id)
|
||||
{
|
||||
$proclamatore = Proclamatore::findOrFail($id);
|
||||
|
||||
if ($proclamatore->assegnazioni()->aperte()->exists()) {
|
||||
session()->flash('error', "Il proclamatore {$proclamatore->nome_completo} ha assegnazioni aperte. Rientra prima i territori.");
|
||||
return;
|
||||
}
|
||||
|
||||
$proclamatore->delete();
|
||||
session()->flash('success', "Proclamatore {$proclamatore->nome_completo} spostato nel cestino.");
|
||||
}
|
||||
|
||||
public function anonimizza(int $id)
|
||||
{
|
||||
$proclamatore = Proclamatore::findOrFail($id);
|
||||
$proclamatore->anonimizza();
|
||||
session()->flash('success', "Dati personali del proclamatore anonimizzati (GDPR).");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Proclamatore::query();
|
||||
|
||||
if ($this->search !== '') {
|
||||
// In-memory filter because nome/cognome are encrypted
|
||||
$all = $query->get();
|
||||
$filtered = $all->filter(function ($p) {
|
||||
return str_contains(
|
||||
mb_strtolower($p->nome . ' ' . $p->cognome),
|
||||
mb_strtolower($this->search)
|
||||
);
|
||||
});
|
||||
|
||||
if ($this->filtroStato === 'attivo') {
|
||||
$filtered = $filtered->where('attivo', true);
|
||||
} elseif ($this->filtroStato === 'inattivo') {
|
||||
$filtered = $filtered->where('attivo', false);
|
||||
}
|
||||
|
||||
// Sort in-memory
|
||||
$filtered = $filtered->sortBy(function ($p) {
|
||||
return match ($this->sortField) {
|
||||
'nome' => mb_strtolower($p->nome),
|
||||
'cognome' => mb_strtolower($p->cognome),
|
||||
default => mb_strtolower($p->cognome),
|
||||
};
|
||||
}, SORT_REGULAR, $this->sortDirection === 'desc');
|
||||
|
||||
// Manual pagination
|
||||
$page = $this->getPage();
|
||||
$perPage = 20;
|
||||
$items = $filtered->slice(($page - 1) * $perPage, $perPage)->values();
|
||||
$proclamatori = new \Illuminate\Pagination\LengthAwarePaginator(
|
||||
$items, $filtered->count(), $perPage, $page,
|
||||
['path' => request()->url()]
|
||||
);
|
||||
} else {
|
||||
if ($this->filtroStato === 'attivo') {
|
||||
$query->where('attivo', true);
|
||||
} elseif ($this->filtroStato === 'inattivo') {
|
||||
$query->where('attivo', false);
|
||||
}
|
||||
|
||||
// cognome/nome encrypted, so sort in-memory
|
||||
$all = $query->get()->sortBy(function ($p) {
|
||||
return match ($this->sortField) {
|
||||
'nome' => mb_strtolower($p->nome),
|
||||
'cognome' => mb_strtolower($p->cognome),
|
||||
default => mb_strtolower($p->cognome),
|
||||
};
|
||||
}, SORT_REGULAR, $this->sortDirection === 'desc');
|
||||
|
||||
$page = $this->getPage();
|
||||
$perPage = 20;
|
||||
$items = $all->slice(($page - 1) * $perPage, $perPage)->values();
|
||||
$proclamatori = new \Illuminate\Pagination\LengthAwarePaginator(
|
||||
$items, $all->count(), $perPage, $page,
|
||||
['path' => request()->url()]
|
||||
);
|
||||
}
|
||||
|
||||
return view('livewire.proclamatori.proclamatore-index', [
|
||||
'proclamatori' => $proclamatori,
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
app/Livewire/Proclamatori/ProclamatoreShow.php
Normal file
42
app/Livewire/Proclamatori/ProclamatoreShow.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Proclamatori;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Proclamatore;
|
||||
use App\Models\Assegnazione;
|
||||
|
||||
class ProclamatoreShow extends Component
|
||||
{
|
||||
public Proclamatore $proclamatore;
|
||||
|
||||
public function mount(Proclamatore $proclamatore)
|
||||
{
|
||||
$this->proclamatore = $proclamatore;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$assegnazioni = Assegnazione::where('proclamatore_id', $this->proclamatore->id)
|
||||
->with(['territorio', 'annoTeocratico', 'campagna'])
|
||||
->orderByDesc('assigned_at')
|
||||
->get()
|
||||
->groupBy(fn($a) => $a->annoTeocratico->label);
|
||||
|
||||
$stats = [
|
||||
'totale_assegnazioni' => Assegnazione::where('proclamatore_id', $this->proclamatore->id)->count(),
|
||||
'attualmente_assegnati' => Assegnazione::where('proclamatore_id', $this->proclamatore->id)->aperte()->count(),
|
||||
'media_giorni' => round(
|
||||
Assegnazione::where('proclamatore_id', $this->proclamatore->id)
|
||||
->chiuse()
|
||||
->get()
|
||||
->avg(fn($a) => $a->giorni) ?? 0
|
||||
),
|
||||
];
|
||||
|
||||
return view('livewire.proclamatori.proclamatore-show', [
|
||||
'assegnazioniPerAnno' => $assegnazioni,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
}
|
||||
101
app/Livewire/Registro.php
Normal file
101
app/Livewire/Registro.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
class Registro extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $filtroAnno = '';
|
||||
public string $filtroZona = '';
|
||||
public string $filtroTipologia = '';
|
||||
public string $filtroStato = ''; // aperte, chiuse
|
||||
public string $sortField = 'assigned_at';
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'filtroAnno' => ['except' => ''],
|
||||
'filtroZona' => ['except' => ''],
|
||||
'filtroTipologia' => ['except' => ''],
|
||||
'filtroStato' => ['except' => ''],
|
||||
];
|
||||
|
||||
public function updatingSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function sortBy(string $field)
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortField = $field;
|
||||
$this->sortDirection = 'desc';
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Assegnazione::with(['territorio.zona', 'proclamatore', 'annoTeocratico', 'campagna']);
|
||||
|
||||
if ($this->filtroAnno) {
|
||||
$query->where('anno_teocratico_id', $this->filtroAnno);
|
||||
}
|
||||
|
||||
if ($this->filtroStato === 'aperte') {
|
||||
$query->aperte();
|
||||
} elseif ($this->filtroStato === 'chiuse') {
|
||||
$query->chiuse();
|
||||
}
|
||||
|
||||
if ($this->filtroZona) {
|
||||
$query->whereHas('territorio', fn($q) => $q->where('zona_id', $this->filtroZona));
|
||||
}
|
||||
|
||||
if ($this->filtroTipologia) {
|
||||
$query->whereHas('territorio', fn($q) => $q->where('tipologia_id', $this->filtroTipologia));
|
||||
}
|
||||
|
||||
$query->orderBy($this->sortField, $this->sortDirection);
|
||||
|
||||
// In-memory search for encrypted proclamatore fields / territorio numero
|
||||
if ($this->search !== '') {
|
||||
$all = $query->get();
|
||||
$filtered = $all->filter(function ($a) {
|
||||
$haystack = mb_strtolower(
|
||||
($a->territorio?->numero ?? '') . ' ' .
|
||||
($a->proclamatore?->nome ?? '') . ' ' .
|
||||
($a->proclamatore?->cognome ?? '')
|
||||
);
|
||||
return str_contains($haystack, mb_strtolower($this->search));
|
||||
});
|
||||
|
||||
$page = $this->getPage();
|
||||
$perPage = 25;
|
||||
$items = $filtered->slice(($page - 1) * $perPage, $perPage)->values();
|
||||
$assegnazioni = new \Illuminate\Pagination\LengthAwarePaginator(
|
||||
$items, $filtered->count(), $perPage, $page,
|
||||
['path' => request()->url()]
|
||||
);
|
||||
} else {
|
||||
$assegnazioni = $query->paginate(25);
|
||||
}
|
||||
|
||||
return view('livewire.registro', [
|
||||
'assegnazioni' => $assegnazioni,
|
||||
'anni' => AnnoTeocratico::orderByDesc('start_date')->get(),
|
||||
'zone' => Zona::attive()->orderBy('nome')->get(),
|
||||
'tipologie' => Tipologia::orderBy('nome')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
53
app/Livewire/Settings/SettingsEdit.php
Normal file
53
app/Livewire/Settings/SettingsEdit.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SettingsEdit extends Component
|
||||
{
|
||||
public string $congregazione_nome = '';
|
||||
public int $giorni_giacenza_prioritari = 180;
|
||||
public int $giorni_per_smarrito = 120;
|
||||
public int $audit_retention_days = 365;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$settings = Setting::instance();
|
||||
$this->congregazione_nome = $settings->congregazione_nome ?? '';
|
||||
$this->giorni_giacenza_prioritari = $settings->giorni_giacenza_prioritari ?? 180;
|
||||
$this->giorni_per_smarrito = $settings->giorni_per_smarrito ?? 120;
|
||||
$this->audit_retention_days = $settings->audit_retention_days ?? 365;
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'congregazione_nome' => 'required|string|max:255',
|
||||
'giorni_giacenza_prioritari' => 'required|integer|min:1|max:730',
|
||||
'giorni_per_smarrito' => 'required|integer|min:30|max:365',
|
||||
'audit_retention_days' => 'required|integer|min:30|max:3650',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$settings = Setting::instance();
|
||||
$settings->update([
|
||||
'congregazione_nome' => $this->congregazione_nome,
|
||||
'giorni_giacenza_prioritari' => $this->giorni_giacenza_prioritari,
|
||||
'giorni_per_smarrito' => $this->giorni_per_smarrito,
|
||||
'audit_retention_days' => $this->audit_retention_days,
|
||||
]);
|
||||
|
||||
session()->flash('success', 'Impostazioni aggiornate.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.settings-edit');
|
||||
}
|
||||
}
|
||||
68
app/Livewire/Settings/TipologieIndex.php
Normal file
68
app/Livewire/Settings/TipologieIndex.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
class TipologieIndex extends Component
|
||||
{
|
||||
public string $nuovaTipologia = '';
|
||||
public ?int $editingId = null;
|
||||
public string $editingNome = '';
|
||||
|
||||
public function addTipologia()
|
||||
{
|
||||
$this->validate(['nuovaTipologia' => 'required|string|max:100|unique:tipologie,nome']);
|
||||
Tipologia::create(['nome' => $this->nuovaTipologia, 'attivo' => true]);
|
||||
$this->nuovaTipologia = '';
|
||||
session()->flash('success', 'Tipologia aggiunta.');
|
||||
}
|
||||
|
||||
public function startEdit(int $id)
|
||||
{
|
||||
$tipo = Tipologia::findOrFail($id);
|
||||
$this->editingId = $id;
|
||||
$this->editingNome = $tipo->nome;
|
||||
}
|
||||
|
||||
public function saveEdit()
|
||||
{
|
||||
$this->validate(['editingNome' => "required|string|max:100|unique:tipologie,nome,{$this->editingId}"]);
|
||||
$tipo = Tipologia::findOrFail($this->editingId);
|
||||
$tipo->update(['nome' => $this->editingNome]);
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
session()->flash('success', 'Tipologia aggiornata.');
|
||||
}
|
||||
|
||||
public function cancelEdit()
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
}
|
||||
|
||||
public function toggleActive(int $id)
|
||||
{
|
||||
$tipo = Tipologia::findOrFail($id);
|
||||
$tipo->update(['attivo' => !$tipo->attivo]);
|
||||
}
|
||||
|
||||
public function deleteTipologia(int $id)
|
||||
{
|
||||
$tipo = Tipologia::findOrFail($id);
|
||||
if ($tipo->territori()->exists()) {
|
||||
session()->flash('error', "Impossibile eliminare: la tipologia '{$tipo->nome}' ha territori associati.");
|
||||
return;
|
||||
}
|
||||
$tipo->delete();
|
||||
session()->flash('success', "Tipologia '{$tipo->nome}' eliminata.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.tipologie-index', [
|
||||
'tipologie' => Tipologia::orderBy('nome')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
68
app/Livewire/Settings/ZoneIndex.php
Normal file
68
app/Livewire/Settings/ZoneIndex.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Zona;
|
||||
|
||||
class ZoneIndex extends Component
|
||||
{
|
||||
public string $nuovaZona = '';
|
||||
public ?int $editingId = null;
|
||||
public string $editingNome = '';
|
||||
|
||||
public function addZona()
|
||||
{
|
||||
$this->validate(['nuovaZona' => 'required|string|max:100|unique:zone,nome']);
|
||||
Zona::create(['nome' => $this->nuovaZona, 'attivo' => true]);
|
||||
$this->nuovaZona = '';
|
||||
session()->flash('success', 'Zona aggiunta.');
|
||||
}
|
||||
|
||||
public function startEdit(int $id)
|
||||
{
|
||||
$zona = Zona::findOrFail($id);
|
||||
$this->editingId = $id;
|
||||
$this->editingNome = $zona->nome;
|
||||
}
|
||||
|
||||
public function saveEdit()
|
||||
{
|
||||
$this->validate(['editingNome' => "required|string|max:100|unique:zone,nome,{$this->editingId}"]);
|
||||
$zona = Zona::findOrFail($this->editingId);
|
||||
$zona->update(['nome' => $this->editingNome]);
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
session()->flash('success', 'Zona aggiornata.');
|
||||
}
|
||||
|
||||
public function cancelEdit()
|
||||
{
|
||||
$this->editingId = null;
|
||||
$this->editingNome = '';
|
||||
}
|
||||
|
||||
public function toggleActive(int $id)
|
||||
{
|
||||
$zona = Zona::findOrFail($id);
|
||||
$zona->update(['attivo' => !$zona->attivo]);
|
||||
}
|
||||
|
||||
public function deleteZona(int $id)
|
||||
{
|
||||
$zona = Zona::findOrFail($id);
|
||||
if ($zona->territori()->exists()) {
|
||||
session()->flash('error', "Impossibile eliminare: la zona '{$zona->nome}' ha territori associati.");
|
||||
return;
|
||||
}
|
||||
$zona->delete();
|
||||
session()->flash('success', "Zona '{$zona->nome}' eliminata.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.zone-index', [
|
||||
'zone' => Zona::orderBy('nome')->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
app/Livewire/Setup/Wizard.php
Normal file
121
app/Livewire/Setup/Wizard.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Setup;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class Wizard extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public int $step = 1;
|
||||
|
||||
// Step 1
|
||||
public string $congregazione_nome = '';
|
||||
public $logo;
|
||||
|
||||
// Step 2
|
||||
public int $giorni_giacenza_da_assegnare = 120;
|
||||
public int $giorni_giacenza_prioritari = 180;
|
||||
public int $giorni_per_smarrito = 120;
|
||||
public int $home_limit_list = 10;
|
||||
|
||||
// Step 3 (admin creation if no users)
|
||||
public string $admin_name = '';
|
||||
public string $admin_email = '';
|
||||
public string $admin_password = '';
|
||||
public string $admin_password_confirmation = '';
|
||||
public bool $needsAdmin = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (Setting::isSetupComplete()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
$this->needsAdmin = User::count() <= 1;
|
||||
|
||||
$setting = Setting::first();
|
||||
if ($setting) {
|
||||
$this->congregazione_nome = $setting->congregazione_nome ?? '';
|
||||
$this->giorni_giacenza_da_assegnare = $setting->giorni_giacenza_da_assegnare;
|
||||
$this->giorni_giacenza_prioritari = $setting->giorni_giacenza_prioritari;
|
||||
$this->giorni_per_smarrito = $setting->giorni_per_smarrito;
|
||||
$this->home_limit_list = $setting->home_limit_list;
|
||||
}
|
||||
}
|
||||
|
||||
public function nextStep()
|
||||
{
|
||||
if ($this->step === 1) {
|
||||
$this->validate([
|
||||
'congregazione_nome' => 'required|string|max:255',
|
||||
'logo' => 'nullable|image|max:2048',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->step === 2) {
|
||||
$this->validate([
|
||||
'giorni_giacenza_da_assegnare' => 'required|integer|min:1|max:999',
|
||||
'giorni_giacenza_prioritari' => 'required|integer|min:1|max:999',
|
||||
'giorni_per_smarrito' => 'required|integer|min:1|max:999',
|
||||
'home_limit_list' => 'required|integer|min:1|max:100',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->step++;
|
||||
}
|
||||
|
||||
public function previousStep()
|
||||
{
|
||||
$this->step = max(1, $this->step - 1);
|
||||
}
|
||||
|
||||
public function finish()
|
||||
{
|
||||
if ($this->needsAdmin) {
|
||||
$this->validate([
|
||||
'admin_name' => 'required|string|max:255',
|
||||
'admin_email' => 'required|email|unique:users,email',
|
||||
'admin_password' => 'required|min:8|confirmed',
|
||||
]);
|
||||
}
|
||||
|
||||
$setting = Setting::instance();
|
||||
$setting->congregazione_nome = $this->congregazione_nome;
|
||||
$setting->giorni_giacenza_da_assegnare = $this->giorni_giacenza_da_assegnare;
|
||||
$setting->giorni_giacenza_prioritari = $this->giorni_giacenza_prioritari;
|
||||
$setting->giorni_per_smarrito = $this->giorni_per_smarrito;
|
||||
$setting->home_limit_list = $this->home_limit_list;
|
||||
$setting->setup_completed = true;
|
||||
|
||||
if ($this->logo) {
|
||||
$path = $this->logo->store('logos', 'public');
|
||||
$setting->logo_path = $path;
|
||||
}
|
||||
|
||||
$setting->save();
|
||||
|
||||
if ($this->needsAdmin && $this->admin_email) {
|
||||
$admin = User::create([
|
||||
'name' => $this->admin_name,
|
||||
'email' => $this->admin_email,
|
||||
'password' => Hash::make($this->admin_password),
|
||||
]);
|
||||
$admin->assignRole('amministratore');
|
||||
}
|
||||
|
||||
session()->flash('success', 'Setup completato con successo!');
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.setup.wizard')
|
||||
->layout('components.layouts.guest', ['title' => 'Setup iniziale']);
|
||||
}
|
||||
}
|
||||
45
app/Livewire/Territori/TerritorioCestino.php
Normal file
45
app/Livewire/Territori/TerritorioCestino.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Territori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Territorio;
|
||||
|
||||
class TerritorioCestino extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public function restore(int $id)
|
||||
{
|
||||
$territorio = Territorio::onlyTrashed()->findOrFail($id);
|
||||
$territorio->restore();
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($territorio)
|
||||
->log('restored');
|
||||
session()->flash('success', "Territorio {$territorio->numero} ripristinato.");
|
||||
}
|
||||
|
||||
public function forceDelete(int $id)
|
||||
{
|
||||
$territorio = Territorio::onlyTrashed()->findOrFail($id);
|
||||
|
||||
if ($territorio->assegnazioni()->exists()) {
|
||||
session()->flash('error', 'Impossibile eliminare definitivamente: il territorio ha assegnazioni nello storico.');
|
||||
return;
|
||||
}
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->withProperties(['numero' => $territorio->numero])
|
||||
->log('force_deleted_territorio');
|
||||
$territorio->forceDelete();
|
||||
session()->flash('success', 'Territorio eliminato definitivamente.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.territori.territorio-cestino', [
|
||||
'territori' => Territorio::onlyTrashed()->orderByDesc('deleted_at')->paginate(20),
|
||||
]);
|
||||
}
|
||||
}
|
||||
74
app/Livewire/Territori/TerritorioCreate.php
Normal file
74
app/Livewire/Territori/TerritorioCreate.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Territori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
class TerritorioCreate extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public string $numero = '';
|
||||
public ?int $zona_id = null;
|
||||
public ?int $tipologia_id = null;
|
||||
public string $note = '';
|
||||
public string $confini = '';
|
||||
public $pdf;
|
||||
public bool $prioritario = false;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'numero' => 'required|string|max:20|unique:territori,numero',
|
||||
'zona_id' => 'nullable|exists:zone,id',
|
||||
'tipologia_id' => 'nullable|exists:tipologie,id',
|
||||
'note' => 'nullable|string|max:5000',
|
||||
'confini' => 'nullable|string|max:5000',
|
||||
'pdf' => 'nullable|file|mimes:pdf|max:10240',
|
||||
'prioritario' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$data = [
|
||||
'numero' => $this->numero,
|
||||
'zona_id' => $this->zona_id,
|
||||
'tipologia_id' => $this->tipologia_id,
|
||||
'note' => $this->note ?: null,
|
||||
'confini' => $this->confini ?: null,
|
||||
'prioritario' => $this->prioritario,
|
||||
'attivo' => true,
|
||||
];
|
||||
|
||||
if ($this->pdf) {
|
||||
$data['pdf_path'] = $this->pdf->store('territori-pdf', 'public');
|
||||
}
|
||||
|
||||
$territorio = Territorio::create($data);
|
||||
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($territorio)
|
||||
->withProperties(['numero' => $territorio->numero])
|
||||
->log('created');
|
||||
|
||||
session()->flash('success', "Territorio {$territorio->numero} creato.");
|
||||
return redirect()->route('territori.index');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.territori.territorio-form', [
|
||||
'zone' => Zona::attive()->get(),
|
||||
'tipologie' => Tipologia::attive()->get(),
|
||||
'isEdit' => false,
|
||||
'title' => 'Nuovo Territorio',
|
||||
]);
|
||||
}
|
||||
}
|
||||
96
app/Livewire/Territori/TerritorioEdit.php
Normal file
96
app/Livewire/Territori/TerritorioEdit.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Territori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class TerritorioEdit extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public Territorio $territorio;
|
||||
public string $numero = '';
|
||||
public ?int $zona_id = null;
|
||||
public ?int $tipologia_id = null;
|
||||
public string $note = '';
|
||||
public string $confini = '';
|
||||
public $pdf;
|
||||
public bool $prioritario = false;
|
||||
|
||||
public function mount(Territorio $territorio)
|
||||
{
|
||||
$this->territorio = $territorio;
|
||||
$this->numero = $territorio->numero;
|
||||
$this->zona_id = $territorio->zona_id;
|
||||
$this->tipologia_id = $territorio->tipologia_id;
|
||||
$this->note = $territorio->note ?? '';
|
||||
$this->confini = $territorio->confini ?? '';
|
||||
$this->prioritario = $territorio->prioritario;
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'numero' => 'required|string|max:20|unique:territori,numero,' . $this->territorio->id,
|
||||
'zona_id' => 'nullable|exists:zone,id',
|
||||
'tipologia_id' => 'nullable|exists:tipologie,id',
|
||||
'note' => 'nullable|string|max:5000',
|
||||
'confini' => 'nullable|string|max:5000',
|
||||
'pdf' => 'nullable|file|mimes:pdf|max:10240',
|
||||
'prioritario' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$data = [
|
||||
'numero' => $this->numero,
|
||||
'zona_id' => $this->zona_id,
|
||||
'tipologia_id' => $this->tipologia_id,
|
||||
'note' => $this->note ?: null,
|
||||
'confini' => $this->confini ?: null,
|
||||
'prioritario' => $this->prioritario,
|
||||
];
|
||||
|
||||
if ($this->pdf) {
|
||||
// Remove old PDF
|
||||
if ($this->territorio->pdf_path) {
|
||||
Storage::disk('public')->delete($this->territorio->pdf_path);
|
||||
}
|
||||
$data['pdf_path'] = $this->pdf->store('territori-pdf', 'public');
|
||||
}
|
||||
|
||||
$this->territorio->update($data);
|
||||
|
||||
session()->flash('success', "Territorio {$this->territorio->numero} aggiornato.");
|
||||
return redirect()->route('territori.show', $this->territorio);
|
||||
}
|
||||
|
||||
public function removePdf()
|
||||
{
|
||||
if ($this->territorio->pdf_path) {
|
||||
Storage::disk('public')->delete($this->territorio->pdf_path);
|
||||
$this->territorio->update(['pdf_path' => null]);
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($this->territorio)
|
||||
->log('removed_pdf');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.territori.territorio-form', [
|
||||
'zone' => Zona::attive()->get(),
|
||||
'tipologie' => Tipologia::attive()->get(),
|
||||
'isEdit' => true,
|
||||
'title' => "Modifica Territorio {$this->territorio->numero}",
|
||||
]);
|
||||
}
|
||||
}
|
||||
105
app/Livewire/Territori/TerritorioIndex.php
Normal file
105
app/Livewire/Territori/TerritorioIndex.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Territori;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Zona;
|
||||
use App\Models\Tipologia;
|
||||
|
||||
class TerritorioIndex extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public string $search = '';
|
||||
public string $filterZona = '';
|
||||
public string $filterTipologia = '';
|
||||
public string $filterStato = '';
|
||||
public string $sortField = 'numero';
|
||||
public string $sortDirection = 'asc';
|
||||
|
||||
protected $queryString = [
|
||||
'search' => ['except' => ''],
|
||||
'filterZona' => ['except' => ''],
|
||||
'filterTipologia' => ['except' => ''],
|
||||
'filterStato' => ['except' => ''],
|
||||
];
|
||||
|
||||
public function updatingSearch()
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function sortBy(string $field)
|
||||
{
|
||||
if ($this->sortField === $field) {
|
||||
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$this->sortField = $field;
|
||||
$this->sortDirection = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleActive(Territorio $territorio)
|
||||
{
|
||||
$territorio->update(['attivo' => !$territorio->attivo]);
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($territorio)
|
||||
->withProperties(['attivo' => $territorio->attivo])
|
||||
->log($territorio->attivo ? 'activated' : 'deactivated');
|
||||
}
|
||||
|
||||
public function togglePriority(Territorio $territorio)
|
||||
{
|
||||
$territorio->update(['prioritario' => !$territorio->prioritario]);
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($territorio)
|
||||
->withProperties(['prioritario' => $territorio->prioritario])
|
||||
->log($territorio->prioritario ? 'set_priority' : 'unset_priority');
|
||||
}
|
||||
|
||||
public function deleteTerritorio(Territorio $territorio)
|
||||
{
|
||||
activity()->causedBy(auth()->user())
|
||||
->performedOn($territorio)
|
||||
->log('soft_deleted');
|
||||
$territorio->delete();
|
||||
session()->flash('success', "Territorio {$territorio->numero} spostato nel cestino.");
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$query = Territorio::with(['zona', 'tipologia', 'assegnazioneCorrente.proclamatore']);
|
||||
|
||||
if ($this->search) {
|
||||
$query->where('numero', 'like', "%{$this->search}%");
|
||||
}
|
||||
|
||||
if ($this->filterZona) {
|
||||
$query->where('zona_id', $this->filterZona);
|
||||
}
|
||||
|
||||
if ($this->filterTipologia) {
|
||||
$query->where('tipologia_id', $this->filterTipologia);
|
||||
}
|
||||
|
||||
if ($this->filterStato) {
|
||||
match ($this->filterStato) {
|
||||
'in_reparto' => $query->inReparto(),
|
||||
'assegnato' => $query->assegnato(),
|
||||
'da_rientrare' => $query->daRientrare(),
|
||||
'inattivo' => $query->where('attivo', false),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
$query->orderBy($this->sortField, $this->sortDirection);
|
||||
|
||||
return view('livewire.territori.territorio-index', [
|
||||
'territori' => $query->paginate(20),
|
||||
'zone' => Zona::attive()->get(),
|
||||
'tipologie' => Tipologia::attive()->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Livewire/Territori/TerritorioShow.php
Normal file
31
app/Livewire/Territori/TerritorioShow.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Territori;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Territorio;
|
||||
use App\Models\Assegnazione;
|
||||
use App\Models\AnnoTeocratico;
|
||||
|
||||
class TerritorioShow extends Component
|
||||
{
|
||||
public Territorio $territorio;
|
||||
|
||||
public function mount(Territorio $territorio)
|
||||
{
|
||||
$this->territorio = $territorio->load(['zona', 'tipologia']);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$assegnazioni = Assegnazione::where('territorio_id', $this->territorio->id)
|
||||
->with(['proclamatore', 'annoTeocratico', 'campagna', 'creatoDa', 'rientratoDa'])
|
||||
->orderByDesc('assigned_at')
|
||||
->get()
|
||||
->groupBy(fn($a) => $a->annoTeocratico->label);
|
||||
|
||||
return view('livewire.territori.territorio-show', [
|
||||
'assegnazioniPerAnno' => $assegnazioni,
|
||||
]);
|
||||
}
|
||||
}
|
||||
70
app/Models/AnnoTeocratico.php
Normal file
70
app/Models/AnnoTeocratico.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AnnoTeocratico extends Model
|
||||
{
|
||||
protected $table = 'anni_teocratici';
|
||||
|
||||
protected $fillable = ['label', 'start_date', 'end_date'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the theocratic year for a given date.
|
||||
*/
|
||||
public static function perData(Carbon $date = null): static
|
||||
{
|
||||
$date = $date ?? now();
|
||||
|
||||
if ($date->month >= 9) {
|
||||
$startYear = $date->year;
|
||||
$endYear = $date->year + 1;
|
||||
} else {
|
||||
$startYear = $date->year - 1;
|
||||
$endYear = $date->year;
|
||||
}
|
||||
|
||||
$label = "{$startYear}-{$endYear}";
|
||||
|
||||
return static::firstOrCreate(
|
||||
['label' => $label],
|
||||
[
|
||||
'start_date' => Carbon::create($startYear, 9, 1),
|
||||
'end_date' => Carbon::create($endYear, 8, 31),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theocratic year.
|
||||
*/
|
||||
public static function corrente(): static
|
||||
{
|
||||
return static::perData(now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of months elapsed since start of this theocratic year.
|
||||
*/
|
||||
public function getMesiTrascorsiAttribute(): int
|
||||
{
|
||||
$start = $this->start_date;
|
||||
$end = now()->lt($this->end_date) ? now() : $this->end_date;
|
||||
return max(1, $start->diffInMonths($end));
|
||||
}
|
||||
|
||||
public function assegnazioni()
|
||||
{
|
||||
return $this->hasMany(Assegnazione::class, 'anno_teocratico_id');
|
||||
}
|
||||
}
|
||||
113
app/Models/Assegnazione.php
Normal file
113
app/Models/Assegnazione.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Assegnazione extends Model
|
||||
{
|
||||
protected $table = 'assegnazioni';
|
||||
|
||||
protected $fillable = [
|
||||
'territorio_id',
|
||||
'proclamatore_id',
|
||||
'anno_teocratico_id',
|
||||
'assigned_at',
|
||||
'returned_at',
|
||||
'counted_in_campaign',
|
||||
'campaign_id',
|
||||
'note',
|
||||
'created_by',
|
||||
'returned_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'assigned_at' => 'date',
|
||||
'returned_at' => 'date',
|
||||
'counted_in_campaign' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Relationships ─────────────────────────────────────────
|
||||
|
||||
public function territorio()
|
||||
{
|
||||
return $this->belongsTo(Territorio::class, 'territorio_id')->withTrashed();
|
||||
}
|
||||
|
||||
public function proclamatore()
|
||||
{
|
||||
return $this->belongsTo(Proclamatore::class, 'proclamatore_id')->withTrashed();
|
||||
}
|
||||
|
||||
public function annoTeocratico()
|
||||
{
|
||||
return $this->belongsTo(AnnoTeocratico::class, 'anno_teocratico_id');
|
||||
}
|
||||
|
||||
public function campagna()
|
||||
{
|
||||
return $this->belongsTo(Campagna::class, 'campaign_id');
|
||||
}
|
||||
|
||||
public function creatoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function rientratoDa()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'returned_by');
|
||||
}
|
||||
|
||||
// ─── Computed ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Number of days between assignment and return (or today if still open).
|
||||
*/
|
||||
public function getGiorniAttribute(): int
|
||||
{
|
||||
$end = $this->returned_at ?? now();
|
||||
return Carbon::parse($this->assigned_at)->diffInDays($end);
|
||||
}
|
||||
|
||||
public function getIsApertaAttribute(): bool
|
||||
{
|
||||
return is_null($this->returned_at);
|
||||
}
|
||||
|
||||
// ─── Scopes ─────────────────────────────────────────────────
|
||||
|
||||
public function scopeAperte($query)
|
||||
{
|
||||
return $query->whereNull('returned_at');
|
||||
}
|
||||
|
||||
public function scopeChiuse($query)
|
||||
{
|
||||
return $query->whereNotNull('returned_at');
|
||||
}
|
||||
|
||||
public function scopePerAnnoTeocratico($query, $annoId)
|
||||
{
|
||||
return $query->where('anno_teocratico_id', $annoId);
|
||||
}
|
||||
|
||||
// ─── Business Logic ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if a campaign prompt should be shown when returning this assignment.
|
||||
* Returns the matching campaign or null.
|
||||
*/
|
||||
public function campagnaApplicabile(?\Carbon\Carbon $returnDate = null): ?Campagna
|
||||
{
|
||||
$returnDate = $returnDate ?? now();
|
||||
|
||||
return Campagna::where('start_date', '<=', $returnDate)
|
||||
->where('end_date', '>=', $this->assigned_at)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
94
app/Models/Campagna.php
Normal file
94
app/Models/Campagna.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Campagna extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected $table = 'campagne';
|
||||
|
||||
protected $fillable = ['start_date', 'end_date', 'descrizione'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['start_date', 'end_date', 'descrizione'])
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this campaign currently active?
|
||||
*/
|
||||
public function getIsAttivaAttribute(): bool
|
||||
{
|
||||
$today = now()->toDateString();
|
||||
return $this->start_date->toDateString() <= $today
|
||||
&& $this->end_date->toDateString() >= $today;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the currently active campaign (if any).
|
||||
*/
|
||||
public static function attiva(): ?static
|
||||
{
|
||||
return static::where('start_date', '<=', now())
|
||||
->where('end_date', '>=', now())
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assignments counted for this campaign.
|
||||
*/
|
||||
public function assegnazioniConteggiate()
|
||||
{
|
||||
return $this->hasMany(Assegnazione::class, 'campaign_id')
|
||||
->where('counted_in_campaign', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* All assignments with assigned_at in this campaign's range.
|
||||
*/
|
||||
public function assegnazioniNelRange()
|
||||
{
|
||||
return Assegnazione::where('assigned_at', '>=', $this->start_date)
|
||||
->where('assigned_at', '<=', $this->end_date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Campaign coverage percentage.
|
||||
* Numerator: assignments counted for campaign
|
||||
* Denominator: ALL assignments with assigned_at in campaign range (returned or not)
|
||||
*/
|
||||
public function getPercentualePercorrenzaAttribute(): float
|
||||
{
|
||||
$totaleNelRange = $this->assegnazioniNelRange()->count();
|
||||
|
||||
if ($totaleNelRange === 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$conteggiate = $this->assegnazioniConteggiate()->count();
|
||||
|
||||
return round(($conteggiate / $totaleNelRange) * 100, 1);
|
||||
}
|
||||
|
||||
public function scopeCompletate($query)
|
||||
{
|
||||
return $query->where('end_date', '<', now())->orderByDesc('end_date');
|
||||
}
|
||||
}
|
||||
70
app/Models/Proclamatore.php
Normal file
70
app/Models/Proclamatore.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Proclamatore extends Model
|
||||
{
|
||||
use SoftDeletes, LogsActivity;
|
||||
|
||||
protected $table = 'proclamatori';
|
||||
|
||||
protected $fillable = ['nome', 'cognome', 'attivo'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'nome' => 'encrypted',
|
||||
'cognome' => 'encrypted',
|
||||
'attivo' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['attivo']) // Do NOT log nome/cognome in audit (encrypted, GDPR)
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full name (decrypted, only visible in PHP/UI).
|
||||
*/
|
||||
public function getNomeCompletoAttribute(): string
|
||||
{
|
||||
return trim($this->cognome . ' ' . $this->nome);
|
||||
}
|
||||
|
||||
public function assegnazioni()
|
||||
{
|
||||
return $this->hasMany(Assegnazione::class, 'proclamatore_id');
|
||||
}
|
||||
|
||||
public function assegnazioniAperte()
|
||||
{
|
||||
return $this->hasMany(Assegnazione::class, 'proclamatore_id')
|
||||
->whereNull('returned_at');
|
||||
}
|
||||
|
||||
public function scopeAttivi($query)
|
||||
{
|
||||
return $query->where('attivo', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize this proclamatore (GDPR right to be forgotten).
|
||||
*/
|
||||
public function anonimizza(): void
|
||||
{
|
||||
$this->nome = 'Anonimo';
|
||||
$this->cognome = 'Proclamatore #' . $this->id;
|
||||
$this->attivo = false;
|
||||
$this->save();
|
||||
$this->delete(); // soft delete
|
||||
}
|
||||
}
|
||||
56
app/Models/Setting.php
Normal file
56
app/Models/Setting.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'congregazione_nome',
|
||||
'logo_path',
|
||||
'giorni_giacenza_da_assegnare',
|
||||
'giorni_giacenza_prioritari',
|
||||
'giorni_per_smarrito',
|
||||
'home_limit_list',
|
||||
'audit_retention_days',
|
||||
'setup_completed',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'setup_completed' => 'boolean',
|
||||
'giorni_giacenza_da_assegnare' => 'integer',
|
||||
'giorni_giacenza_prioritari' => 'integer',
|
||||
'giorni_per_smarrito' => 'integer',
|
||||
'home_limit_list' => 'integer',
|
||||
'audit_retention_days' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton settings instance (first row).
|
||||
*/
|
||||
public static function instance(): static
|
||||
{
|
||||
return static::firstOrCreate([], [
|
||||
'giorni_giacenza_da_assegnare' => 120,
|
||||
'giorni_giacenza_prioritari' => 180,
|
||||
'giorni_per_smarrito' => 120,
|
||||
'home_limit_list' => 10,
|
||||
'audit_retention_days' => 730,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function isSetupComplete(): bool
|
||||
{
|
||||
$setting = static::first();
|
||||
return $setting && $setting->setup_completed;
|
||||
}
|
||||
|
||||
public static function getValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return static::instance()->{$key} ?? $default;
|
||||
}
|
||||
}
|
||||
219
app/Models/Territorio.php
Normal file
219
app/Models/Territorio.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Territorio extends Model
|
||||
{
|
||||
use SoftDeletes, LogsActivity;
|
||||
|
||||
protected $table = 'territori';
|
||||
|
||||
protected $fillable = [
|
||||
'numero',
|
||||
'zona_id',
|
||||
'tipologia_id',
|
||||
'note',
|
||||
'confini',
|
||||
'pdf_path',
|
||||
'attivo',
|
||||
'prioritario',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'attivo' => 'boolean',
|
||||
'prioritario' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['numero', 'zona_id', 'tipologia_id', 'attivo', 'prioritario', 'pdf_path'])
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
// ─── Relationships ─────────────────────────────────────────
|
||||
|
||||
public function zona()
|
||||
{
|
||||
return $this->belongsTo(Zona::class, 'zona_id');
|
||||
}
|
||||
|
||||
public function tipologia()
|
||||
{
|
||||
return $this->belongsTo(Tipologia::class, 'tipologia_id');
|
||||
}
|
||||
|
||||
public function assegnazioni()
|
||||
{
|
||||
return $this->hasMany(Assegnazione::class, 'territorio_id');
|
||||
}
|
||||
|
||||
public function assegnazioneCorrente()
|
||||
{
|
||||
return $this->hasOne(Assegnazione::class, 'territorio_id')
|
||||
->whereNull('returned_at')
|
||||
->latestOfMany('assigned_at');
|
||||
}
|
||||
|
||||
public function ultimaAssegnazione()
|
||||
{
|
||||
return $this->hasOne(Assegnazione::class, 'territorio_id')
|
||||
->latestOfMany('assigned_at');
|
||||
}
|
||||
|
||||
// ─── Computed State ─────────────────────────────────────────
|
||||
|
||||
public function getStatoAttribute(): string
|
||||
{
|
||||
if (!$this->attivo) {
|
||||
return 'inattivo';
|
||||
}
|
||||
|
||||
$corrente = $this->assegnazioneCorrente;
|
||||
if ($corrente) {
|
||||
$giorniAssegnato = Carbon::parse($corrente->assigned_at)->diffInDays(now());
|
||||
$sogliaSmarrito = Setting::getValue('giorni_per_smarrito', 120);
|
||||
|
||||
if ($giorniAssegnato > $sogliaSmarrito) {
|
||||
return 'da_rientrare';
|
||||
}
|
||||
return 'assegnato';
|
||||
}
|
||||
|
||||
return 'in_reparto';
|
||||
}
|
||||
|
||||
public function getAssegnatarioAttribute(): ?Proclamatore
|
||||
{
|
||||
return $this->assegnazioneCorrente?->proclamatore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Days since last return (or creation if never assigned).
|
||||
*/
|
||||
public function getGiorniGiacenzaAttribute(): int
|
||||
{
|
||||
$ultima = $this->ultimaAssegnazione;
|
||||
|
||||
if ($ultima && $ultima->returned_at) {
|
||||
return Carbon::parse($ultima->returned_at)->diffInDays(now());
|
||||
}
|
||||
|
||||
if (!$ultima) {
|
||||
return $this->created_at->diffInDays(now());
|
||||
}
|
||||
|
||||
// Currently assigned, no giacenza concept
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this territory "prioritario"?
|
||||
* Manual flag OR giacenza exceeds threshold (threshold always wins).
|
||||
*/
|
||||
public function getIsPrioritarioAttribute(): bool
|
||||
{
|
||||
if (!$this->attivo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->prioritario) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Threshold-based priority (only when in reparto)
|
||||
if ($this->stato === 'in_reparto') {
|
||||
$soglia = Setting::getValue('giorni_giacenza_prioritari', 180);
|
||||
return $this->giorni_giacenza > $soglia;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Scopes ─────────────────────────────────────────────────
|
||||
|
||||
public function scopeAttivi($query)
|
||||
{
|
||||
return $query->where('attivo', true);
|
||||
}
|
||||
|
||||
public function scopeInReparto($query)
|
||||
{
|
||||
return $query->attivi()
|
||||
->whereDoesntHave('assegnazioni', function ($q) {
|
||||
$q->whereNull('returned_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeAssegnato($query)
|
||||
{
|
||||
return $query->attivi()
|
||||
->whereHas('assegnazioni', function ($q) {
|
||||
$q->whereNull('returned_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeDaRientrare($query)
|
||||
{
|
||||
$soglia = Setting::getValue('giorni_per_smarrito', 120);
|
||||
|
||||
return $query->attivi()
|
||||
->whereHas('assegnazioni', function ($q) use ($soglia) {
|
||||
$q->whereNull('returned_at')
|
||||
->where('assigned_at', '<=', now()->subDays($soglia));
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeDaAssegnare($query)
|
||||
{
|
||||
$soglia = Setting::getValue('giorni_giacenza_da_assegnare', 120);
|
||||
|
||||
return $query->inReparto()
|
||||
->where(function ($q) use ($soglia) {
|
||||
// Territories whose last assignment returned > soglia days ago
|
||||
$q->whereHas('assegnazioni', function ($sub) use ($soglia) {
|
||||
$sub->whereNotNull('returned_at')
|
||||
->where('returned_at', '<=', now()->subDays($soglia))
|
||||
->whereRaw('id = (SELECT MAX(a2.id) FROM assegnazioni a2 WHERE a2.territorio_id = assegnazioni.territorio_id)');
|
||||
})
|
||||
// Or territories never assigned, created > soglia days ago
|
||||
->orWhere(function ($sub) use ($soglia) {
|
||||
$sub->doesntHave('assegnazioni')
|
||||
->where('created_at', '<=', now()->subDays($soglia));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function scopePrioritari($query)
|
||||
{
|
||||
$soglia = Setting::getValue('giorni_giacenza_prioritari', 180);
|
||||
|
||||
return $query->inReparto()
|
||||
->where(function ($q) use ($soglia) {
|
||||
// Manual priority flag
|
||||
$q->where('prioritario', true)
|
||||
// OR threshold-based
|
||||
->orWhere(function ($sub) use ($soglia) {
|
||||
$sub->whereHas('assegnazioni', function ($a) use ($soglia) {
|
||||
$a->whereNotNull('returned_at')
|
||||
->where('returned_at', '<=', now()->subDays($soglia))
|
||||
->whereRaw('id = (SELECT MAX(a2.id) FROM assegnazioni a2 WHERE a2.territorio_id = assegnazioni.territorio_id)');
|
||||
})
|
||||
->orWhere(function ($never) use ($soglia) {
|
||||
$never->doesntHave('assegnazioni')
|
||||
->where('created_at', '<=', now()->subDays($soglia));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
41
app/Models/Tipologia.php
Normal file
41
app/Models/Tipologia.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Tipologia extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected $table = 'tipologie';
|
||||
|
||||
protected $fillable = ['nome', 'attivo'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'attivo' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['nome', 'attivo'])
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function territori()
|
||||
{
|
||||
return $this->hasMany(Territorio::class, 'tipologia_id');
|
||||
}
|
||||
|
||||
public function scopeAttive($query)
|
||||
{
|
||||
return $query->where('attivo', true);
|
||||
}
|
||||
}
|
||||
42
app/Models/User.php
Normal file
42
app/Models/User.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable, HasRoles, LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['name', 'email'])
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
}
|
||||
41
app/Models/Zona.php
Normal file
41
app/Models/Zona.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Zona extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
protected $table = 'zone';
|
||||
|
||||
protected $fillable = ['nome', 'attivo'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'attivo' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['nome', 'attivo'])
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function territori()
|
||||
{
|
||||
return $this->hasMany(Territorio::class, 'zona_id');
|
||||
}
|
||||
|
||||
public function scopeAttive($query)
|
||||
{
|
||||
return $query->where('attivo', true);
|
||||
}
|
||||
}
|
||||
27
app/Providers/AppServiceProvider.php
Normal file
27
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Auto-generate APP_KEY if missing
|
||||
if (empty(config('app.key'))) {
|
||||
Artisan::call('key:generate', ['--force' => true]);
|
||||
}
|
||||
|
||||
Gate::before(function ($user, $ability) {
|
||||
return $user->hasRole('amministratore') ? true : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user