529 lines
19 KiB
PHP
529 lines
19 KiB
PHP
@extends('admin.layouts.app')
|
||
|
||
@section('title', 'Dashboard')
|
||
|
||
@push('styles')
|
||
<link href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
||
{{-- kalau pakai responsive --}}
|
||
<link href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.bootstrap5.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
||
|
||
<style>
|
||
.soft-card {
|
||
border: 0;
|
||
border-radius: 16px;
|
||
box-shadow: 0 10px 30px rgba(16, 24, 40, .06);
|
||
}
|
||
|
||
.metric-wrap .metric {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-radius: 12px
|
||
}
|
||
|
||
.metric .icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 999px;
|
||
background: #eef2f7
|
||
}
|
||
|
||
.divider-vert {
|
||
width: 1px;
|
||
background: #eaeaea
|
||
}
|
||
|
||
.table thead th {
|
||
font-weight: 600;
|
||
color: #000000
|
||
}
|
||
|
||
.table-hover tbody tr:hover {
|
||
background: #f8fafc
|
||
}
|
||
|
||
.badge-status-hadir {
|
||
background: #a6e7d8;
|
||
color: #008767;
|
||
border: 1px solid rgba(0, 0, 0, .05)
|
||
}
|
||
|
||
.badge-status-izin {
|
||
background: #ffc5c5;
|
||
color: #df0404;
|
||
border: 1px solid rgba(0, 0, 0, .05)
|
||
}
|
||
|
||
/* DataTables polish */
|
||
.dataTables_wrapper .dataTables_info {
|
||
padding-top: .75rem
|
||
}
|
||
|
||
.dataTables_wrapper .dataTables_paginate .pagination {
|
||
margin: 0
|
||
}
|
||
|
||
.dataTables_wrapper .form-select.form-select-sm {
|
||
padding: .25rem 1.5rem .25rem .5rem
|
||
}
|
||
|
||
.dataTables_wrapper .dataTables_filter input {
|
||
width: 180px
|
||
}
|
||
|
||
@media (min-width:992px) {
|
||
.dataTables_wrapper .dataTables_filter input {
|
||
width: 260px
|
||
}
|
||
}
|
||
|
||
/* Kolom yang tak perlu wrapping supaya rapi tanpa scrollbar halaman */
|
||
th.col-time,
|
||
td.col-time,
|
||
th.col-date,
|
||
td.col-date,
|
||
th.col-dur,
|
||
td.col-dur,
|
||
th.col-status,
|
||
td.col-status {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Sticky header (opsional), hapus jika tidak perlu */
|
||
.table-sticky thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
background: #fff;
|
||
z-index: 1
|
||
}
|
||
|
||
/* Sembunyikan kontrol bawaan DT karena kita pakai kontrol custom */
|
||
div.dataTables_filter,
|
||
div.dataTables_length {
|
||
display: none
|
||
}
|
||
|
||
/* Only-horizontal-lines table (no vertical borders, no highlights) */
|
||
.table-hlines {
|
||
--line: #e9edf4;
|
||
}
|
||
|
||
/* reset & apply bottom borders only */
|
||
.table-hlines> :not(caption)>*>* {
|
||
border-top: 0;
|
||
border-right: 0 !important;
|
||
border-left: 0 !important;
|
||
border-bottom: 1px solid var(--line);
|
||
background: transparent;
|
||
/* no zebra bg */
|
||
}
|
||
|
||
/* header: garis bawah sedikit lebih tebal */
|
||
.table-hlines thead th {
|
||
border-bottom: 2px solid var(--line) !important;
|
||
}
|
||
|
||
/* baris terakhir: opsional, kalau mau tanpa garis paling bawah, aktifkan: */
|
||
/* .table-hlines tbody tr:last-child > *{ border-bottom:0; } */
|
||
|
||
/* matikan efek hover/highlight (Bootstrap + DataTables) */
|
||
.table-hlines tbody tr:hover>* {
|
||
background: transparent !important;
|
||
}
|
||
|
||
table.dataTable.hover>tbody>tr:hover>*,
|
||
table.dataTable.display>tbody>tr:hover>* {
|
||
background: transparent !important;
|
||
}
|
||
|
||
table.dataTable tbody tr.selected>* {
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
/* kalau sebelumnya ada rule .table-hover tbody tr:hover{background:#f8fafc} — override: */
|
||
.table-hlines.table-hover tbody tr:hover {
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* --- Unified metrics card --- */
|
||
.metrics-card.soft-card {
|
||
border-radius: 16px;
|
||
box-shadow: 0 10px 30px rgba(16, 24, 40, .06);
|
||
}
|
||
|
||
.metrics-row {
|
||
display: grid;
|
||
gap: 12px;
|
||
}
|
||
|
||
.metric-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 14px 18px;
|
||
border-radius: 12px;
|
||
/* biar tetap manis saat mobile (stack) */
|
||
flex: 1 1 auto;
|
||
min-width: 240px;
|
||
}
|
||
|
||
.metric-item .icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 999px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: #e9fbf2;
|
||
color: #0f766e;
|
||
/* mint + hijau */
|
||
}
|
||
|
||
.metric-item .metric-title {
|
||
color: #6b7280;
|
||
font-size: .85rem;
|
||
margin-bottom: .15rem;
|
||
}
|
||
|
||
.metric-item .metric-value {
|
||
font-weight: 600;
|
||
font-size: 1.35rem;
|
||
margin: 0;
|
||
}
|
||
|
||
/* Divider antar metric saat layar ≥ md */
|
||
@media (min-width: 768px) {
|
||
.metrics-row {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 0;
|
||
}
|
||
|
||
.metric-item {
|
||
padding: 18px 22px;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.metric-item+.metric-item {
|
||
border-left: 1px solid #eef2f7;
|
||
}
|
||
}
|
||
|
||
/* Tanpa highlight saat hover */
|
||
.metric-item:hover {
|
||
background: transparent;
|
||
}
|
||
|
||
/* Toolbar mobile: jadi kolom penuh */
|
||
@media (max-width: 767.98px) {
|
||
.toolbar {
|
||
flex-direction: column !important;
|
||
align-items: stretch !important;
|
||
gap: .5rem !important;
|
||
}
|
||
|
||
.toolbar .controls {
|
||
flex-direction: column !important;
|
||
align-items: stretch !important;
|
||
}
|
||
|
||
.toolbar .controls>* {
|
||
width: 100% !important;
|
||
/* setiap kontrol full width */
|
||
}
|
||
|
||
/* search biar melebar penuh di mobile */
|
||
.toolbar .input-group {
|
||
max-width: 100% !important;
|
||
}
|
||
}
|
||
|
||
/* Mobile: semua kontrol full width; Desktop: auto */
|
||
@media (max-width: 767.98px) {
|
||
.input-group.input-group-sm {
|
||
max-width: 100% !important;
|
||
}
|
||
|
||
.dropdown>.btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.dropdown-menu {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* Desktop: kasih ruang cari sedikit lebih lebar */
|
||
@media (min-width: 992px) {
|
||
.input-group.input-group-sm {
|
||
max-width: 320px !important;
|
||
}
|
||
}
|
||
</style>
|
||
@endpush
|
||
|
||
|
||
@section('content')
|
||
{{-- ======= TOP METRICS (unified card) ======= --}}
|
||
<div class="card soft-card metrics-card mb-4">
|
||
<div class="card-body">
|
||
<div class="metrics-row">
|
||
<!-- Presensi -->
|
||
<div class="metric-item">
|
||
<div class="icon"><i class="ti ti-users-group fs-4"></i></div>
|
||
<div>
|
||
<div class="metric-title">Presensi</div>
|
||
<div class="d-flex align-items-baseline gap-2">
|
||
<h4 class="metric-value">{{ $lengthAttendance }}/{{ $lengthEmployee }}</h4>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Anggota -->
|
||
<div class="metric-item">
|
||
<div class="icon"><i class="ti ti-user-plus fs-4"></i></div>
|
||
<div>
|
||
<div class="metric-title">Anggota</div>
|
||
<div class="d-flex align-items-baseline gap-2">
|
||
<h4 class="metric-value">{{ $lengthEmployee }}</h4>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Izin -->
|
||
<div class="metric-item">
|
||
<div class="icon"><i class="ti ti-device-desktop fs-4"></i></div>
|
||
<div>
|
||
<div class="metric-title">Izin</div>
|
||
<div class="d-flex align-items-baseline gap-2">
|
||
<h4 class="metric-value">{{ $izin->count() }}</h4>
|
||
<small class="badge text-bg-light">—</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ======= TABLE CARD ======= --}}
|
||
<div class="card soft-card">
|
||
<div class="card-body">
|
||
|
||
<!-- Toolbar: length + search + sort -->
|
||
<div class="row gy-2 align-items-center mb-3">
|
||
<!-- Kiri: judul -->
|
||
<div class="col-12 col-md">
|
||
<h4 class="mb-0">Absensi Perangkat Desa</h4>
|
||
<small style="color:#0f766e">Rekapitulasi Absensi Tanggal : {{ now()->format('d-m-Y') }}</small>
|
||
</div>
|
||
|
||
<!-- Kanan: kontrol (stack di mobile, sejajar di md+) -->
|
||
<div class="col-12 col-md-auto">
|
||
<div class="d-flex flex-column flex-md-row align-items-stretch align-items-md-center gap-2">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<label for="tableLength" class="small text-secondary mb-0 d-none d-md-inline">Tampilkan</label>
|
||
<select id="tableLength" class="form-select form-select-sm w-auto">
|
||
<option value="8" selected>8</option>
|
||
<option value="15">15</option>
|
||
<option value="25">25</option>
|
||
<option value="50">50</option>
|
||
<option value="-1">Semua</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="input-group input-group-sm grow flex-md-grow-0" style="max-width:260px;">
|
||
<span class="input-group-text bg-white"><i class="ti ti-search"></i></span>
|
||
<input id="tableSearch" type="text" class="form-control" placeholder="Cari nama/tanggal...">
|
||
</div>
|
||
|
||
<div class="dropdown">
|
||
<button id="sortDropdownLabel" class="btn btn-sm btn-light border dropdown-toggle"
|
||
data-bs-toggle="dropdown">
|
||
Sort by: Newest
|
||
</button>
|
||
<ul class="dropdown-menu dropdown-menu-end">
|
||
<li><a class="dropdown-item" href="#" data-sort="newest">Newest</a></li>
|
||
<li><a class="dropdown-item" href="#" data-sort="oldest">Oldest</a></li>
|
||
<li><a class="dropdown-item" href="#" data-sort="name-asc">Name A–Z</a></li>
|
||
<li><a class="dropdown-item" href="#" data-sort="name-desc">Name Z–A</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<table id="attendanceTable" class="table align-middle w-100 table-sticky table-hlines">
|
||
|
||
<thead class="border-bottom">
|
||
<tr>
|
||
<th>Nama</th>
|
||
<th class="col-time d-none d-md-table-cell">Masuk</th>
|
||
<th class="col-time d-none d-md-table-cell">Pulang</th>
|
||
<th class="col-date">Tanggal</th>
|
||
<th class="col-dur d-none d-md-table-cell">Durasi</th>
|
||
<th class="col-status">Keterangan</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse ($data as $attendance)
|
||
@php
|
||
// nama
|
||
$name = $attendance->user->name;
|
||
|
||
// jam masuk & pulang (atau '-' jika null)
|
||
$masuk = optional($attendance->check_in)->format('H:i') ?: '-';
|
||
$pulang = optional($attendance->check_out)->format('H:i') ?: '-';
|
||
|
||
// tanggal untuk display & order
|
||
$tanggalOrd = $attendance->date->format('Ymd');
|
||
$tanggalDisp = $attendance->date->format('d M Y');
|
||
|
||
// hitung durasi jika ada check_in & check_out
|
||
if ($attendance->check_in && $attendance->check_out) {
|
||
$diff = $attendance->check_in->diff($attendance->check_out);
|
||
$durasi = sprintf('%02d/8 jam', $diff->h, $diff->i);
|
||
} else {
|
||
$durasi = '-';
|
||
}
|
||
|
||
// badge
|
||
// Mapping status → [CSS class, Teks badge]
|
||
$badgeMap = [
|
||
'hadir' => ['badge-status-hadir', 'Hadir'],
|
||
'izin' => ['badge-status-izin', 'Izin'],
|
||
'sakit' => ['badge-status-sakit', 'Sakit'],
|
||
'alpha' => ['badge-status-izin', 'Alpha'],
|
||
];
|
||
|
||
// Ambil status user, fallback ke 'alpha' kalau tidak ketemu
|
||
$status = $attendance->status;
|
||
[$badgeClass, $badgeText] = $badgeMap[$status] ?? $badgeMap['alpha'];
|
||
@endphp
|
||
<tr>
|
||
<td>{{ $name }}</td>
|
||
<td d-none d-md-table-cell>{{ $masuk }}</td>
|
||
<td d-none d-md-table-cell>{{ $pulang }}</td>
|
||
<td data-order="{{ $tanggalOrd }}">{{ $tanggalDisp }}</td>
|
||
<td d-none d-md-table-cell>{{ $durasi }}</td>
|
||
<td>
|
||
<span class="badge rounded-pill {{ $badgeClass }}">
|
||
{{ $badgeText }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="6" class="text-center py-4">
|
||
<div class="text-muted">
|
||
<i class="ti ti-news-off" style="font-size: 2rem;"></i>
|
||
<p class="mt-2 mb-0">Belum ada Pegawai Absen Hari ini</p>
|
||
<small>Silakan beritahu pegawai agar melakukan absensi</small>
|
||
</div>
|
||
</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 = $('#attendanceTable tbody tr').length > 0 && !$('#attendanceTable tbody tr td[colspan]')
|
||
.length;
|
||
|
||
if (typeof $.fn.DataTable !== 'undefined' && hasData) {
|
||
var table = $('#attendanceTable').DataTable({
|
||
autoWidth: false,
|
||
responsive: {
|
||
details: false
|
||
},
|
||
paging: true,
|
||
pageLength: 8,
|
||
lengthChange: true, // tetap true agar API page.len() aktif
|
||
info: true,
|
||
ordering: true,
|
||
searching: true,
|
||
order: [
|
||
[3, 'desc']
|
||
], // Tanggal terbaru
|
||
language: {
|
||
search: "Cari:",
|
||
searchPlaceholder: "Ketik untuk mencari...",
|
||
lengthMenu: "Tampilkan _MENU_",
|
||
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, 5],
|
||
className: 'text-nowrap'
|
||
},
|
||
{
|
||
targets: 5,
|
||
orderable: false
|
||
}
|
||
],
|
||
// DOM minimal: biar info & paginate otomatis di bawah, tanpa filter/length bawaan (kita pakai custom)
|
||
dom: "rt<'row align-items-center mt-3'<'col-sm-6'i><'col-sm-6 d-flex justify-content-end'p>>"
|
||
});
|
||
|
||
// Search custom
|
||
$('#tableSearch').on('keyup', function() {
|
||
table.search(this.value).draw();
|
||
});
|
||
|
||
// Page length custom
|
||
$('#tableLength').on('change', function() {
|
||
table.page.len(parseInt(this.value, 10)).draw();
|
||
});
|
||
|
||
// Sort dropdown custom
|
||
$('[data-sort]').on('click', function(e) {
|
||
e.preventDefault();
|
||
const sort = $(this).data('sort');
|
||
if (sort === 'newest') table.order([
|
||
[3, 'desc']
|
||
]).draw();
|
||
if (sort === 'oldest') table.order([
|
||
[3, 'asc']
|
||
]).draw();
|
||
if (sort === 'name-asc') table.order([
|
||
[0, 'asc']
|
||
]).draw();
|
||
if (sort === 'name-desc') table.order([
|
||
[0, 'desc']
|
||
]).draw();
|
||
$('#sortDropdownLabel').text($(this).text());
|
||
});
|
||
|
||
} else if (!hasData) {
|
||
console.log('Table is empty, DataTables not initialized');
|
||
} else {
|
||
console.error('DataTables not loaded!');
|
||
}
|
||
});
|
||
</script>
|
||
@endpush
|