515 lines
19 KiB
PHP
515 lines
19 KiB
PHP
@extends('admin.layouts.app')
|
|
|
|
@section('title', 'Detail Absensi')
|
|
|
|
@push('styles')
|
|
<style>
|
|
.soft-card {
|
|
border: 0;
|
|
border-radius: 16px;
|
|
box-shadow: 0 10px 30px rgba(16, 24, 40, .06)
|
|
}
|
|
|
|
.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
|
|
}
|
|
|
|
.avatar-xl {
|
|
width: 72px;
|
|
height: 72px;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
flex: 0 0 auto;
|
|
border: 4px solid #fff;
|
|
box-shadow: 0 10px 25px rgba(16, 24, 40, .12)
|
|
}
|
|
|
|
.avatar-xl img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover
|
|
}
|
|
|
|
.avatar-fallback-xl {
|
|
width: 72px;
|
|
height: 72px;
|
|
border-radius: 999px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
font-weight: 800;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.mini-stat {
|
|
border-radius: 12px;
|
|
color: #fff;
|
|
padding: 6px 10px;
|
|
display: flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
min-height: 50px;
|
|
}
|
|
|
|
.mini-stat i {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.mini-stat .num {
|
|
font-size: 22px;
|
|
font-weight: 800;
|
|
line-height: 1
|
|
}
|
|
|
|
.mini-stat .lbl {
|
|
font-size: 14px;
|
|
opacity: .95
|
|
}
|
|
|
|
.bg-hadir {
|
|
background: #0f766e
|
|
}
|
|
|
|
.bg-izin {
|
|
background: #10b981
|
|
}
|
|
|
|
.bg-sakit {
|
|
background: #f97316
|
|
}
|
|
|
|
.bg-alpha {
|
|
background: #ef4444
|
|
}
|
|
|
|
.badge-status {
|
|
padding: .35rem .7rem;
|
|
border-radius: 999px;
|
|
font-weight: 700;
|
|
font-size: .75rem
|
|
}
|
|
|
|
.st-hadir {
|
|
background: #d1fae5;
|
|
color: #065f46
|
|
}
|
|
|
|
.st-izin {
|
|
background: #dcfce7;
|
|
color: #166534
|
|
}
|
|
|
|
.st-sakit {
|
|
background: #ffedd5;
|
|
color: #9a3412
|
|
}
|
|
|
|
.st-alpha {
|
|
background: #fee2e2;
|
|
color: #991b1b
|
|
}
|
|
|
|
.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
|
|
}
|
|
|
|
.top-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.summary-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.profile-panel {
|
|
/* border: 1px solid #e9edf4;
|
|
border-radius: 12px; */
|
|
padding: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
flex: 1 1 300px;
|
|
min-width: 280px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(100px, 1fr));
|
|
gap: 10px;
|
|
flex: 0 1 600px;
|
|
width: 100%;
|
|
max-width: 600px;
|
|
min-width: 320px;
|
|
}
|
|
|
|
.stats-grid .mini-stat {
|
|
height: 100%;
|
|
}
|
|
|
|
.profile-panel .h5 {
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
.profile-panel .text-muted {
|
|
font-size: .95rem;
|
|
}
|
|
|
|
.present-progress .progress {
|
|
height: 18px;
|
|
border-radius: 999px;
|
|
background: #e2e8f0;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.present-progress .progress-bar {
|
|
background: #0f766e;
|
|
}
|
|
|
|
.present-progress .progress-label {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.btn-download {
|
|
background: #f97316;
|
|
color: #fff;
|
|
border: 0;
|
|
}
|
|
|
|
.btn-download:hover {
|
|
background: #f97316;
|
|
color: #fff;
|
|
opacity: .9;
|
|
}
|
|
|
|
@media (max-width: 992px) {
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
|
}
|
|
}
|
|
</style>
|
|
@endpush
|
|
|
|
@section('content')
|
|
@php
|
|
use Illuminate\Support\Str;
|
|
|
|
$photo = null;
|
|
if (!empty($user->url_photo)) {
|
|
$photo = Str::startsWith($user->url_photo, ['http://', 'https://'])
|
|
? $user->url_photo
|
|
: asset('storage/' . ltrim($user->url_photo, '/'));
|
|
}
|
|
|
|
$initials = strtoupper(
|
|
collect(preg_split('/\s+/', trim((string) $user->name)))
|
|
->filter()
|
|
->take(2)
|
|
->map(fn($w) => Str::substr($w, 0, 1))
|
|
->join(''),
|
|
);
|
|
$selectedPeriod = sprintf('%04d-%02d', (int) $year, (int) $month);
|
|
@endphp
|
|
|
|
<div class="card soft-card">
|
|
<div class="card-body">
|
|
@if (session('success'))
|
|
<div class="alert alert-success">{{ session('success') }}</div>
|
|
@endif
|
|
|
|
@if ($errors->any())
|
|
<div class="alert alert-danger">
|
|
<ul class="mb-0">
|
|
@foreach ($errors->all() as $error)
|
|
<li>{{ $error }}</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Top filter actions --}}
|
|
<div class="top-actions">
|
|
<div class=" d-flex align-items-center gap-2">
|
|
<span class="filter-label">Periode:</span>
|
|
|
|
<form id="periodForm" method="GET" action="{{ route('admin.laporan.detail', ['user' => $user->id]) }}">
|
|
<select id="periodSelect" class="form-select form-select-sm" style="min-width:180px;">
|
|
@for ($i = 0; $i < 24; $i++)
|
|
@php
|
|
$d = now()->subMonths($i);
|
|
$val = $d->format('Y-m');
|
|
@endphp
|
|
<option value="{{ $val }}" {{ $val === $selectedPeriod ? 'selected' : '' }}>
|
|
{{ $d->translatedFormat('F Y') }}
|
|
</option>
|
|
@endfor
|
|
</select>
|
|
<input type="hidden" name="month" id="monthInput" value="{{ (int) $month }}">
|
|
<input type="hidden" name="year" id="yearInput" value="{{ (int) $year }}">
|
|
</form>
|
|
</div>
|
|
|
|
<a class="btn btn-sm btn-download rounded-3 p-2 me-1 d-inline-flex align-items-center"
|
|
href="{{ route('admin.laporan.detail.export', ['user' => $user->id, 'month' => (int) $month, 'year' => (int) $year]) }}">
|
|
<i class="ti ti-download me-1"></i> Unduh Rekap
|
|
</a>
|
|
</div>
|
|
|
|
{{-- Profile + rekap sejajar --}}
|
|
<div class="summary-row">
|
|
<div class="profile-panel">
|
|
@if ($photo)
|
|
<div class="avatar-xl"><img src="{{ $photo }}" alt="{{ $user->name }}"></div>
|
|
@else
|
|
<div class="avatar-fallback-xl">{{ $initials }}</div>
|
|
@endif
|
|
|
|
<div>
|
|
<div class="text-muted small">Anggota</div>
|
|
<div class="h5 mb-0">{{ $user->name }}</div>
|
|
<div class="text-muted">{{ $user->jabatan ?? '-' }}</div>
|
|
|
|
<div class="mt-2 present-progress" style="max-width:260px;">
|
|
<div class="progress">
|
|
<div class="progress-bar" role="progressbar"
|
|
style="width: {{ max(0, min(100, (int) $presentPercent)) }}%"></div>
|
|
<span class="progress-label">{{ (int) $presentPercent }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="mini-stat bg-hadir">
|
|
<div><i class="ti ti-checks"></i></div>
|
|
<div>
|
|
<div class="num">{{ $summary['hadir'] }}</div>
|
|
<div class="lbl">Hadir</div>
|
|
</div>
|
|
</div>
|
|
<div class="mini-stat bg-izin">
|
|
<div><i class="ti ti-file-text"></i></div>
|
|
<div>
|
|
<div class="num">{{ $summary['izin'] }}</div>
|
|
<div class="lbl">Izin</div>
|
|
</div>
|
|
</div>
|
|
<div class="mini-stat bg-sakit">
|
|
<div><i class="ti ti-thermometer"></i></div>
|
|
<div>
|
|
<div class="num">{{ $summary['sakit'] }}</div>
|
|
<div class="lbl">Sakit</div>
|
|
</div>
|
|
</div>
|
|
<div class="mini-stat bg-alpha">
|
|
<div><i class="ti ti-alert-triangle"></i></div>
|
|
<div>
|
|
<div class="num">{{ $summary['alpha'] }}</div>
|
|
<div class="lbl">Alpha</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Table detail --}}
|
|
<div class="table-responsive">
|
|
<table class="table align-middle w-100 table-hlines">
|
|
<thead>
|
|
<tr>
|
|
<th>Tanggal</th>
|
|
<th class="text-center">Datang</th>
|
|
<th class="text-center">Pulang</th>
|
|
<th class="text-center">Durasi</th>
|
|
<th class="text-center">Status</th>
|
|
<th>Keterangan</th>
|
|
<th class="text-center">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($items as $it)
|
|
@php
|
|
$status = $it->status ?? '-';
|
|
$badge = match ($status) {
|
|
'hadir' => 'st-hadir',
|
|
'izin' => 'st-izin',
|
|
'sakit' => 'st-sakit',
|
|
'alpha' => 'st-alpha',
|
|
default => 'st-izin',
|
|
};
|
|
@endphp
|
|
<tr>
|
|
<td class="text-nowrap">{{ $it->date_label }}</td>
|
|
<td class="text-center text-nowrap">{{ $it->check_in_label }}</td>
|
|
<td class="text-center text-nowrap">{{ $it->check_out_label }}</td>
|
|
<td class="text-center text-nowrap">{{ $it->duration_label }}</td>
|
|
<td class="text-center">
|
|
<span class="badge-status {{ $badge }}">{{ ucfirst($status) }}</span>
|
|
</td>
|
|
<td>{{ $it->notes_label }}</td>
|
|
<td class="text-center">
|
|
<div class="d-flex justify-content-center gap-1">
|
|
@if ($it->status == 'hadir' && $it->osm_url)
|
|
<button type="button" class="btn btn-sm btn-light border"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#locationAttendanceModal-{{ $it->id }}"
|
|
{{ $it->osm_url ? '' : 'disabled' }}>
|
|
Detail Lokasi
|
|
</button>
|
|
@endif
|
|
<button type="button" class="btn btn-sm btn-light border" data-bs-toggle="modal"
|
|
data-bs-target="#editAttendanceModal-{{ $it->id }}">
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted py-4">
|
|
Tidak ada riwayat absensi pada periode ini.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
@foreach ($items as $it)
|
|
<div class="modal fade" id="locationAttendanceModal-{{ $it->id }}" tabindex="-1"
|
|
aria-labelledby="locationAttendanceLabel-{{ $it->id }}" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="locationAttendanceLabel-{{ $it->id }}">Detail Lokasi
|
|
Absensi
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-2"><strong>Koordinat:</strong> {{ $it->location_label }}</div>
|
|
@if ($it->osm_embed_url && $it->osm_url)
|
|
<div class="ratio ratio-16x9 mb-3">
|
|
<iframe src="{{ $it->osm_embed_url }}" loading="lazy"
|
|
referrerpolicy="no-referrer-when-downgrade"></iframe>
|
|
</div>
|
|
<a href="{{ $it->osm_url }}" target="_blank" rel="noopener"
|
|
class="btn btn-sm btn-success">
|
|
Buka di OpenStreetMap
|
|
</a>
|
|
@else
|
|
<div class="alert alert-warning mb-0">Koordinat tidak tersedia atau tidak valid.</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="editAttendanceModal-{{ $it->id }}" tabindex="-1"
|
|
aria-labelledby="editAttendanceLabel-{{ $it->id }}" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editAttendanceLabel-{{ $it->id }}">Edit Absensi</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
|
aria-label="Close"></button>
|
|
</div>
|
|
|
|
<form action="{{ route('admin.attendance.update', ['attendance' => $it->id]) }}"
|
|
method="POST">
|
|
@csrf
|
|
@method('PUT')
|
|
<input type="hidden" name="month" value="{{ (int) $month }}">
|
|
<input type="hidden" name="year" value="{{ (int) $year }}">
|
|
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Status</label>
|
|
<select name="status" class="form-select" required>
|
|
<option value="hadir" {{ $it->status === 'hadir' ? 'selected' : '' }}>Hadir
|
|
</option>
|
|
<option value="izin" {{ $it->status === 'izin' ? 'selected' : '' }}>Izin
|
|
</option>
|
|
<option value="sakit" {{ $it->status === 'sakit' ? 'selected' : '' }}>Sakit
|
|
</option>
|
|
<option value="alpha" {{ $it->status === 'alpha' ? 'selected' : '' }}>Alpha
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-0">
|
|
<label class="form-label">Keterangan</label>
|
|
<textarea name="notes" class="form-control" rows="4" placeholder="Tulis keterangan...">{{ $it->notes }}</textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Batal</button>
|
|
<button type="submit" class="btn btn-success">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.getElementById('periodSelect')?.addEventListener('change', function() {
|
|
const val = this.value; // YYYY-MM
|
|
const parts = val.split('-');
|
|
if (parts.length === 2) {
|
|
document.getElementById('yearInput').value = parseInt(parts[0], 10);
|
|
document.getElementById('monthInput').value = parseInt(parts[1], 10);
|
|
document.getElementById('periodForm').submit();
|
|
}
|
|
});
|
|
</script>
|
|
@endpush
|