Blocco 2 - Esiti delle azioni Livewire come avvisi (trait Avvisa) invece di flash che comparivano alla pagina dopo. - PDF, miniature e versioni dei territori non più serviti da nginx su /storage: TerritorioFileController con login, permessi e cache privata. - Ripristino tutto o niente: verifica dell'archivio, una transazione con import, piantine, blocco account e scambio dei file annullabile. - Backup formato 2 con piantine, storico PDF e area KML. - Posto unico preso in un passo (Cache::lock) e rispettato anche da import XML e PDF. - Import PDF da cartella: abbinamento corretto (1-A, 12 B, ambigui), caricamento a lotti, file non PDF ignorati, PDF sostituito nello storico. - L'import XML elimina i file dei territori che non usa più; comando territori:file-orfani per quelli accumulati prima. Blocco 3 - Password attuale richiesta al cambio volontario; AuthenticateSession chiude le sessioni aperte altrove. - Niente nomi dei proclamatori in chiaro nel registro attività. - Export XML senza remember_token; chi riceve l'hash dal file deve cambiare password. - Campagne: ultimo giorno incluso, percentuale sui territori distinti. - Coda: retry_after 960 s, stop_grace_period 16m, Redis volatile-lru, impostazioni rilette a ogni job. - Azzeramento che cancella anche piantine, area KML e anteprime. - Pulsanti profilo e menu che non si nascondevano; icona del sito. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
244 lines
7.8 KiB
PHP
244 lines
7.8 KiB
PHP
<?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',
|
|
'thumbnail_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()
|
|
{
|
|
// Il filtro sulle aperte va nella closure di ofMany: scritto fuori con
|
|
// whereNull() vale solo per la query esterna, mentre il MAX(assigned_at)
|
|
// del sottoquery si calcola su tutte le assegnazioni. Bastava una
|
|
// chiusa più recente dell'aperta per far risultare il territorio
|
|
// libero mentre era fuori, e quindi assegnabile una seconda volta.
|
|
return $this->hasOne(Assegnazione::class, 'territorio_id')
|
|
->ofMany(['assigned_at' => 'max', 'id' => 'max'], fn ($q) => $q->whereNull('returned_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)->startOfDay()->diffInDays(today());
|
|
}
|
|
|
|
if (!$ultima) {
|
|
return $this->created_at->startOfDay()->diffInDays(today());
|
|
}
|
|
|
|
// 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('assegnazioneCorrente', function ($q) use ($soglia) {
|
|
$q->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));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── Indirizzi dei file (serviti da TerritorioFileController) ──────────
|
|
|
|
/**
|
|
* ?v= cambia con il file: l'indirizzo resta per territorio, e senza la
|
|
* cache del browser mostrerebbe il PDF di prima dopo una sostituzione.
|
|
*/
|
|
public function pdfUrl(): ?string
|
|
{
|
|
return $this->pdf_path
|
|
? route('territori.file.pdf', ['territorio' => $this->id, 'v' => substr(md5($this->pdf_path), 0, 8)])
|
|
: null;
|
|
}
|
|
|
|
public function miniaturaUrl(): ?string
|
|
{
|
|
return $this->thumbnail_path
|
|
? route('territori.file.miniatura', ['territorio' => $this->id, 'v' => substr(md5($this->thumbnail_path), 0, 8)])
|
|
: null;
|
|
}
|
|
}
|