sidakpelem/resources/views/admin/features/laporan/detail-laporan.blade.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