++ fix prioritario display and add link sent toggle
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
|
|||||||
use App\Models\Assegnazione;
|
use App\Models\Assegnazione;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
@@ -14,6 +15,10 @@ class AssignmentPdfController extends Controller
|
|||||||
{
|
{
|
||||||
$this->validateAccess($assignment, $code);
|
$this->validateAccess($assignment, $code);
|
||||||
|
|
||||||
|
if ($expired = $this->linkScaduto($assignment)) {
|
||||||
|
return $expired;
|
||||||
|
}
|
||||||
|
|
||||||
$pdfUrl = route('assignments.pdf.file', [
|
$pdfUrl = route('assignments.pdf.file', [
|
||||||
'assignment' => $assignment->id,
|
'assignment' => $assignment->id,
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
@@ -25,10 +30,14 @@ class AssignmentPdfController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse
|
public function file(Request $request, Assegnazione $assignment, string $code): StreamedResponse|View
|
||||||
{
|
{
|
||||||
$this->validateAccess($assignment, $code);
|
$this->validateAccess($assignment, $code);
|
||||||
|
|
||||||
|
if ($expired = $this->linkScaduto($assignment)) {
|
||||||
|
return $expired;
|
||||||
|
}
|
||||||
|
|
||||||
$pdfPath = $assignment->territorio?->pdf_path;
|
$pdfPath = $assignment->territorio?->pdf_path;
|
||||||
abort_unless($pdfPath && Storage::disk('public')->exists($pdfPath), 404);
|
abort_unless($pdfPath && Storage::disk('public')->exists($pdfPath), 404);
|
||||||
|
|
||||||
@@ -48,4 +57,22 @@ class AssignmentPdfController extends Controller
|
|||||||
abort_unless($assignment->is_aperta, 403);
|
abort_unless($assignment->is_aperta, 403);
|
||||||
abort_unless($assignment->territorio?->pdf_path, 404);
|
abort_unless($assignment->territorio?->pdf_path, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function linkScaduto(Assegnazione $assignment): ?View
|
||||||
|
{
|
||||||
|
if (auth()->check()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ttlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1));
|
||||||
|
|
||||||
|
if ($assignment->assigned_at->copy()->addMonths($ttlMonths)->isPast()) {
|
||||||
|
return view('assignments.link-scaduto', [
|
||||||
|
'numero' => $assignment->territorio?->numero,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,32 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Assegnazione;
|
use App\Models\Assegnazione;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
class ShortPdfLinkController extends Controller
|
class ShortPdfLinkController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(string $code): RedirectResponse
|
public function __invoke(string $code): RedirectResponse|View
|
||||||
{
|
{
|
||||||
$assignment = Assegnazione::where('pdf_access_code', $code)->firstOrFail();
|
$assignment = Assegnazione::where('pdf_access_code', $code)->firstOrFail();
|
||||||
|
|
||||||
abort_unless($assignment->is_aperta, 403);
|
abort_unless($assignment->is_aperta, 403);
|
||||||
abort_unless($assignment->territorio?->pdf_path, 404);
|
abort_unless($assignment->territorio?->pdf_path, 404);
|
||||||
|
|
||||||
|
// Unauthenticated users (proclamatori) are subject to link TTL
|
||||||
|
if (! auth()->check()) {
|
||||||
|
$ttlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1));
|
||||||
|
if ($assignment->assigned_at->copy()->addMonths($ttlMonths)->isPast()) {
|
||||||
|
return view('assignments.link-scaduto', [
|
||||||
|
'numero' => $assignment->territorio?->numero,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('assignments.pdf.viewer', [
|
return redirect()->route('assignments.pdf.viewer', [
|
||||||
'assignment' => $assignment->id,
|
'assignment' => $assignment->id,
|
||||||
'code' => $code,
|
'code' => $code,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ class Assegna extends Component
|
|||||||
return $this->redirect(route('territori.show', $territorio), navigate: true);
|
return $this->redirect(route('territori.show', $territorio), navigate: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toggleLinkSent(int $assegnazioneId): void
|
||||||
|
{
|
||||||
|
$this->authorize('territori.assign');
|
||||||
|
$assegnazione = Assegnazione::findOrFail($assegnazioneId);
|
||||||
|
$assegnazione->forceFill(['link_sent' => ! $assegnazione->link_sent])->saveQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
#[Computed]
|
#[Computed]
|
||||||
public function selectedThumbnailUrl(): ?string
|
public function selectedThumbnailUrl(): ?string
|
||||||
{
|
{
|
||||||
@@ -122,10 +129,13 @@ class Assegna extends Component
|
|||||||
->get()
|
->get()
|
||||||
->sortBy(fn($a) => (int) $a->territorio?->numero);
|
->sortBy(fn($a) => (int) $a->territorio?->numero);
|
||||||
|
|
||||||
|
$linkTtlMonths = max(1, (int) \App\Models\Setting::getValue('assignment_link_ttl_hours', 1));
|
||||||
|
|
||||||
return view('livewire.assegnazioni.assegna', [
|
return view('livewire.assegnazioni.assegna', [
|
||||||
'territoriDisponibili' => $territoriDisponibili,
|
'territoriDisponibili' => $territoriDisponibili,
|
||||||
'proclamatoriAttivi' => $proclamatoriAttivi,
|
'proclamatoriAttivi' => $proclamatoriAttivi,
|
||||||
'assegnazioniAperte' => $assegnazioniAperte,
|
'assegnazioniAperte' => $assegnazioniAperte,
|
||||||
|
'linkTtlMonths' => $linkTtlMonths,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ class CampagnaShow extends Component
|
|||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
// All assignments with returned_at in campaign range that were counted
|
// Assignments counted for this campaign:
|
||||||
$conteggiate = Assegnazione::where('campagna_id', $this->campagna->id)
|
// - assigned on or after campaign start
|
||||||
|
// - linked to this campaign (campaign_id), regardless of returned_at (retroactive returns allowed)
|
||||||
|
$conteggiate = Assegnazione::where('campaign_id', $this->campagna->id)
|
||||||
->where('counted_in_campaign', true)
|
->where('counted_in_campaign', true)
|
||||||
|
->where('assigned_at', '>=', $this->campagna->start_date)
|
||||||
->with(['territorio', 'proclamatore'])
|
->with(['territorio', 'proclamatore'])
|
||||||
->orderBy('returned_at')
|
->orderBy('returned_at')
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Assegnazione extends Model
|
|||||||
'counted_in_campaign',
|
'counted_in_campaign',
|
||||||
'campaign_id',
|
'campaign_id',
|
||||||
'pdf_access_code',
|
'pdf_access_code',
|
||||||
|
'link_sent',
|
||||||
'note',
|
'note',
|
||||||
'created_by',
|
'created_by',
|
||||||
'returned_by',
|
'returned_by',
|
||||||
@@ -30,6 +31,7 @@ class Assegnazione extends Model
|
|||||||
'assigned_at' => 'date',
|
'assigned_at' => 'date',
|
||||||
'returned_at' => 'date',
|
'returned_at' => 'date',
|
||||||
'counted_in_campaign' => 'boolean',
|
'counted_in_campaign' => 'boolean',
|
||||||
|
'link_sent' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +119,11 @@ class Assegnazione extends Model
|
|||||||
return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]);
|
return route('assignments.pdf.short', ['code' => $this->ensurePdfAccessCode()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function markLinkSent(): void
|
||||||
|
{
|
||||||
|
$this->forceFill(['link_sent' => true])->saveQuietly();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Scopes ─────────────────────────────────────────────────
|
// ─── Scopes ─────────────────────────────────────────────────
|
||||||
|
|
||||||
public function scopeAperte($query)
|
public function scopeAperte($query)
|
||||||
|
|||||||
@@ -72,19 +72,19 @@ class Campagna extends Model
|
|||||||
/**
|
/**
|
||||||
* Campaign coverage percentage.
|
* Campaign coverage percentage.
|
||||||
* Numerator: assignments counted for campaign
|
* Numerator: assignments counted for campaign
|
||||||
* Denominator: ALL assignments with assigned_at in campaign range (returned or not)
|
* Denominator: total active territories
|
||||||
*/
|
*/
|
||||||
public function getPercentualePercorrenzaAttribute(): float
|
public function getPercentualePercorrenzaAttribute(): float
|
||||||
{
|
{
|
||||||
$totaleNelRange = $this->assegnazioniNelRange()->count();
|
$totaleAttivi = Territorio::where('attivo', true)->count();
|
||||||
|
|
||||||
if ($totaleNelRange === 0) {
|
if ($totaleAttivi === 0) {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$conteggiate = $this->assegnazioniConteggiate()->count();
|
$conteggiate = $this->assegnazioniConteggiate()->count();
|
||||||
|
|
||||||
return round(($conteggiate / $totaleNelRange) * 100, 1);
|
return round(($conteggiate / $totaleAttivi) * 100, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeCompletate($query)
|
public function scopeCompletate($query)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('assegnazioni', function (Blueprint $table) {
|
||||||
|
$table->boolean('link_sent')->default(false)->after('pdf_access_code');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('assegnazioni', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('link_sent');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
80
resources/views/assignments/link-scaduto.blade.php
Normal file
80
resources/views/assignments/link-scaduto.blade.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Link scaduto — TerManager2</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: #f1f5f9;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,.08);
|
||||||
|
padding: 40px 32px;
|
||||||
|
max-width: 420px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.territory {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="icon">
|
||||||
|
<svg width="26" height="26" fill="none" viewBox="0 0 24 24" stroke="#ef4444" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1>Link scaduto</h1>
|
||||||
|
<p>Il link per questo territorio non è più valido.<br>Contatta il responsabile dei territori per ricevere un nuovo link.</p>
|
||||||
|
@if($numero)
|
||||||
|
<span class="territory">Territorio N° {{ $numero }}</span>
|
||||||
|
@endif
|
||||||
|
<p class="footer">TerManager2</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -3,42 +3,178 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>PDF territorio {{ $assignment->territorio?->numero }}</title>
|
<title>Territorio {{ $assignment->territorio?->numero }}</title>
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
body {
|
||||||
background: #111827;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: #f1f5f9;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer {
|
header {
|
||||||
width: 100%;
|
background: #fff;
|
||||||
height: 100%;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
border: 0;
|
padding: 14px 20px;
|
||||||
display: block;
|
display: flex;
|
||||||
background: #111827;
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fallback {
|
.logo {
|
||||||
position: fixed;
|
font-size: 15px;
|
||||||
right: 12px;
|
font-weight: 700;
|
||||||
bottom: 12px;
|
color: #4f46e5;
|
||||||
z-index: 10;
|
letter-spacing: -.3px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 5px;
|
||||||
padding: 10px 14px;
|
background: #f1f5f9;
|
||||||
border-radius: 999px;
|
border: 1px solid #e2e8f0;
|
||||||
background: rgba(17, 24, 39, 0.9);
|
border-radius: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip .label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip .value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: #4f46e5;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font: 600 14px/1 system-ui, sans-serif;
|
font-size: 13px;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
font-weight: 600;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex: none;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
.open-btn:hover { background: #4338ca; }
|
||||||
|
|
||||||
|
.pdf-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-embed {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 60vh;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,.08);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-embed embed,
|
||||||
|
.pdf-embed iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 60vh;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-notice {
|
||||||
|
background: #fef9c3;
|
||||||
|
border: 1px solid #fde047;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #854d0e;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
header { padding: 10px 14px; }
|
||||||
|
.chip { font-size: 12px; padding: 3px 8px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<iframe class="viewer" src="{{ $pdfUrl }}#toolbar=0&navpanes=0&scrollbar=0" title="PDF territorio {{ $assignment->territorio?->numero }}"></iframe>
|
<header>
|
||||||
<a class="fallback" href="{{ $pdfUrl }}" target="_blank" rel="noopener noreferrer">Apri PDF</a>
|
<span class="logo">TerManager2</span>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<div class="chip">
|
||||||
|
<span class="label">Territorio</span>
|
||||||
|
<span class="value">N° {{ $assignment->territorio?->numero }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chip">
|
||||||
|
<span class="label">Assegnato a</span>
|
||||||
|
<span class="value">{{ $assignment->proclamatore?->nome_completo ?? '—' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chip">
|
||||||
|
<span class="label">Data</span>
|
||||||
|
<span class="value">{{ $assignment->assigned_at->format('d/m/Y') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ $pdfUrl }}" download class="open-btn">
|
||||||
|
<svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V4"/></svg>
|
||||||
|
Scarica PDF
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pdf-area">
|
||||||
|
<div class="pdf-embed" id="pdfContainer">
|
||||||
|
<embed src="{{ $pdfUrl }}" type="application/pdf" id="pdfEmbed">
|
||||||
|
</div>
|
||||||
|
<div class="fallback-notice" id="fallbackNotice">
|
||||||
|
Il tuo dispositivo non supporta la visualizzazione del PDF nel browser.
|
||||||
|
<a href="{{ $pdfUrl }}" download style="color:#4f46e5;font-weight:600;margin-left:6px;">Scarica il file PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Detect if embed loaded - fallback for mobile devices that don't support PDF embed
|
||||||
|
var embed = document.getElementById('pdfEmbed');
|
||||||
|
var notice = document.getElementById('fallbackNotice');
|
||||||
|
embed.onerror = function() {
|
||||||
|
notice.style.display = 'block';
|
||||||
|
};
|
||||||
|
// iOS Safari: embed always renders but PDF viewer doesn't work - show download link anyway
|
||||||
|
var ua = navigator.userAgent;
|
||||||
|
if (/iP(hone|ad|od)/i.test(ua) || (/Macintosh/i.test(ua) && 'ontouchend' in document)) {
|
||||||
|
document.getElementById('pdfContainer').innerHTML =
|
||||||
|
'<div style="display:flex;align-items:center;justify-content:center;height:100%;min-height:60vh;flex-direction:column;gap:16px;padding:24px;text-align:center;">' +
|
||||||
|
'<svg width="48" height="48" fill="none" viewBox="0 0 24 24" stroke="#94a3b8" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>' +
|
||||||
|
'<p style="font-size:14px;color:#64748b;max-width:240px">Safari su iOS non supporta la visualizzazione PDF nel browser.</p>' +
|
||||||
|
'<a href="{{ $pdfUrl }}" download style="background:#4f46e5;color:#fff;text-decoration:none;font-weight:600;font-size:14px;padding:10px 20px;border-radius:8px;">Scarica PDF</a>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,112 +8,160 @@
|
|||||||
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
|
<p class="text-sm text-gray-500 mt-1">Questa funzione consente un'assegnazione arbitraria, indipendente dalle tre schede della Home.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 max-w-3xl">
|
<div x-data="{ formOpen: true }">
|
||||||
<form wire:submit="save" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
@if(!$preselectedTerritorioId)
|
|
||||||
<label for="territorio_search" class="block text-sm font-medium text-gray-700">Filtra territori</label>
|
|
||||||
<input wire:model.live.debounce.300ms="territorioSearch"
|
|
||||||
type="text"
|
|
||||||
id="territorio_search"
|
|
||||||
placeholder="Cerca per numero, zona o tipologia"
|
|
||||||
class="mt-1 mb-2 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<label for="territorio_id" class="block text-sm font-medium text-gray-700">Territorio *</label>
|
{{-- Toggle form --}}
|
||||||
<select wire:model.live="territorio_id" id="territorio_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" @if($preselectedTerritorioId) disabled @endif>
|
<button type="button"
|
||||||
<option value="">Seleziona un territorio</option>
|
x-on:click="formOpen = !formOpen"
|
||||||
@foreach($territoriDisponibili as $t)
|
class="mb-4 inline-flex items-center gap-2 text-sm font-medium text-indigo-600 hover:text-indigo-800 transition">
|
||||||
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }} ({{ $t->tipologia?->nome }})</option>
|
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@endforeach
|
<path x-show="formOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
|
||||||
</select>
|
<path x-show="!formOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||||
@if($preselectedTerritorioId)
|
</svg>
|
||||||
<input type="hidden" wire:model="territorio_id" value="{{ $preselectedTerritorioId }}">
|
<span x-text="formOpen ? 'Nascondi form' : 'Nuova assegnazione'"></span>
|
||||||
@endif
|
</button>
|
||||||
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
|
||||||
|
|
||||||
@if($territorio_id)
|
{{-- Form --}}
|
||||||
<div class="mt-3">
|
<div x-show="formOpen" x-cloak class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6 mb-6 max-w-3xl">
|
||||||
<p class="text-xs text-gray-500 mb-2">Anteprima territorio</p>
|
<form wire:submit="save" class="space-y-4">
|
||||||
@if($this->selectedThumbnailUrl)
|
<div>
|
||||||
<div class="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm">
|
@if(!$preselectedTerritorioId)
|
||||||
<img src="{{ $this->selectedThumbnailUrl }}"
|
<label for="territorio_search" class="block text-sm font-medium text-gray-700">Filtra territori</label>
|
||||||
alt="Thumbnail territorio selezionato"
|
<input wire:model.live.debounce.300ms="territorioSearch"
|
||||||
class="block w-full h-auto max-h-[70vh] object-contain bg-white">
|
type="text"
|
||||||
</div>
|
id="territorio_search"
|
||||||
<p class="mt-2 text-xs text-gray-500">Miniatura del territorio ottimizzata per consultazione rapida anche da mobile.</p>
|
placeholder="Cerca per numero, zona o tipologia"
|
||||||
@else
|
class="mt-1 mb-2 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||||
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
@endif
|
||||||
Nessuna thumbnail disponibile per questo territorio.
|
|
||||||
|
<label for="territorio_id" class="block text-sm font-medium text-gray-700">Territorio *</label>
|
||||||
|
<select wire:model.live="territorio_id" id="territorio_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm" @if($preselectedTerritorioId) disabled @endif>
|
||||||
|
<option value="">Seleziona un territorio</option>
|
||||||
|
@foreach($territoriDisponibili as $t)
|
||||||
|
<option value="{{ $t->id }}">N° {{ $t->numero }} — {{ $t->zona?->nome }} ({{ $t->tipologia?->nome }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@if($preselectedTerritorioId)
|
||||||
|
<input type="hidden" wire:model="territorio_id" value="{{ $preselectedTerritorioId }}">
|
||||||
|
@endif
|
||||||
|
@error('territorio_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||||
|
|
||||||
|
@if($territorio_id)
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-xs text-gray-500 mb-2">Anteprima territorio</p>
|
||||||
|
@if($this->selectedThumbnailUrl)
|
||||||
|
<div class="overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm">
|
||||||
|
<img src="{{ $this->selectedThumbnailUrl }}"
|
||||||
|
alt="Thumbnail territorio selezionato"
|
||||||
|
class="block w-full h-auto max-h-[70vh] object-contain bg-white">
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-6 text-sm text-gray-500">
|
||||||
|
Nessuna thumbnail disponibile per questo territorio.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="proclamatore_id" class="block text-sm font-medium text-gray-700">Proclamatore *</label>
|
<label for="proclamatore_id" class="block text-sm font-medium text-gray-700">Proclamatore *</label>
|
||||||
<select wire:model="proclamatore_id" id="proclamatore_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
<select wire:model="proclamatore_id" id="proclamatore_id" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||||
<option value="">Seleziona un proclamatore</option>
|
<option value="">Seleziona un proclamatore</option>
|
||||||
@foreach($proclamatoriAttivi as $p)
|
@foreach($proclamatoriAttivi as $p)
|
||||||
<option value="{{ $p->id }}">{{ $p->cognome }} {{ $p->nome }}</option>
|
<option value="{{ $p->id }}">{{ $p->cognome }} {{ $p->nome }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
@error('proclamatore_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
@error('proclamatore_id') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="assigned_at" class="block text-sm font-medium text-gray-700">Data Assegnazione *</label>
|
<label for="assigned_at" class="block text-sm font-medium text-gray-700">Data Assegnazione *</label>
|
||||||
<input wire:model="assigned_at" type="date" id="assigned_at" max="{{ now()->format('Y-m-d') }}" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
<input wire:model="assigned_at" type="date" id="assigned_at" max="{{ now()->format('Y-m-d') }}" class="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||||
@error('assigned_at') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
@error('assigned_at') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse gap-3 pt-4 sm:flex-row sm:items-center">
|
||||||
|
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition">Assegna</button>
|
||||||
|
<button type="button" x-on:click="formOpen = false" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition">Annulla</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col-reverse gap-3 pt-4 sm:flex-row sm:items-center">
|
|
||||||
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition">Assegna</button>
|
|
||||||
<a href="{{ route('territori.index') }}" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition">Annulla</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Elenco territori attualmente assegnati con link --}}
|
{{-- Elenco territori attualmente assegnati --}}
|
||||||
@if($assegnazioniAperte->count())
|
@if($assegnazioniAperte->count())
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mt-10">
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mt-2">
|
||||||
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-color:#c7d2fe">
|
<div class="px-5 py-4 border-b flex items-center gap-3" style="background:linear-gradient(135deg,#eef2ff,#e0e7ff);border-color:#c7d2fe">
|
||||||
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#6366f1">
|
<div class="h-8 w-8 rounded-lg flex items-center justify-center" style="background:#6366f1">
|
||||||
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold" style="color:#3730a3">Territori Assegnati ({{ $assegnazioniAperte->count() }})</h3>
|
<h3 class="text-sm font-semibold" style="color:#3730a3">Territori Assegnati ({{ $assegnazioniAperte->count() }})</h3>
|
||||||
<p class="text-xs" style="color:#4f46e5">Per ogni territorio è visibile il link da condividere</p>
|
<p class="text-xs" style="color:#4f46e5">Link PDF · stato invio · link valido {{ $linkTtlMonths }} {{ $linkTtlMonths === 1 ? 'mese' : 'mesi' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divide-y divide-gray-100">
|
<div class="divide-y divide-gray-100">
|
||||||
@foreach($assegnazioniAperte as $a)
|
@foreach($assegnazioniAperte as $a)
|
||||||
@php($pdfUrl = $a->shortPdfUrl())
|
@php
|
||||||
|
$pdfUrl = $a->shortPdfUrl();
|
||||||
|
$linkScaduto = ! auth()->check() && $a->assigned_at->copy()->addMonths($linkTtlMonths)->isPast();
|
||||||
|
@endphp
|
||||||
<div class="px-5 py-4 hover:bg-indigo-50/30 transition-colors">
|
<div class="px-5 py-4 hover:bg-indigo-50/30 transition-colors">
|
||||||
<div class="flex items-center justify-between gap-4">
|
{{-- Testata riga --}}
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-x-6 gap-y-2">
|
||||||
<a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-semibold text-sm">N° {{ $a->territorio?->numero }}</a>
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs text-gray-400">{{ $a->territorio?->zona?->nome }}</span>
|
<a href="{{ route('territori.show', $a->territorio_id) }}" class="text-indigo-600 hover:underline font-bold text-sm">N° {{ $a->territorio?->numero }}</a>
|
||||||
|
@if($a->territorio?->zona?->nome)
|
||||||
|
<span class="text-xs text-gray-400 bg-gray-100 rounded px-1.5 py-0.5">{{ $a->territorio?->zona?->nome }}</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-5 text-sm">
|
<div class="flex items-center gap-3 text-sm flex-wrap">
|
||||||
<span class="text-gray-700">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</span>
|
<span class="font-medium text-gray-800">{{ $a->proclamatore?->nome_completo ?? 'N/A' }}</span>
|
||||||
<span class="text-gray-400">{{ $a->assigned_at->format('d/m/Y') }}</span>
|
<span class="text-gray-300">·</span>
|
||||||
<span class="text-xs font-medium {{ $a->giorni > 120 ? 'text-red-600' : ($a->giorni > 90 ? 'text-amber-600' : 'text-gray-500') }}">{{ $a->giorni }}g</span>
|
<span class="text-gray-500 text-xs">{{ $a->assigned_at->format('d/m/Y') }}</span>
|
||||||
|
<span class="text-gray-300">·</span>
|
||||||
|
<span class="text-xs font-semibold px-2 py-0.5 rounded-full
|
||||||
|
{{ $a->giorni > 120 ? 'bg-red-50 text-red-600' : ($a->giorni > 90 ? 'bg-amber-50 text-amber-600' : 'bg-gray-100 text-gray-500') }}">
|
||||||
|
{{ $a->giorni }}g
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if($pdfUrl)
|
|
||||||
<div x-data="{ copied: false }" class="mt-2 flex items-center gap-2">
|
{{-- Avviso link scaduto (solo per non loggati) --}}
|
||||||
|
@if($linkScaduto)
|
||||||
|
<div class="mt-3 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold bg-red-50 border border-red-200 text-red-700">
|
||||||
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
Link scaduto — rigenerare dal dettaglio territorio
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Link attivo --}}
|
||||||
|
@elseif($pdfUrl)
|
||||||
|
<div x-data="{ copied: false }" class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
<code class="flex-1 min-w-0 text-xs bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-gray-600 break-all select-all cursor-text" x-on:click="window.getSelection().selectAllChildren($el)">{{ $pdfUrl }}</code>
|
<code class="flex-1 min-w-0 text-xs bg-gray-50 border border-gray-200 rounded-md px-3 py-2 text-gray-600 break-all select-all cursor-text" x-on:click="window.getSelection().selectAllChildren($el)">{{ $pdfUrl }}</code>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
x-on:click="
|
x-on:click="navigator.clipboard.writeText($el.closest('div').querySelector('code').textContent).then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
|
||||||
const text = $el.closest('div').querySelector('code').textContent;
|
|
||||||
navigator.clipboard.writeText(text).then(() => { copied = true; setTimeout(() => copied = false, 2000) });
|
|
||||||
"
|
|
||||||
class="flex-none btn-action btn-action-indigo" title="Copia link" style="padding:6px 10px">
|
class="flex-none btn-action btn-action-indigo" title="Copia link" style="padding:6px 10px">
|
||||||
<svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
<svg x-show="!copied" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
||||||
<svg x-show="copied" x-cloak class="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
<svg x-show="copied" x-cloak class="h-4 w-4 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{{-- Flag Link Inviato --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="toggleLinkSent({{ $a->id }})"
|
||||||
|
title="{{ $a->link_sent ? 'Link inviato — clicca per annullare' : 'Segna come inviato' }}"
|
||||||
|
class="flex-none inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-semibold border transition
|
||||||
|
{{ $a->link_sent ? 'bg-green-50 border-green-200 text-green-700 hover:bg-green-100' : 'bg-gray-50 border-gray-200 text-gray-500 hover:bg-gray-100' }}">
|
||||||
|
@if($a->link_sent)
|
||||||
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
Link inviato
|
||||||
|
@else
|
||||||
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/></svg>
|
||||||
|
Non inviato
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
|
||||||
<span x-show="copied" x-cloak class="flex-none text-xs text-green-600 font-medium">Copiato!</span>
|
<span x-show="copied" x-cloak class="flex-none text-xs text-green-600 font-medium">Copiato!</span>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
|||||||
Reference in New Issue
Block a user