Files
termanager2/app/Models/Proclamatore.php
Francesco Picone fda428f242 ++ scelta di territori e proclamatori con ricerca
Al posto delle tendine (Assegna, Registro, editor delle piantine) un
campo in cui si scrive e compaiono le proposte più vicine: uguale, inizia
con, contiene, errore di battitura ("forse cercavi"); parole in qualsiasi
ordine, senza badare a maiuscole e accenti. Campo vuoto: la tendina
completa. Fatto anche per il telefono (16px, voci da 44px, scelta al tocco).

- Logica pura in resources/js/scelta.js con test node (tests/js).
- Opzioni dal server: Territorio/Proclamatore::opzioneScelta(); i
  territori già assegnati si trovano ("già assegnato a …") ma non si
  scelgono. Nomi cifrati ordinati e cercati decifrati, mai in SQL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:35:56 +02:00

144 lines
4.4 KiB
PHP

<?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', 'email', 'telefono', 'attivo'];
protected function casts(): array
{
return [
'nome' => 'encrypted',
'cognome' => 'encrypted',
// Facoltativi. Cifrati come nome e cognome: dati personali.
'email' => 'encrypted',
'telefono' => 'encrypted',
'attivo' => 'boolean',
];
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['attivo']) // Do NOT log nome/cognome/email/telefono in audit (encrypted, GDPR)
->logOnlyDirty()
->dontSubmitEmptyLogs();
}
/**
* Full name (decrypted, only visible in PHP/UI).
*/
public function getNomeCompletoAttribute(): string
{
return trim($this->cognome . ' ' . $this->nome);
}
/**
* Il numero come lo vuole WhatsApp: solo cifre, con il prefisso
* internazionale. Un numero senza prefisso si considera italiano: un
* cellulare scritto "333 1234567" diventa 393331234567. Null se non c'è un
* numero utilizzabile.
*/
public function telefonoInternazionale(): ?string
{
$grezzo = trim((string) $this->telefono);
if ($grezzo === '') {
return null;
}
$cifre = preg_replace('/\D+/', '', $grezzo);
if (str_starts_with($cifre, '00') && ! str_starts_with($grezzo, '+')) {
$cifre = substr($cifre, 2);
} elseif (! str_starts_with($grezzo, '+') && (! str_starts_with($cifre, '39') || strlen($cifre) <= 10)) {
// Senza prefisso: cellulari (3…) e fissi (0…) italiani. Un
// "39…" di 11 cifre o più ha già il prefisso, scritto senza "+".
$cifre = '39' . $cifre;
}
return strlen($cifre) >= 8 && strlen($cifre) <= 15 ? $cifre : null;
}
/**
* Il link che apre una chat WhatsApp con il proclamatore, con un messaggio
* già scritto se c'è. Null se non ha un numero utilizzabile.
*/
public function whatsappUrl(?string $messaggio = null): ?string
{
$numero = $this->telefonoInternazionale();
if ($numero === null) {
return null;
}
return 'https://wa.me/' . $numero . ($messaggio !== null && $messaggio !== '' ? '?text=' . rawurlencode($messaggio) : '');
}
/**
* L'opzione per il campo di scelta con ricerca (<x-scelta>): si cerca per
* cognome e nome. Nome e cognome sono cifrati nel database: la ricerca
* avviene sui valori già decifrati, nella pagina, mai in SQL.
*/
public function opzioneScelta(?string $motivo = null): array
{
return array_filter([
'valore' => (string) $this->id,
'etichetta' => $this->nome_completo,
'chiavi' => [(string) $this->cognome, (string) $this->nome],
'motivo' => $motivo,
], fn ($v) => $v !== null);
}
/**
* Le opzioni di un elenco di proclamatori, in ordine di cognome e nome.
* L'ordine si fa qui: in SQL ordinerebbe i testi cifrati.
*/
public static function opzioniScelta(iterable $proclamatori): array
{
return collect($proclamatori)
->sortBy(fn (self $p) => mb_strtolower($p->cognome . ' ' . $p->nome))
->map(fn (self $p) => $p->opzioneScelta($p->attivo ? null : 'non attivo'))
->values()
->all();
}
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->email = null;
$this->telefono = null;
$this->attivo = false;
$this->save();
$this->delete(); // soft delete
}
}