363 lines
21 KiB
PHP
363 lines
21 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>NetGescon Convocazione & Votazione</title>
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<!-- Google Fonts -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<!-- FontAwesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
<style>
|
|
body {
|
|
font-family: 'Outfit', sans-serif;
|
|
background: radial-gradient(circle at top, #1e293b 0%, #0f172a 100%);
|
|
}
|
|
.glass-card {
|
|
background: rgba(30, 41, 59, 0.7);
|
|
backdrop-filter: blur(16px);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen text-slate-100 flex flex-col justify-between">
|
|
|
|
<!-- Header -->
|
|
<header class="glass-card sticky top-0 z-50 border-b border-slate-800 px-4 py-3 flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-500/20">
|
|
<i class="fas fa-building text-white text-sm"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xs font-semibold tracking-wider text-blue-400 uppercase">NetGescon Mobile</h1>
|
|
<p class="text-sm font-bold truncate max-w-[200px]">{{ $assemblea->stabile->denominazione }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-xxs px-2 py-1 rounded-full bg-slate-800 text-slate-400 font-semibold border border-slate-700">
|
|
Unit: {{ $convocazione->unitaImmobiliare->interno ?: $convocazione->unitaImmobiliare->codice_unita }}
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-grow p-4 space-y-6">
|
|
|
|
<!-- Welcome Card -->
|
|
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-4">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h2 class="text-lg font-bold text-white">Gentile {{ $convocazione->soggetto->nome_completo ?? 'Condomino' }}</h2>
|
|
<p class="text-xs text-slate-400">Sei collegato alla gestione digitale dell'assemblea.</p>
|
|
</div>
|
|
<span class="px-2 py-1 text-[10px] font-bold uppercase rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
|
|
{{ $convocazione->ruolo === 'I' ? 'Inquilino' : 'Proprietario' }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Receipt Confirmation Box -->
|
|
<div id="receipt-box" class="p-3.5 rounded-xl bg-slate-900/60 border border-slate-800 flex items-center justify-between gap-3">
|
|
<div class="flex items-center space-x-3">
|
|
<div id="receipt-icon" class="w-9 h-9 rounded-lg flex items-center justify-center {{ $convocazione->ricezione_confermata_at ? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30' }}">
|
|
<i class="fas {{ $convocazione->ricezione_confermata_at ? 'fa-check-double' : 'fa-envelope-open' }}"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate-300">Ricezione convocazione</p>
|
|
<p id="receipt-status" class="text-[10px] text-slate-400">
|
|
{{ $convocazione->ricezione_confermata_at ? 'Firma apposta il ' . $convocazione->ricezione_confermata_at->format('d/m/Y H:i') : 'Firma elettronica richiesta' }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
@if(!$convocazione->ricezione_confermata_at)
|
|
<button onclick="confirmReceipt()" id="receipt-btn" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-semibold shadow-lg shadow-blue-500/10 active:scale-95 transition-all">
|
|
Firma
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assembly Info Panel -->
|
|
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-4">
|
|
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center"><i class="fas fa-calendar-day mr-2 text-blue-500"></i>Dettagli convocazione</h3>
|
|
|
|
<div class="grid grid-cols-2 gap-4 text-xs">
|
|
<div class="bg-slate-900/40 p-3 rounded-xl border border-slate-800/80">
|
|
<p class="text-slate-400 mb-1">1ª Convocazione</p>
|
|
<p class="font-bold text-white">{{ $assemblea->data_prima_convocazione ? $assemblea->data_prima_convocazione->format('d/m/Y H:i') : 'n/d' }}</p>
|
|
</div>
|
|
<div class="bg-slate-900/40 p-3 rounded-xl border border-slate-800/80">
|
|
<p class="text-slate-400 mb-1">2ª Convocazione</p>
|
|
<p class="font-bold text-white">{{ $assemblea->data_seconda_convocazione ? $assemblea->data_seconda_convocazione->format('d/m/Y H:i') : 'n/d' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-slate-900/40 p-3 rounded-xl border border-slate-800/80 text-xs">
|
|
<p class="text-slate-400 mb-1">Sede e Luogo</p>
|
|
<p class="font-semibold text-white flex items-center"><i class="fas fa-map-marker-alt mr-1.5 text-red-400"></i>{{ $assemblea->luogo ?: 'Presso i locali condominiali / Telematica' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Real-Time Voting Box -->
|
|
<div id="live-voting-panel" class="p-5 rounded-2xl bg-gradient-to-br from-indigo-900/50 to-purple-900/30 border border-indigo-500/20 shadow-2xl relative overflow-hidden hidden">
|
|
<!-- Pulsante decorativo pulsante -->
|
|
<div class="absolute -top-12 -right-12 w-28 h-28 bg-indigo-500/10 rounded-full blur-xl animate-pulse"></div>
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="flex h-2.5 w-2.5 relative">
|
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
|
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-red-500"></span>
|
|
</span>
|
|
<h3 class="text-sm font-bold tracking-wider text-indigo-300 uppercase">Votazione in corso</h3>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full bg-indigo-500/20 text-indigo-300 text-[10px] font-semibold border border-indigo-400/20">LIVE</span>
|
|
</div>
|
|
|
|
<div id="live-odg-details" class="space-y-4">
|
|
<div>
|
|
<h4 id="live-odg-title" class="text-md font-bold text-white leading-snug"></h4>
|
|
<p id="live-odg-desc" class="text-xs text-slate-300 mt-1.5 leading-relaxed"></p>
|
|
</div>
|
|
|
|
<!-- Bottoni di voto -->
|
|
<div class="space-y-3 pt-2">
|
|
<!-- Votazione propria -->
|
|
<div id="own-vote-section" class="space-y-2">
|
|
<p class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Il tuo voto (Int. {{ $convocazione->unitaImmobiliare->interno }})</p>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<button onclick="submitVote('favorevole')" class="py-2.5 rounded-xl border border-emerald-500/20 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-xs font-bold active:scale-95 transition-all flex flex-col items-center justify-center gap-1">
|
|
<i class="fas fa-thumbs-up"></i> Favorevole
|
|
</button>
|
|
<button onclick="submitVote('contrario')" class="py-2.5 rounded-xl border border-rose-500/20 bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-bold active:scale-95 transition-all flex flex-col items-center justify-center gap-1">
|
|
<i class="fas fa-thumbs-down"></i> Contrario
|
|
</button>
|
|
<button onclick="submitVote('astenuto')" class="py-2.5 rounded-xl border border-slate-500/20 bg-slate-500/10 hover:bg-slate-500/20 text-slate-400 text-xs font-bold active:scale-95 transition-all flex flex-col items-center justify-center gap-1">
|
|
<i class="fas fa-ban"></i> Astenuto
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sezione deleghe -->
|
|
@if($deleghe->isNotEmpty())
|
|
<div id="deleghe-vote-section" class="mt-4 pt-3 border-t border-slate-800 space-y-3">
|
|
<p class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Voti delegati in delega</p>
|
|
@foreach($deleghe as $delega)
|
|
<div class="p-2.5 rounded-xl bg-slate-900/60 border border-slate-800 space-y-2">
|
|
<div class="flex justify-between items-center text-[10px] text-slate-300">
|
|
<span>Int. {{ $delega->unitaImmobiliare->interno }} - {{ $delega->soggetto->nome_completo }}</span>
|
|
<span class="text-indigo-400 font-semibold">Delega</span>
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<button onclick="submitVote('favorevole', {{ $delega->unita_immobiliare_id }}, {{ $delega->soggetto_id }}, this)" class="py-1.5 rounded-lg border border-emerald-500/20 bg-emerald-500/10 text-emerald-400 text-[11px] font-semibold active:scale-95 transition-all">
|
|
Favorevole
|
|
</button>
|
|
<button onclick="submitVote('contrario', {{ $delega->unita_immobiliare_id }}, {{ $delega->soggetto_id }}, this)" class="py-1.5 rounded-lg border border-rose-500/20 bg-rose-500/10 text-rose-400 text-[11px] font-semibold active:scale-95 transition-all">
|
|
Contrario
|
|
</button>
|
|
<button onclick="submitVote('astenuto', {{ $delega->unita_immobiliare_id }}, {{ $delega->soggetto_id }}, this)" class="py-1.5 rounded-lg border border-slate-500/20 bg-slate-500/10 text-slate-400 text-[11px] font-semibold active:scale-95 transition-all">
|
|
Astenuto
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ordine del giorno & Allegati -->
|
|
<div class="glass-card rounded-2xl p-5 shadow-xl space-y-4">
|
|
<h3 class="text-sm font-bold text-slate-300 uppercase tracking-wider flex items-center"><i class="fas fa-list-ol mr-2 text-blue-500"></i>Ordine del Giorno (OdG)</h3>
|
|
|
|
<div class="space-y-3">
|
|
@foreach($odg as $punto)
|
|
<div class="p-3.5 rounded-xl bg-slate-900/40 border border-slate-800 space-y-2">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="flex items-start space-x-2">
|
|
<span class="w-5 h-5 rounded-full bg-slate-800 flex items-center justify-center text-xxs font-bold text-slate-300 mt-0.5">
|
|
{{ $punto->numero_punto }}
|
|
</span>
|
|
<div>
|
|
<h4 class="text-xs font-bold text-white leading-snug">{{ $punto->titolo }}</h4>
|
|
<p class="text-xxs text-slate-400 mt-1 leading-relaxed">{{ $punto->descrizione }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-800/80 text-[10px]">
|
|
@if($punto->articolo_legge)
|
|
<span class="text-blue-400"><i class="fas fa-gavel mr-1"></i>Rif: {{ $punto->articolo_legge }}</span>
|
|
@endif
|
|
@if($punto->tabellaMillesimale)
|
|
<span class="text-slate-400"><i class="fas fa-calculator mr-1"></i>Tab: {{ $punto->tabellaMillesimale->denominazione }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<!-- Toast Notification -->
|
|
<div id="toast" class="fixed bottom-4 left-4 right-4 p-4 rounded-xl bg-slate-900 border border-slate-800 shadow-2xl flex items-center justify-between text-xs transition-all transform translate-y-24 opacity-0 z-50">
|
|
<span id="toast-message" class="text-slate-300"></span>
|
|
<button onclick="hideToast()" class="text-slate-400 hover:text-white"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="text-center py-4 text-xxs text-slate-500 border-t border-slate-800 glass-card">
|
|
NetGescon © 2026 - Piattaforma Gestione Assemblee Condominiali
|
|
</footer>
|
|
|
|
<!-- Polling & Event Javascript -->
|
|
<script>
|
|
const token = "{{ $convocazione->token_accesso }}";
|
|
let activeOdgId = null;
|
|
|
|
function showToast(message, isSuccess = true) {
|
|
const toast = document.getElementById('toast');
|
|
const msgEl = document.getElementById('toast-message');
|
|
msgEl.textContent = message;
|
|
if (isSuccess) {
|
|
toast.classList.remove('border-red-500/20');
|
|
toast.classList.add('border-emerald-500/20');
|
|
} else {
|
|
toast.classList.remove('border-emerald-500/20');
|
|
toast.classList.add('border-red-500/20');
|
|
}
|
|
toast.classList.remove('translate-y-24', 'opacity-0');
|
|
setTimeout(hideToast, 4000);
|
|
}
|
|
|
|
function hideToast() {
|
|
document.getElementById('toast').classList.add('translate-y-24', 'opacity-0');
|
|
}
|
|
|
|
function confirmReceipt() {
|
|
const btn = document.getElementById('receipt-btn');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner animate-spin"></i>';
|
|
|
|
fetch(`/public/assemblea/${token}/ricezione`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
}
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast('Firma elettronica inserita con successo!');
|
|
btn.remove();
|
|
document.getElementById('receipt-status').textContent = 'Firma apposta il ' + data.ricezione_confermata_at;
|
|
document.getElementById('receipt-icon').className = 'w-9 h-9 rounded-lg flex items-center justify-center bg-emerald-500/20 text-emerald-400 border border-emerald-500/30';
|
|
document.getElementById('receipt-icon').innerHTML = '<i class="fas fa-check-double"></i>';
|
|
} else {
|
|
showToast(data.message || 'Errore durante la firma.', false);
|
|
btn.disabled = false;
|
|
btn.textContent = 'Firma';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('Errore di rete. Riprova.', false);
|
|
btn.disabled = false;
|
|
btn.textContent = 'Firma';
|
|
});
|
|
}
|
|
|
|
function submitVote(voteStr, unitaId = null, soggettoId = null, btnEl = null) {
|
|
if (btnEl) {
|
|
// Disable sister buttons for the same delegation
|
|
const container = btnEl.closest('div');
|
|
container.querySelectorAll('button').forEach(b => {
|
|
b.classList.remove('bg-emerald-500/30', 'bg-rose-500/30', 'bg-slate-500/30', 'border-emerald-500/40', 'border-rose-500/40', 'border-slate-500/40');
|
|
});
|
|
}
|
|
|
|
fetch(`/public/assemblea/${token}/vota`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
},
|
|
body: JSON.stringify({
|
|
ordine_giorno_id: activeOdgId,
|
|
voto: voteStr,
|
|
unita_immobiliare_id: unitaId,
|
|
soggetto_id: soggettoId
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showToast('Voto inviato con successo!');
|
|
if (btnEl) {
|
|
// Highlight selected button
|
|
let colorClass = voteStr === 'favorevole' ? 'bg-emerald-500/30 border-emerald-500/40' : (voteStr === 'contrario' ? 'bg-rose-500/30 border-rose-500/40' : 'bg-slate-500/30 border-slate-500/40');
|
|
btnEl.className.split(' ').forEach(c => {
|
|
if (c.startsWith('bg-') || c.startsWith('border-')) {
|
|
btnEl.classList.remove(c);
|
|
}
|
|
});
|
|
colorClass.split(' ').forEach(c => btnEl.classList.add(c));
|
|
} else {
|
|
// Highlight own vote section
|
|
const container = document.getElementById('own-vote-section');
|
|
container.querySelectorAll('button').forEach(b => {
|
|
b.classList.remove('bg-emerald-500/30', 'bg-rose-500/30', 'bg-slate-500/30');
|
|
if (b.onclick.toString().includes(voteStr)) {
|
|
let bg = voteStr === 'favorevole' ? 'bg-emerald-500/30' : (voteStr === 'contrario' ? 'bg-rose-500/30' : 'bg-slate-500/30');
|
|
b.classList.add(bg);
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
showToast(data.message || 'Errore nell\'invio del voto.', false);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('Errore di rete. Riprova.', false);
|
|
});
|
|
}
|
|
|
|
function pollActiveVoting() {
|
|
fetch(`/public/assemblea/${token}/poll`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const livePanel = document.getElementById('live-voting-panel');
|
|
if (data.attivo_odg_id) {
|
|
if (activeOdgId !== data.attivo_odg_id) {
|
|
activeOdgId = data.attivo_odg_id;
|
|
document.getElementById('live-odg-title').textContent = `${data.punto.numero}. ${data.punto.titolo}`;
|
|
document.getElementById('live-odg-desc').textContent = data.punto.descrizione;
|
|
|
|
// Reset selections in buttons
|
|
document.querySelectorAll('#live-voting-panel button').forEach(b => {
|
|
b.classList.remove('bg-emerald-500/30', 'bg-rose-500/30', 'bg-slate-500/30', 'border-emerald-500/40', 'border-rose-500/40', 'border-slate-500/40');
|
|
});
|
|
|
|
livePanel.classList.remove('hidden');
|
|
showToast('Nuova votazione aperta!');
|
|
}
|
|
} else {
|
|
activeOdgId = null;
|
|
livePanel.classList.add('hidden');
|
|
}
|
|
})
|
|
.catch(err => console.error('Polling error:', err));
|
|
}
|
|
|
|
// Avvia polling ogni 3 secondi
|
|
setInterval(pollActiveVoting, 3000);
|
|
pollActiveVoting();
|
|
</script>
|
|
</body>
|
|
</html>
|