'date', 'returned_at' => 'date', 'counted_in_campaign' => 'boolean', 'link_sent' => 'boolean', ]; } protected static function booted(): void { // Assegnare un territorio ne azzera il flag "prioritario": una volta in // carico a un proclamatore (assegnazione aperta) non ha più senso // segnalarlo come prioritario. La priorità automatica per soglia di // giacenza riguarda solo i territori in reparto, quindi resta il solo // flag manuale da spegnere. Vale per ogni via di creazione (pagina // Assegna, CRUD del Registro, import XML di assegnazioni aperte). static::created(function (self $assegnazione): void { if ($assegnazione->returned_at !== null) { return; } $territorio = $assegnazione->territorio; if ($territorio && $territorio->prioritario) { $territorio->update(['prioritario' => false]); } }); } // ─── 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); } /** * Scadenza dell'assegnazione: data entro cui il territorio andrebbe * riconsegnato, cioè assigned_at + la soglia "giorni_per_smarrito" (la * stessa oltre cui Territorio::stato diventa "da_rientrare"). Null per le * assegnazioni già rientrate. */ public function getDataConsegnaPresuntaAttribute(): ?Carbon { if ($this->returned_at !== null) { return null; } $soglia = (int) Setting::getValue('giorni_per_smarrito', 120); return Carbon::parse($this->assigned_at)->addDays($soglia); } /** * Giorni che mancano alla scadenza: positivo se ancora entro i termini, * negativo (giorni di ritardo) se già scaduta. Null se rientrata. */ public function getGiorniAllaScadenzaAttribute(): ?int { $scadenza = $this->data_consegna_presunta?->copy()->startOfDay(); if (! $scadenza) { return null; } $oggi = Carbon::today(); $giorni = (int) $oggi->diffInDays($scadenza); return $oggi->lessThanOrEqualTo($scadenza) ? $giorni : -$giorni; } public function ensurePdfAccessCode(): string { if ($this->pdf_access_code) { return $this->pdf_access_code; } do { $code = strtoupper(Str::random(12)); } while (static::query()->where('pdf_access_code', $code)->exists()); $this->forceFill(['pdf_access_code' => $code])->saveQuietly(); return $code; } public function temporaryPdfViewerUrl(): ?string { if (! $this->is_aperta || ! $this->territorio?->pdf_path) { return null; } return route('assignments.pdf.viewer', [ 'assignment' => $this->id, 'code' => $this->ensurePdfAccessCode(), ]); } public function shortPdfUrl(): ?string { if (! $this->is_aperta || ! $this->territorio?->pdf_path) { return null; } return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]); } public function markLinkSent(): void { $this->forceFill(['link_sent' => true])->saveQuietly(); } // ─── 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 ───────────────────────────────────────── /** * Registra nell'audit log la scelta dell'operatore sul conteggio in * campagna: evento dedicato "campagna_percorso", leggibile senza dover * interpretare le proprietà dell'evento "returned". */ public function logCampagnaPercorso(?Campagna $campagna, bool $percorso, string $origine): void { $this->loadMissing(['territorio', 'proclamatore']); $giorni = $this->giorniPercorrenza(); $soglia = static::sogliaPercorrenzaElevata(); activity()->causedBy(auth()->user()) ->performedOn($this) ->withProperties([ 'territorio' => $this->territorio?->numero, 'proclamatore' => $this->proclamatore?->nome_completo, 'campagna' => $campagna?->descrizione, 'percorso_in_campagna' => $percorso ? 'Sì' : 'No', 'giorni_percorrenza' => $giorni, 'avviso_tempo_elevato' => $giorni > $soglia ? "Sì (soglia {$soglia} giorni)" : 'No', 'origine' => $origine, ]) ->log('campagna_percorso'); } /** * Giorni tra assegnazione e rientro (o la data indicata, o oggi). */ public function giorniPercorrenza(?Carbon $returnDate = null): int { $end = $returnDate ?? $this->returned_at ?? now(); return (int) Carbon::parse($this->assigned_at)->startOfDay()->diffInDays(Carbon::parse($end)->startOfDay()); } /** * Oltre questa soglia il tempo di percorrenza è considerato molto alto: * è la stessa soglia "giorni_per_smarrito" dello stato "da_rientrare". */ public static function sogliaPercorrenzaElevata(): int { return (int) Setting::getValue('giorni_per_smarrito', 120); } /** * 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())->toDateString(); $assignedAt = $this->assigned_at->toDateString(); // Il territorio può essere conteggiato per una campagna se è rimasto // in mano al proclamatore almeno in parte del periodo: assegnato // durante la campagna, rientrato durante la campagna, oppure fuori per // tutta la sua durata (assegnato prima e rientrato dopo). Se sia stato // davvero percorso lo decide l'operatore con la spunta. // Se più campagne corrispondono si propone la più recente. return Campagna::where('start_date', '<=', $returnDate) ->where('end_date', '>=', $assignedAt) ->orderByDesc('start_date') ->first(); } }