504 lines
21 KiB
PHP
504 lines
21 KiB
PHP
<section id="features" class="features section">
|
||
<div class="container section-title" data-aos="fade-up">
|
||
<h2>Informasi</h2>
|
||
<p>halooo Warga Desa Pelem Ayoo, inilo fiturnya ayooo</p>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="d-flex justify-content-center">
|
||
<ul class="nav nav-tabs" data-aos="fade-up" data-aos-delay="100">
|
||
<li class="nav-item">
|
||
<a class="nav-link active show" data-bs-toggle="tab" data-bs-target="#features-tab-1">
|
||
<h4>Kehadiran Pegawai</h4>
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#features-tab-2">
|
||
<h4>Struktur Desa</h4>
|
||
</a>
|
||
</li>
|
||
{{-- <li class="nav-item">
|
||
<a class="nav-link" data-bs-toggle="tab" data-bs-target="#features-tab-3">
|
||
<h4>Pengumuman & berita</h4>
|
||
</a>
|
||
</li> --}}
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="tab-content" data-aos="fade-up" data-aos-delay="200">
|
||
<div class="tab-pane fade active show" id="features-tab-1">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="card shadow-sm border-0">
|
||
<div class="card-header bg-white">
|
||
<h4 class="mb-0">Kehadiran Pegawai ·
|
||
{{ \Carbon\Carbon::today()->translatedFormat('d F Y') }}</h4>
|
||
<small class="text-muted">Realtime update – tanpa refresh</small>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="table-responsive">
|
||
<table class="table align-middle">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Nama</th>
|
||
<th>Jabatan</th>
|
||
<th>Phone</th>
|
||
<th>Tanggal</th>
|
||
<th>Check-In</th>
|
||
<th>Check-Out</th>
|
||
<th>Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="members-body">
|
||
@foreach ($members as $m)
|
||
@php
|
||
// Normalisasi status
|
||
$statusLower = strtolower($m['status'] ?? '');
|
||
|
||
// Mapping: status => [bg-class, text-class, label]
|
||
$badgeMap = [
|
||
'hadir' => ['bg-success-subtle', 'text-success', 'Hadir'],
|
||
'izin' => ['bg-warning-subtle', 'text-warning', 'Izin'],
|
||
'sakit' => ['bg-info-subtle', 'text-info', 'Sakit'],
|
||
'alpha' => ['bg-danger-subtle', 'text-danger', 'Alpha'],
|
||
];
|
||
|
||
// Ambil entry, fallback ke 'alpha'
|
||
[$bgClass, $textClass, $label] =
|
||
$badgeMap[$statusLower] ?? $badgeMap['alpha'];
|
||
@endphp
|
||
<tr data-user-id="{{ $m['id'] }}">
|
||
<td data-col="name">{{ $m['name'] }}</td>
|
||
<td data-col="role">{{ $m['role'] }}</td>
|
||
<td data-col="phone">{{ $m['phone'] }}</td>
|
||
<td data-col="date">{{ $m['date'] }}</td>
|
||
<td data-col="checkin">{{ $m['checkin'] }}</td>
|
||
<td data-col="checkout">{{ $m['checkout'] ?? '-' }}</td>
|
||
<td data-col="status">
|
||
<span
|
||
class="badge {{ $bgClass }} {{ $textClass }} fw-semibold px-3 py-2">
|
||
{{ $label }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div id="attn-hint" class="small text-muted"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Menampilkan Bagan Struktur Organisasi --}}
|
||
<div class="tab-content" data-aos="fade-up" data-aos-delay="200">
|
||
<div class="tab-pane fade" id="features-tab-2">
|
||
<div class="row justify-content-center">
|
||
<div class="col-lg-10 text-center">
|
||
|
||
<img src="{{ asset('assets/images/bagandesa.jpg') }}" alt="Struktur Organisasi Desa"
|
||
class="img-fluid rounded shadow-lg mb-3"
|
||
style="max-height: 500px; object-fit: contain; border: 4px solid #f8f9fa;">
|
||
<p class="fst-italic text-muted mt-3">Bagan struktur organisasi Desa Pelem</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Menampilkan Pengumuman dan Berita --}}
|
||
{{-- <div class="tab-pane fade" id="features-tab-3">
|
||
<div class="row">
|
||
<div class="col-12">
|
||
<div class="d-flex flex-wrap justify-content-center gap-2 mb-4" id="category-buttons">
|
||
<button class="btn btn-primary btn-sm px-3 py-2 category-filter" data-category="all">
|
||
Semua Kategori
|
||
</button>
|
||
</div>
|
||
|
||
<div class="row" id="news-grid">
|
||
<div class="col-12 text-center py-5">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
<p class="mt-2 text-muted">Memuat berita...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-center mt-4" id="load-more-container" style="display: none;">
|
||
<button class="btn btn-outline-primary" id="load-more-btn">
|
||
Muat Berita Lainnya
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div> --}}
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<style>
|
||
.news-card {
|
||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.news-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1) !important;
|
||
}
|
||
|
||
.category-filter {
|
||
transition: all 0.2s ease-in-out;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.category-filter:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* === PERUBAHAN DIMULAI DI SINI === */
|
||
|
||
/* Mengubah warna solid button menjadi hijau */
|
||
.features .btn-primary {
|
||
background: #077a7d;
|
||
border-color: #077a7d;
|
||
}
|
||
|
||
/* Mengubah warna outline button (border dan teks) menjadi hijau */
|
||
.features .btn-outline-primary {
|
||
color: #077a7d;
|
||
border-color: #077a7d;
|
||
}
|
||
|
||
/* Mengubah warna background saat hover pada outline button */
|
||
.features .btn-outline-primary:hover {
|
||
background-color: #077a7d;
|
||
color: #fff;
|
||
/* Agar teks menjadi putih saat di-hover */
|
||
}
|
||
|
||
/* Mengubah warna spinner loading menjadi hijau */
|
||
.features .text-primary {
|
||
color: #077a7d !important;
|
||
/* !important diperlukan untuk menimpa utility class bootstrap */
|
||
}
|
||
|
||
/* === PERUBAHAN SELESAI === */
|
||
|
||
|
||
#news-grid {
|
||
min-height: 200px;
|
||
}
|
||
|
||
.spinner-border {
|
||
width: 3rem;
|
||
height: 3rem;
|
||
}
|
||
|
||
.card-img-top {
|
||
transition: transform 0.3s ease-in-out;
|
||
}
|
||
|
||
.news-card:hover .card-img-top {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
/* .badge {
|
||
font-size: 0.75rem;
|
||
padding: 0.5rem 0.75rem;
|
||
border-radius: 12px;
|
||
} */
|
||
</style>
|
||
<script src="https://js.pusher.com/8.4.0/pusher.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/laravel-echo@1.16.1/dist/echo.iife.js"></script>
|
||
<script>
|
||
// Realtime Absensi
|
||
(function() {
|
||
// helper badge
|
||
function statusBadge(status) {
|
||
const s = (status || '').toLowerCase().trim();
|
||
|
||
// 1) Jika kosong, tampilkan neutral "-"
|
||
if (!s) {
|
||
return '<span class="badge bg-secondary-subtle text-secondary fw-semibold px-3 py-2">-</span>';
|
||
}
|
||
|
||
// 2) Kondisi khusus
|
||
switch (s) {
|
||
case 'hadir':
|
||
return '<span class="badge bg-success-subtle text-success fw-semibold px-3 py-2">Hadir</span>';
|
||
|
||
// Izin disetujui maupun "izin" biasa
|
||
case 'izin':
|
||
case 'izin_disetujui':
|
||
return '<span class="badge bg-warning-subtle text-warning fw-semibold px-3 py-2">Izin</span>';
|
||
|
||
case 'sakit':
|
||
return '<span class="badge bg-info-subtle text-info fw-semibold px-3 py-2">Sakit</span>';
|
||
|
||
case 'alpha':
|
||
return '<span class="badge bg-danger-subtle text-danger fw-semibold px-3 py-2">Alpha</span>';
|
||
|
||
// 3) Semua selain di atas → neutral
|
||
default:
|
||
return '<span class="badge bg-secondary-subtle text-secondary fw-semibold px-3 py-2">-</span>';
|
||
}
|
||
}
|
||
|
||
// format time helper
|
||
function formatTime(val) {
|
||
if (!val) return '-';
|
||
const d = new Date(val);
|
||
if (isNaN(d.getTime())) return val; // fallback jika bukan ISO format
|
||
return d.toLocaleTimeString('id-ID', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
}
|
||
|
||
// patch 1 baris di tabel
|
||
function patchAttendanceRow(update) {
|
||
const {
|
||
user_id,
|
||
status,
|
||
check_in,
|
||
check_out
|
||
} = update;
|
||
const tr = document.querySelector(`tr[data-user-id="${user_id}"]`);
|
||
if (!tr) return;
|
||
|
||
const tdCheckin = tr.querySelector('[data-col="checkin"]');
|
||
const tdCheckout = tr.querySelector('[data-col="checkout"]');
|
||
const tdStatus = tr.querySelector('[data-col="status"]');
|
||
|
||
if (tdCheckin) tdCheckin.textContent = formatTime(check_in);
|
||
if (tdCheckout) tdCheckout.textContent = formatTime(check_out);
|
||
if (tdStatus) tdStatus.innerHTML = statusBadge(status);
|
||
}
|
||
|
||
// refresh penuh tabel
|
||
async function refreshMembers() {
|
||
try {
|
||
const res = await fetch('/api/attendance/today', {
|
||
cache: 'no-store'
|
||
});
|
||
if (!res.ok) return;
|
||
const rows = await res.json();
|
||
const today = new Date().toLocaleDateString('id-ID', {
|
||
day: '2-digit',
|
||
month: 'long',
|
||
year: 'numeric'
|
||
});
|
||
|
||
const html = rows.map(r => `
|
||
<tr data-user-id="${r.id}">
|
||
<td data-col="name">${r.name ?? '-'}</td>
|
||
<td data-col="role">${r.role ?? '-'}</td>
|
||
<td data-col="phone">${r.phone ?? '-'}</td>
|
||
<td data-col="date">${today}</td>
|
||
<td data-col="checkin">${formatTime(r.check_in)}</td>
|
||
<td data-col="checkout">${formatTime(r.check_out)}</td>
|
||
<td data-col="status">${statusBadge(r.status)}</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
document.getElementById('members-body').innerHTML = html;
|
||
console.log('[TABLE] Updated');
|
||
} catch (e) {
|
||
console.warn('[TABLE] Refresh failed', e);
|
||
}
|
||
}
|
||
|
||
// init Echo Reverb
|
||
window.Pusher = window.Pusher || Pusher;
|
||
|
||
const echo = new window.Echo({
|
||
broadcaster: 'pusher',
|
||
key: "{{ env('PUSHER_APP_KEY') }}",
|
||
cluster: "{{ env('PUSHER_APP_CLUSTER', 'ap1') }}",
|
||
forceTLS: "{{ env('PUSHER_SCHEME', 'https') }}" === 'https',
|
||
});
|
||
|
||
echo.channel('attendance.global')
|
||
.subscribed(() => {
|
||
console.log('[Echo] Subscribed: attendance.global');
|
||
document.getElementById('attn-hint').textContent = 'Realtime terhubung';
|
||
})
|
||
.error(err => console.error('[Echo] Channel error:', err))
|
||
.listen('.attendance.updated', e => {
|
||
console.log('[Attendance] Update:', e);
|
||
patchAttendanceRow(e);
|
||
});
|
||
|
||
setInterval(refreshMembers, 60_000); // sync tiap menit
|
||
refreshMembers(); // initial load
|
||
})();
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
let currentPage = 1;
|
||
let currentCategory = 'all';
|
||
let loading = false;
|
||
|
||
// Load categories and initial news
|
||
loadCategories();
|
||
loadNews();
|
||
|
||
// Load categories dynamically
|
||
function loadCategories() {
|
||
console.log('Loading categories...');
|
||
fetch('/api/news/categories')
|
||
.then(response => {
|
||
console.log('Categories response status:', response.status);
|
||
return response.json();
|
||
})
|
||
.then(categories => {
|
||
console.log('Categories loaded:', categories);
|
||
const categoryButtons = document.getElementById('category-buttons');
|
||
const allButton = categoryButtons.querySelector('[data-category="all"]');
|
||
|
||
// Clear existing buttons except "Semua Kategori"
|
||
categoryButtons.innerHTML = '';
|
||
categoryButtons.appendChild(allButton);
|
||
|
||
// Add dynamic category buttons
|
||
categories.forEach(category => {
|
||
const button = document.createElement('button');
|
||
button.className =
|
||
'btn btn-outline-primary btn-sm px-3 py-2 category-filter';
|
||
button.setAttribute('data-category', category);
|
||
button.textContent = category;
|
||
button.addEventListener('click', handleCategoryClick);
|
||
categoryButtons.appendChild(button);
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading categories:', error);
|
||
// Fallback to default categories if API fails
|
||
const defaultCategories = ['Infrastruktur', 'Ekonomi', 'Kesehatan', 'Budaya',
|
||
'Pemerintahan', 'Pendidikan', 'Lingkungan', 'Teknologi'
|
||
];
|
||
const categoryButtons = document.getElementById('category-buttons');
|
||
const allButton = categoryButtons.querySelector('[data-category="all"]');
|
||
|
||
categoryButtons.innerHTML = '';
|
||
categoryButtons.appendChild(allButton);
|
||
|
||
defaultCategories.forEach(category => {
|
||
const button = document.createElement('button');
|
||
button.className =
|
||
'btn btn-outline-primary btn-sm px-3 py-2 category-filter';
|
||
button.setAttribute('data-category', category);
|
||
button.textContent = category;
|
||
button.addEventListener('click', handleCategoryClick);
|
||
categoryButtons.appendChild(button);
|
||
});
|
||
|
||
// Also try to load news with fallback
|
||
//loadNewsWithFallback();
|
||
});
|
||
}
|
||
|
||
// Category filter click handler
|
||
function handleCategoryClick() {
|
||
const category = this.dataset.category;
|
||
|
||
// Update button states
|
||
document.querySelectorAll('.category-filter').forEach(btn => {
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-outline-primary');
|
||
});
|
||
this.classList.remove('btn-outline-primary');
|
||
this.classList.add('btn-primary');
|
||
|
||
// Reset and load news
|
||
currentCategory = category;
|
||
currentPage = 1;
|
||
document.getElementById('news-grid').innerHTML = '';
|
||
loadNews();
|
||
}
|
||
|
||
// Add event listener to "Semua Kategori" button
|
||
document.querySelector('[data-category="all"]').addEventListener('click', handleCategoryClick);
|
||
|
||
// Load more button handler
|
||
document.getElementById('load-more-btn').addEventListener('click', function() {
|
||
if (!loading) {
|
||
currentPage++;
|
||
loadNews(true);
|
||
}
|
||
});
|
||
|
||
function loadNews(append = false) {
|
||
if (loading) return;
|
||
loading = true;
|
||
|
||
// Show loading indicator
|
||
if (!append) {
|
||
document.getElementById('news-grid').innerHTML = `
|
||
<div class="col-12 text-center py-5">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
<p class="mt-2 text-muted">Memuat berita...</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const url = new URL('/news', window.location.origin);
|
||
url.searchParams.set('category', currentCategory);
|
||
url.searchParams.set('page', currentPage);
|
||
url.searchParams.set('ajax', '1');
|
||
|
||
console.log('Fetching URL:', url.toString());
|
||
fetch(url, {
|
||
headers: {
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
}
|
||
})
|
||
.then(response => {
|
||
console.log('Response status:', response.status);
|
||
console.log('Response headers:', response.headers);
|
||
if (!response.ok) {
|
||
throw new Error('Network response was not ok');
|
||
}
|
||
return response.text().then(text => {
|
||
console.log('Response text:', text);
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (e) {
|
||
console.error('JSON parse error:', e);
|
||
throw new Error('Invalid JSON response');
|
||
}
|
||
});
|
||
})
|
||
.then(data => {
|
||
if (append) {
|
||
document.getElementById('news-grid').innerHTML += data.html;
|
||
} else {
|
||
document.getElementById('news-grid').innerHTML = data.html;
|
||
}
|
||
|
||
// Show/hide load more button
|
||
if (data.hasMore) {
|
||
document.getElementById('load-more-container').style.display = 'block';
|
||
} else {
|
||
document.getElementById('load-more-container').style.display = 'none';
|
||
}
|
||
|
||
loading = false;
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading news:', error);
|
||
});
|
||
}
|
||
|
||
|
||
});
|
||
</script>
|