310 lines
12 KiB
PHP
310 lines
12 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', 'Laporan')
|
|
|
|
@push('styles')
|
|
<link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
|
<link href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.bootstrap5.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.soft-card {
|
|
border: 0;
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 30px rgba(16, 24, 40, .06);
|
|
}
|
|
|
|
.table-hlines {
|
|
--line: #e9edf4;
|
|
}
|
|
|
|
.table-hlines> :not(caption)>*>* {
|
|
border-top: 0;
|
|
border-right: 0 !important;
|
|
border-left: 0 !important;
|
|
border-bottom: 1px solid var(--line);
|
|
background: transparent;
|
|
}
|
|
|
|
.table-hlines thead th {
|
|
border-bottom: 2px solid var(--line) !important;
|
|
}
|
|
|
|
table.dataTable.hover>tbody>tr:hover>*,
|
|
table.dataTable.display>tbody>tr:hover>* {
|
|
background: transparent !important;
|
|
}
|
|
|
|
.table-sticky thead th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: #fff;
|
|
z-index: 1
|
|
}
|
|
|
|
div.dataTables_filter,
|
|
div.dataTables_length {
|
|
display: none
|
|
}
|
|
|
|
.avatar-sm {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.avatar-sm img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.avatar-fallback {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 999px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ===== Filter Bar (sesuai referensi) ===== */
|
|
.filter-bar {
|
|
border: 1px solid #e9edf4;
|
|
border-radius: 12px;
|
|
padding: 10px 12px;
|
|
background: #fff;
|
|
}
|
|
|
|
.filter-label {
|
|
font-weight: 600;
|
|
font-size: .875rem;
|
|
color: #0f766e;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.filter-bar .form-control,
|
|
.filter-bar .form-select,
|
|
.filter-bar .input-group-text,
|
|
.filter-bar .btn {
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.btn-download {
|
|
background: #f97316;
|
|
color: #fff;
|
|
border: 0;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
background: #f97316;
|
|
color: #fff;
|
|
opacity: .9;
|
|
}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
@php
|
|
use Illuminate\Support\Str;
|
|
|
|
$getInitials = function ($name) {
|
|
$parts = collect(preg_split('/\s+/', trim((string) $name)))->filter();
|
|
return strtoupper($parts->take(2)->map(fn($w) => Str::substr($w, 0, 1))->join(''));
|
|
};
|
|
|
|
$photoUrl = function ($path) {
|
|
if (!$path) {
|
|
return null;
|
|
}
|
|
return Str::startsWith($path, ['http://', 'https://']) ? $path : asset('storage/' . ltrim($path, '/'));
|
|
};
|
|
|
|
// dropdown periode (misal 24 bulan kebelakang)
|
|
$selectedPeriod = sprintf('%04d-%02d', (int) $year, (int) $month);
|
|
@endphp
|
|
|
|
<div class="card soft-card">
|
|
<div class="card-body">
|
|
{{-- Header --}}
|
|
<div class="mb-3">
|
|
<h4 class="mb-0">Laporan Absensi Desa</h4>
|
|
<small style="color:#0f766e">Rekap • {{ $monthLabel }}</small>
|
|
</div>
|
|
|
|
{{-- Filter bar seperti referensi --}}
|
|
<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
|
|
{{-- Nama search --}}
|
|
<div class="d-flex align-items-center gap-2 grow" style="min-width: 280px;">
|
|
<span class="filter-label">Nama:</span>
|
|
<div class="input-group input-group-sm" style="max-width: 360px;">
|
|
<input id="nameSearch" type="text" class="form-control" placeholder="Cari nama anggota ...">
|
|
<button class="btn btn-outline-secondary" type="button" id="btnNameSearch">
|
|
<i class="ti ti-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Periode select (1 dropdown) --}}
|
|
<form id="periodForm" method="GET" action="{{ route('admin.laporan') }}"
|
|
class="d-flex align-items-center gap-2" style="min-width: 260px;">
|
|
<span class="filter-label">Periode:</span>
|
|
|
|
<select id="periodSelect" class="form-select form-select-sm" style="min-width: 170px;">
|
|
@for ($i = 0; $i < 24; $i++)
|
|
@php
|
|
$d = now()->subMonths($i);
|
|
$val = $d->format('Y-m');
|
|
$label = $d->translatedFormat('F Y'); // Januari 2026
|
|
@endphp
|
|
<option value="{{ $val }}" {{ $val === $selectedPeriod ? 'selected' : '' }}>
|
|
{{ $label }}
|
|
</option>
|
|
@endfor
|
|
</select>
|
|
|
|
{{-- hidden param yang dipakai controller --}}
|
|
<input type="hidden" name="month" id="monthInput" value="{{ (int) $month }}">
|
|
<input type="hidden" name="year" id="yearInput" value="{{ (int) $year }}">
|
|
</form>
|
|
|
|
{{-- Unduh rekap --}}
|
|
<div class="ms-auto">
|
|
<a class="btn btn-sm btn-download"
|
|
href="{{ route('admin.laporan.export', ['month' => (int) $month, 'year' => (int) $year]) }}">
|
|
<i class="ti ti-download me-1"></i> Unduh Rekap
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Table --}}
|
|
<table id="laporanTable" class="table align-middle w-100 table-sticky table-hlines table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Nama</th>
|
|
<th class="text-center">Hadir</th>
|
|
<th class="text-center">Izin</th>
|
|
<th class="text-center">Sakit</th>
|
|
<th class="text-center">Alpha</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse ($rows as $r)
|
|
@php $img = $photoUrl($r->url_photo); @endphp
|
|
<tr onclick="window.location='{{ route('admin.laporan.detail', ['user' => $r->id, 'month' => (int) $month, 'year' => (int) $year]) }}'"
|
|
style="cursor:pointer">
|
|
<td>
|
|
<div class="d-flex align-items-center gap-2">
|
|
@if ($img)
|
|
<span class="avatar-sm"><img src="{{ $img }}"
|
|
alt="{{ $r->name }}"></span>
|
|
@else
|
|
<span class="avatar-fallback">{{ $getInitials($r->name) }}</span>
|
|
@endif
|
|
|
|
<div class="lh-sm">
|
|
<div class="fw-semibold text-dark">{{ $r->name }}</div>
|
|
<small class="text-muted">{{ $r->jabatan ?? '-' }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<td class="text-center fw-semibold">{{ (int) ($r->hadir ?? 0) }}</td>
|
|
<td class="text-center fw-semibold">{{ (int) ($r->izin ?? 0) }}</td>
|
|
<td class="text-center fw-semibold">{{ (int) ($r->sakit ?? 0) }}</td>
|
|
<td class="text-center fw-semibold">{{ (int) ($r->alpha ?? 0) }}</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted py-4">
|
|
Tidak ada data user.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
|
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
|
|
<script src="https://cdn.datatables.net/responsive/2.5.0/js/responsive.bootstrap5.min.js"></script>
|
|
|
|
<script>
|
|
$(function() {
|
|
var hasData = $('#laporanTable tbody tr').length > 0 && !$('#laporanTable tbody tr td[colspan]').length;
|
|
|
|
let table = null;
|
|
if (typeof $.fn.DataTable !== 'undefined' && hasData) {
|
|
table = $('#laporanTable').DataTable({
|
|
autoWidth: false,
|
|
responsive: {
|
|
details: false
|
|
},
|
|
paging: true,
|
|
pageLength: 20,
|
|
lengthChange: false,
|
|
info: true,
|
|
ordering: true,
|
|
searching: true,
|
|
order: [
|
|
[0, 'asc']
|
|
],
|
|
language: {
|
|
info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ data",
|
|
infoEmpty: "Menampilkan 0 sampai 0 dari 0 data",
|
|
infoFiltered: "(difilter dari _MAX_ total data)",
|
|
paginate: {
|
|
first: "Pertama",
|
|
last: "Terakhir",
|
|
next: "Selanjutnya",
|
|
previous: "Sebelumnya"
|
|
},
|
|
emptyTable: "Tidak ada data yang tersedia",
|
|
zeroRecords: "Tidak ada data yang cocok"
|
|
},
|
|
columnDefs: [{
|
|
targets: [1, 2, 3, 4],
|
|
className: 'text-center text-nowrap'
|
|
}],
|
|
dom: "rt<'row align-items-center mt-3'<'col-sm-6 text-center text-sm-start'i><'col-sm-6 d-flex justify-content-center justify-content-sm-end'p>>",
|
|
});
|
|
|
|
// Search by name (input + tombol)
|
|
const applySearch = () => table.search($('#nameSearch').val()).draw();
|
|
|
|
$('#nameSearch').on('keyup', function(e) {
|
|
if (e.key === 'Enter') applySearch();
|
|
else table.search(this.value).draw();
|
|
});
|
|
|
|
$('#btnNameSearch').on('click', function() {
|
|
applySearch();
|
|
});
|
|
} else {
|
|
$('#nameSearch').prop('disabled', true);
|
|
$('#btnNameSearch').prop('disabled', true);
|
|
}
|
|
|
|
// Periode select => submit GET month/year
|
|
$('#periodSelect').on('change', function() {
|
|
const val = $(this).val(); // YYYY-MM
|
|
const parts = (val || '').split('-');
|
|
if (parts.length === 2) {
|
|
$('#yearInput').val(parseInt(parts[0], 10));
|
|
$('#monthInput').val(parseInt(parts[1], 10));
|
|
$('#periodForm').trigger('submit');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|