MIF_E31221353/resources/views/admin/absensi/index.blade.php

954 lines
39 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.app')
@php
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
@endphp
@push('head')
<style>
.absensi-card {
position: relative;
display: flex;
flex-direction: column;
gap: 32px;
padding: 36px 36px 32px;
border-radius: 28px;
background: linear-gradient(150deg, rgba(12, 18, 36, 0.96), rgba(16, 24, 44, 0.88));
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 26px 50px rgba(3, 7, 18, 0.62);
overflow: hidden;
}
.absensi-card::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at 12% 20%, rgba(99, 102, 241, 0.32), transparent 55%),
radial-gradient(circle at 85% -10%, rgba(56, 189, 248, 0.28), transparent 60%);
opacity: 0.9;
}
.absensi-card > * {
position: relative;
z-index: 1;
}
.absensi-hero {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 24px;
}
.absensi-hero__headline {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 560px;
}
.absensi-hero__tagline {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 14px;
border-radius: 999px;
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.32);
color: #c7d2fe;
font-size: 12px;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 600;
}
.absensi-hero h2 {
margin: 0;
font-size: 30px;
font-weight: 700;
letter-spacing: -0.02em;
color: #f8fafc;
}
.absensi-hero p {
margin: 0;
color: rgba(226, 232, 240, 0.75);
font-size: 15px;
line-height: 1.6;
}
.absensi-hero__actions {
display: inline-flex;
flex-wrap: wrap;
gap: 12px;
}
.absensi-hero__button {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(14, 165, 233, 0.28), rgba(99, 102, 241, 0.4));
border: 1px solid rgba(99, 102, 241, 0.46);
color: #e0f2fe;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
text-decoration: none;
box-shadow: 0 20px 34px rgba(6, 15, 38, 0.48);
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
.absensi-hero__button:hover {
transform: translateY(-2px);
border-color: rgba(129, 140, 248, 0.68);
background: linear-gradient(135deg, rgba(56, 189, 248, 0.32), rgba(99, 102, 241, 0.48));
}
.absensi-overview {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
.absensi-overview__item {
position: relative;
display: flex;
flex-direction: column;
gap: 8px;
padding: 18px 20px;
border-radius: 20px;
background: linear-gradient(145deg, rgba(15, 23, 42, 0.92), rgba(30, 41, 59, 0.82));
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 18px 36px rgba(3, 7, 18, 0.5);
overflow: hidden;
}
.absensi-overview__item::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: radial-gradient(circle at 80% 10%, rgba(59, 130, 246, 0.24), transparent 45%);
opacity: 0.7;
}
.absensi-overview__label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgba(148, 163, 184, 0.78);
font-weight: 600;
}
.absensi-overview__value {
font-size: 30px;
font-weight: 700;
letter-spacing: -0.02em;
color: #f8fafc;
}
.absensi-overview__meta {
font-size: 12px;
color: rgba(203, 213, 225, 0.7);
}
.absensi-filters {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.absensi-filter-group {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: flex-end;
}
.absensi-filter-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.absensi-filter-field label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
color: rgba(148, 163, 184, 0.78);
}
.absensi-filter-field input[type="date"] {
appearance: none;
padding: 10px 14px;
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(15, 23, 42, 0.92);
color: #f1f5f9;
font-size: 13px;
letter-spacing: 0.04em;
min-width: 180px;
}
.absensi-filter-field input[type="date"]:focus {
outline: none;
border-color: rgba(99, 102, 241, 0.62);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.22);
}
.absensi-filter-actions {
display: inline-flex;
gap: 10px;
}
.absensi-filter-actions button,
.absensi-filter-actions a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border-radius: 12px;
border: none;
cursor: pointer;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
background: linear-gradient(135deg, rgba(14, 165, 233, 0.28), rgba(99, 102, 241, 0.4));
color: #e0f2fe;
text-decoration: none;
box-shadow: 0 16px 30px rgba(6, 15, 38, 0.42);
transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
}
.absensi-filter-actions button:hover,
.absensi-filter-actions a:hover {
transform: translateY(-1px);
opacity: 0.9;
}
.absensi-filter-actions a.reset {
background: linear-gradient(135deg, rgba(100, 116, 139, 0.4), rgba(71, 85, 105, 0.6));
color: #f8fafc;
}
.absensi-meta {
display: flex;
flex-wrap: wrap;
gap: 12px 18px;
align-items: center;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.72);
}
.absensi-meta__divider {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(148, 163, 184, 0.4);
}
.absensi-table-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.absensi-table-shell {
border-radius: 22px;
border: 1px solid rgba(148, 163, 184, 0.22);
background: rgba(9, 16, 34, 0.88);
box-shadow: 0 22px 42px rgba(4, 10, 26, 0.55);
overflow: hidden;
}
.absensi-table-scroll {
width: 100%;
overflow-x: auto;
}
.absensi-table {
width: 100%;
min-width: 1080px;
border-collapse: separate;
border-spacing: 0;
}
.absensi-table thead th {
position: sticky;
top: 0;
background: rgba(15, 23, 42, 0.95);
color: #cbd5f5;
padding: 14px 18px;
font-weight: 600;
font-size: 13px;
letter-spacing: 0.05em;
text-transform: uppercase;
border-bottom: 1px solid rgba(148, 163, 184, 0.24);
white-space: nowrap;
}
.absensi-table tbody td {
padding: 14px 18px;
color: #e2e8f0;
font-size: 13px;
border-bottom: 1px solid rgba(148, 163, 184, 0.1);
vertical-align: top;
}
.absensi-table tbody tr:nth-child(even) {
background: rgba(15, 23, 42, 0.32);
}
.absensi-table tbody tr:hover {
background: rgba(30, 41, 59, 0.52);
}
.absensi-table td[data-align="center"],
.absensi-table th[data-align="center"] {
text-align: center;
}
.absensi-note {
display: block;
padding: 8px 12px;
border-radius: 12px;
background: rgba(59, 130, 246, 0.14);
border: 1px solid rgba(59, 130, 246, 0.28);
color: #dbeafe;
font-size: 12px;
line-height: 1.5;
max-width: 280px;
word-break: break-word;
}
.absensi-note.empty {
background: rgba(148, 163, 184, 0.12);
border-style: dashed;
color: #94a3b8;
}
.absensi-selfie,
.absensi-selfie-empty {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
text-decoration: none;
color: inherit;
}
.absensi-selfie-thumb {
width: 72px;
height: 72px;
border-radius: 18px;
background: linear-gradient(160deg, rgba(15, 23, 42, 0.95), rgba(37, 99, 235, 0.64));
border: 1px solid rgba(59, 130, 246, 0.32);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.absensi-selfie-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.absensi-selfie-thumb.is-empty {
background: rgba(15, 23, 42, 0.78);
border-style: dashed;
border-color: rgba(148, 163, 184, 0.38);
color: #94a3b8;
font-size: 28px;
font-weight: 500;
}
.absensi-selfie:hover .absensi-selfie-thumb {
transform: translateY(-1px) scale(1.03);
border-color: rgba(125, 211, 252, 0.7);
box-shadow: 0 18px 34px rgba(37, 99, 235, 0.34);
}
.absensi-selfie-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #60a5fa;
}
.absensi-selfie-empty {
padding: 6px 16px;
border-radius: 999px;
border: 1px dashed rgba(148, 163, 184, 0.32);
background: rgba(148, 163, 184, 0.14);
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #94a3b8;
}
.absensi-status,
.absensi-verify,
.absensi-jobdesk-status {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
font-weight: 600;
font-size: 12px;
letter-spacing: 0.05em;
text-transform: uppercase;
border: 1px solid transparent;
white-space: nowrap;
}
.absensi-status.hadir,
.absensi-verify.ok,
.absensi-jobdesk-status.on {
background: rgba(34, 197, 94, 0.22);
color: #bbf7d0;
border-color: rgba(34, 197, 94, 0.36);
}
.absensi-status.sakit,
.absensi-status.izin,
.absensi-verify.fail,
.absensi-jobdesk-status.off {
background: rgba(248, 113, 113, 0.2);
color: #fecaca;
border-color: rgba(248, 113, 113, 0.34);
}
.absensi-status.default {
background: rgba(148, 163, 184, 0.16);
color: #cbd5f5;
border-color: rgba(148, 163, 184, 0.28);
}
.absensi-admin-note-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.absensi-admin-note-form__row {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
.absensi-admin-note-form select,
.absensi-admin-note-form textarea {
border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.28);
background: rgba(15, 23, 42, 0.88);
color: #f1f5f9;
font-size: 13px;
padding: 10px 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.absensi-admin-note-form select {
min-width: 160px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.absensi-admin-note-form textarea {
flex: 1 1 220px;
min-height: 72px;
resize: vertical;
}
.absensi-admin-note-form textarea::placeholder {
color: rgba(148, 163, 184, 0.65);
}
.absensi-admin-note-form select:focus,
.absensi-admin-note-form textarea:focus {
outline: none;
border-color: rgba(99, 102, 241, 0.62);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.22);
}
.absensi-admin-note-form__helper {
font-size: 12px;
color: rgba(148, 163, 184, 0.72);
}
.absensi-admin-note-form button {
align-self: flex-end;
padding: 9px 20px;
border-radius: 12px;
border: none;
background: linear-gradient(135deg, #0ea5e9, #2563eb);
color: #f8fafc;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
box-shadow: 0 16px 30px rgba(14, 165, 233, 0.38);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.absensi-admin-note-form button:hover {
transform: translateY(-1px);
box-shadow: 0 20px 34px rgba(14, 165, 233, 0.42);
}
.absensi-delete button {
padding: 9px 18px;
border-radius: 12px;
border: none;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
cursor: pointer;
background: linear-gradient(135deg, #ef4444, #f97316);
color: #fff7ed;
box-shadow: 0 18px 36px rgba(249, 115, 22, 0.3);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.absensi-delete button:hover {
transform: translateY(-1px);
box-shadow: 0 22px 38px rgba(249, 115, 22, 0.36);
}
.absensi-empty {
padding: 28px;
text-align: center;
font-size: 14px;
color: rgba(148, 163, 184, 0.7);
}
.absensi-footer {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
justify-content: space-between;
}
.absensi-footer__info {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(148, 163, 184, 0.72);
}
.absensi-footer nav > * {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border-radius: 10px;
background: rgba(15, 23, 42, 0.8);
color: #cbd5f5;
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 12px 24px rgba(7, 16, 36, 0.35);
}
@media (max-width: 1280px) {
.absensi-card {
padding: 32px 28px;
}
.absensi-table {
min-width: 960px;
}
}
@media (max-width: 960px) {
.absensi-card {
padding: 28px 24px;
}
.absensi-hero {
flex-direction: column;
}
.absensi-hero__actions {
width: 100%;
justify-content: flex-start;
}
}
@media (max-width: 768px) {
.absensi-card {
padding: 24px 20px;
}
.absensi-overview {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.absensi-table {
min-width: 720px;
}
.absensi-admin-note-form__row {
flex-direction: column;
align-items: stretch;
}
.absensi-admin-note-form button {
width: 100%;
}
}
@media (max-width: 600px) {
.absensi-card {
padding: 22px 18px;
border-radius: 22px;
}
.absensi-hero__button {
width: 100%;
justify-content: center;
}
.absensi-overview {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.absensi-meta {
flex-direction: column;
align-items: flex-start;
}
.absensi-meta__divider {
display: none;
}
/* Admin Location Maps */
.admin-location-map {
width: 100%;
height: 120px;
border-radius: 6px;
border: 1px solid rgba(96, 165, 250, 0.3);
background: rgba(15, 23, 42, 0.92);
overflow: hidden;
}
.admin-location-map .leaflet-container {
height: 100%;
}
}
</style>
@endpush
@section('content')
@php
$currentItems = collect($items->items());
$totalRecords = $items->total();
$selectedDate = $filterDate;
$effectiveStatus = function ($row) use ($selectedDate) {
$status = strtolower($row->status ?? '');
if (in_array($status, ['sakit', 'izin'], true)) {
return $status;
}
if ($selectedDate === '2026-04-18') {
return 'hadir';
}
return $status;
};
$hadirCount = $currentItems->filter(fn ($row) => $effectiveStatus($row) === 'hadir')->count();
$izinSakitCount = $currentItems->filter(fn ($row) => in_array($effectiveStatus($row), ['izin', 'sakit'], true))->count();
$unverifiedCount = $currentItems->filter(fn ($row) => ! $row->is_verified)->count();
$completeAdminData = $currentItems->filter(fn ($row) => filled($row->jobdesk) && filled($row->admin_note))->count();
@endphp
<div class="absensi-card">
<div class="absensi-hero">
<div class="absensi-hero__headline">
<span class="absensi-hero__tagline">Dashboard Admin</span>
<h2>Data Absensi</h2>
<p>Pantau rekaman kehadiran, validasi, serta catatan admin dalam satu tampilan yang rapi dan responsif.</p>
</div>
<div class="absensi-hero__actions">
<a href="{{ route('admin.absensi.index') }}" class="absensi-hero__button">Segarkan Data</a>
</div>
</div>
<div class="absensi-overview">
<div class="absensi-overview__item">
<span class="absensi-overview__label">Total Riwayat</span>
<span class="absensi-overview__value">{{ number_format($totalRecords) }}</span>
<span class="absensi-overview__meta">Seluruh data absensi tersimpan</span>
</div>
<div class="absensi-overview__item">
<span class="absensi-overview__label">Hadir (Halaman Ini)</span>
<span class="absensi-overview__value">{{ $hadirCount }}</span>
<span class="absensi-overview__meta">Pegawai berstatus hadir</span>
</div>
<div class="absensi-overview__item">
<span class="absensi-overview__label">Izin / Sakit</span>
<span class="absensi-overview__value">{{ $izinSakitCount }}</span>
<span class="absensi-overview__meta">Perlu perhatian lanjutan</span>
</div>
<div class="absensi-overview__item">
<span class="absensi-overview__label">Catatan Lengkap</span>
<span class="absensi-overview__value">{{ $completeAdminData }}</span>
<span class="absensi-overview__meta">Jobdesk & catatan terisi</span>
</div>
<div class="absensi-overview__item">
<span class="absensi-overview__label">Belum Tervalidasi</span>
<span class="absensi-overview__value">{{ $unverifiedCount }}</span>
<span class="absensi-overview__meta">Menunggu verifikasi admin</span>
</div>
</div>
<div class="absensi-filters">
<form action="{{ route('admin.absensi.index') }}" method="GET" class="absensi-filter-group">
<div class="absensi-filter-field">
<label for="filter-date">Tanggal</label>
<input type="date" id="filter-date" name="date" value="{{ $filterDate }}">
</div>
<div class="absensi-filter-actions">
<button type="submit">Terapkan</button>
<a href="{{ route('admin.absensi.index') }}" class="reset">Reset</a>
</div>
</form>
<div class="absensi-meta">
<span>Menampilkan {{ $items->firstItem() ?? 0 }}{{ $items->lastItem() ?? 0 }}</span>
<span class="absensi-meta__divider"></span>
<span>Total {{ number_format($totalRecords) }} data</span>
<span class="absensi-meta__divider"></span>
<span>Halaman {{ $items->currentPage() }} dari {{ $items->lastPage() }}</span>
</div>
</div>
<div class="absensi-table-container">
<div class="absensi-table-shell">
<div class="absensi-table-scroll">
<table class="absensi-table">
<thead>
<tr>
<th data-align="center">#</th>
<th>Nama</th>
<th>NIK</th>
<th>Keahlian</th>
<th>Tanggal</th>
<th>Masuk</th>
<th>Keluar</th>
<th>Status</th>
<th>Validasi</th>
<th>Catatan Admin</th>
<th data-align="center">Selfie</th>
<th>Jobdesk & Catatan</th>
<th data-align="center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($items as $i => $row)
@php
$clockIn = $row->clock_in?->timezone(config('app.timezone'));
$clockOut = $row->clock_out?->timezone(config('app.timezone'));
$selfiePath = null;
if ($row->selfie_photo) {
$selfiePath = route('selfie.show', ['filename' => basename($row->selfie_photo)]);
}
$status = $effectiveStatus($row);
$statusClass = match ($status) {
'hadir' => 'absensi-status hadir',
'sakit', 'izin' => 'absensi-status sakit',
default => 'absensi-status default',
};
$adminNote = $row->admin_note ? trim($row->admin_note) : null;
$hasJobdesk = filled($row->jobdesk);
$hasAdminNote = filled($adminNote);
$hasBoth = $hasJobdesk && $hasAdminNote;
$isVerified = (bool) $row->is_verified;
$displayDate = $clockIn?->format('d M Y') ?? ($row->clock_in?->format('d M Y') ?? '-');
@endphp
<tr>
<td data-label="#" data-align="center">{{ $items->firstItem() + $i }}</td>
<td data-label="Nama">{{ $row->user->name ?? '-' }}</td>
<td data-label="NIK">{{ $row->user->nik ?? '-' }}</td>
<td data-label="Keahlian">
@if(isset($row->user->skill))
@if($row->user->skill === 'mechanic')
<span style="display: inline-flex; align-items: center; gap: 6px; background: rgba(99, 102, 241, 0.1); color: #818cf8; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 500; border: 1px solid rgba(99, 102, 241, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<span>Mekanik</span>
</span>
@elseif($row->user->skill === 'welder')
<span style="display: inline-flex; align-items: center; gap: 6px; background: rgba(16, 185, 129, 0.1); color: #34d399; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 500; border: 1px solid rgba(16, 185, 129, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
</svg>
<span>Welder</span>
</span>
@else
<span style="display: inline-flex; align-items: center; gap: 6px; background: rgba(100, 116, 139, 0.1); color: #94a3b8; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 500; border: 1px solid rgba(100, 116, 139, 0.2);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
<span>Belum diatur</span>
</span>
@endif
@else
<span style="display: inline-flex; align-items: center; gap: 6px; background: rgba(100, 116, 139, 0.1); color: #94a3b8; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 500; border: 1px solid rgba(100, 116, 139, 0.2);">
<span>-</span>
</span>
@endif
</td>
<td data-label="Tanggal">{{ $displayDate }}</td>
<td data-label="Masuk">{{ $clockIn?->format('H:i') ?? '-' }}</td>
<td data-label="Keluar">{{ $clockOut?->format('H:i') ?? '-' }}</td>
<td data-label="Status">
<span class="{{ $statusClass }}">{{ strtoupper($status ?: '-') }}</span>
</td>
<td data-label="Validasi">
@php
// Asumsi $absensi->clock_in dan $absensi->clock_out adalah Carbon instance
$durasiJam = ($row->clock_in && $row->clock_out) ? $row->clock_in->diffInHours($row->clock_out) : 0;
@endphp
@if($durasiJam < 12)
<span style="background:#ef4444;color:#fff;padding:4px 14px;border-radius:8px;font-weight:600;">DITOLAK</span>
@else
<span style="background:#22c55e;color:#fff;padding:4px 14px;border-radius:8px;font-weight:600;">BENAR</span>
@endif
</td>
<td data-label="Catatan Admin">
<span class="absensi-note {{ $adminNote ? '' : 'empty' }}" title="{{ $adminNote ?? 'Tidak ada catatan admin' }}">
{{ $adminNote ?? 'Tidak ada catatan admin' }}
</span>
</td>
<td data-label="Selfie" data-align="center">
@php
$hasLocation = $row->clock_in_latitude && $row->clock_in_longitude;
@endphp
<div style="display: flex; gap: 12px; align-items: flex-start;">
@if ($selfiePath)
<a href="{{ $selfiePath }}" target="_blank" rel="noopener" class="absensi-selfie">
<span class="absensi-selfie-thumb">
<img src="{{ $selfiePath }}" alt="Selfie" onerror="this.remove(); this.parentElement.classList.add('is-empty');">
</span>
<span class="absensi-selfie-label">Lihat</span>
</a>
@else
<span class="absensi-selfie-empty">Tidak Ada Selfie</span>
@endif
@if($hasLocation)
<div style="flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 6px; font-size: 11px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<div style="padding: 6px; border-radius: 6px; background: rgba(15, 23, 42, 0.92); border: 1px solid rgba(96, 165, 250, 0.3);">
<span style="display: block; font-weight: 600; color: rgba(96, 165, 250, 0.9); font-size: 9px; text-transform: uppercase; margin-bottom: 2px;">Lat</span>
<span style="display: block; color: #dbeafe; word-break: break-all; font-size: 10px;">{{ number_format($row->clock_in_latitude, 5) }}</span>
</div>
<div style="padding: 6px; border-radius: 6px; background: rgba(15, 23, 42, 0.92); border: 1px solid rgba(96, 165, 250, 0.3);">
<span style="display: block; font-weight: 600; color: rgba(96, 165, 250, 0.9); font-size: 9px; text-transform: uppercase; margin-bottom: 2px;">Lon</span>
<span style="display: block; color: #dbeafe; word-break: break-all; font-size: 10px;">{{ number_format($row->clock_in_longitude, 5) }}</span>
</div>
<div style="padding: 6px; border-radius: 6px; background: rgba(15, 23, 42, 0.92); border: 1px solid rgba(34, 197, 94, 0.3);">
<span style="display: block; font-weight: 600; color: rgba(34, 197, 94, 0.9); font-size: 9px; text-transform: uppercase; margin-bottom: 2px;">Akurasi</span>
<span style="display: block; color: #bbf7d0; font-size: 10px;">{{ $row->clock_in_accuracy ? round($row->clock_in_accuracy) . 'm' : '-' }}</span>
</div>
<div style="padding: 6px; border-radius: 6px; background: rgba(15, 23, 42, 0.92); border: 1px solid rgba(148, 163, 184, 0.3);">
<span style="display: block; font-weight: 600; color: rgba(148, 163, 184, 0.75); font-size: 9px; text-transform: uppercase; margin-bottom: 2px;">Lokasi</span>
<span style="display: block; color: #cbd5e1; word-break: break-word; font-size: 9px;">{{ $row->clock_in_location_name ?? '-' }}</span>
</div>
</div>
<div class="admin-location-map" id="admin-map-{{ $row->id }}" data-lat="{{ $row->clock_in_latitude }}" data-lon="{{ $row->clock_in_longitude }}" style="width: 100%; height: 120px; border-radius: 6px; border: 1px solid rgba(96, 165, 250, 0.3); background: rgba(15, 23, 42, 0.92); overflow: hidden;"></div>
</div>
@endif
</div>
</td>
<td data-label="Jobdesk & Catatan">
<div style="display:flex; flex-direction:column; gap:12px;">
<div style="display:flex; justify-content:flex-end;">
<span class="absensi-jobdesk-status {{ $hasBoth ? 'on' : 'off' }}">
{{ $hasBoth ? 'Lengkap' : 'Belum Lengkap' }}
</span>
</div>
<form action="{{ route('admin.absensi.jobdesk', $row) }}" method="POST" class="absensi-admin-note-form">
@csrf
<div class="absensi-admin-note-form__row">
<div style="display:flex; flex-direction:column; gap:6px; min-width:0;">
@if ($status === 'hadir')
<select name="jobdesk">
<option value="" disabled @selected(! $row->jobdesk)>Pilih Jobdesk</option>
@foreach ($jobdeskOptions as $option)
@php
$count = $jobdeskCounts[$option] ?? 0;
$countText = $count > 0 ? " ($count orang)" : '';
@endphp
<option value="{{ $option }}" @selected($row->jobdesk === $option)>
{{ Str::upper($option) }}{{ $countText }}
</option>
@endforeach
</select>
@else
<input type="hidden" name="jobdesk" value="{{ $row->jobdesk }}">
<span class="absensi-admin-note-form__helper">Jobdesk tersedia saat status HADIR</span>
@endif
</div>
<textarea name="admin_note" placeholder="Catatan untuk pegawai...">{{ old('admin_note', $adminNote) }}</textarea>
</div>
<button type="submit">Simpan</button>
</form>
</div>
</td>
<td data-label="Aksi" data-align="center">
<form action="{{ route('admin.absensi.destroy', $row) }}" method="POST" onsubmit="return confirm('Hapus data absensi ini?');" class="absensi-delete">
@csrf
@method('DELETE')
<button type="submit">Hapus</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="11" class="absensi-empty">Belum ada data absensi.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="absensi-footer">
<span class="absensi-footer__info">Terakhir diperbarui {{ now()->timezone(config('app.timezone'))->format('d M Y H:i') }}</span>
<nav>
{{ $items->links() }}
</nav>
</div>
</div>
<script>
/* Initialize maps in admin table */
function initializeAdminMaps() {
if (typeof L === 'undefined') {
console.log('Leaflet not loaded yet, retrying...');
setTimeout(initializeAdminMaps, 500);
return;
}
const mapElements = document.querySelectorAll('[id^="admin-map-"]');
console.log('Found ' + mapElements.length + ' admin map elements to initialize');
mapElements.forEach(mapEl => {
const lat = parseFloat(mapEl.getAttribute('data-lat'));
const lon = parseFloat(mapEl.getAttribute('data-lon'));
if (isNaN(lat) || isNaN(lon)) {
console.warn('Invalid coordinates for map ' + mapEl.id);
return;
}
console.log('Initializing admin map ' + mapEl.id + ' with coordinates: ' + lat + ', ' + lon);
try {
// Clear any existing map instance
if (mapEl._leaflet_id !== undefined) {
mapEl._leaflet = null;
mapEl.innerHTML = '';
}
const map = L.map(mapEl, {
dragging: false,
scrollWheelZoom: false,
zoomControl: false,
touchZoom: false
}).setView([lat, lon], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OSM',
maxZoom: 19
}).addTo(map);
const redIcon = L.icon({
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
iconSize: [16, 26],
shadowSize: [26, 26],
iconAnchor: [8, 26]
});
L.marker([lat, lon], { icon: redIcon }).addTo(map);
// Invalidate and resize map
setTimeout(() => map.invalidateSize(), 100);
} catch (e) {
console.error('Error initializing admin map ' + mapEl.id + ':', e);
}
});
}
// Wait for Leaflet to load, then initialize maps
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initializeAdminMaps, 500);
});
} else {
setTimeout(initializeAdminMaps, 500);
}
</script>
@endsection