418 lines
22 KiB
PHP
418 lines
22 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Manajemen Izin & Cuti')
|
|
|
|
@section('content')
|
|
<div class="space-y-6">
|
|
|
|
<x-page-header title="Manajemen Pengajuan Izin" subtitle="Kelola izin sakit, cuti, dan keperluan lainnya."
|
|
class="lg:items-end">
|
|
<div class="flex gap-3">
|
|
<x-button type="button" x-data @click="$dispatch('open-modal', 'create-izin')" class="h-10">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Buat Izin
|
|
</x-button>
|
|
|
|
<form action="{{ route('izin.index') }}" method="GET" class="flex gap-2 w-full md:w-auto">
|
|
<input type="hidden" name="status" value="{{ $statusId }}">
|
|
<x-input type="text" name="search" value="{{ request('search') }}" placeholder="Cari nama pegawai..."
|
|
class="!mb-0 w-full md:w-64" />
|
|
<x-button type="submit" variant="secondary" class="h-10 px-3">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
</x-button>
|
|
</form>
|
|
|
|
<x-filter-select name="status"
|
|
onchange="window.location.href='{{ route('izin.index') }}?status=' + this.value + '&search={{ request('search') }}'">
|
|
<option value="">Semua Status</option>
|
|
<option value="{{ \App\Enums\StatusPengajuan::PENDING }}" {{ $statusId == \App\Enums\StatusPengajuan::PENDING ? 'selected' : '' }}>Pending</option>
|
|
<option value="{{ \App\Enums\StatusPengajuan::DISETUJUI }}" {{ $statusId == \App\Enums\StatusPengajuan::DISETUJUI ? 'selected' : '' }}>Disetujui</option>
|
|
<option value="{{ \App\Enums\StatusPengajuan::DITOLAK }}" {{ $statusId == \App\Enums\StatusPengajuan::DITOLAK ? 'selected' : '' }}>Ditolak</option>
|
|
</x-filter-select>
|
|
</div>
|
|
</x-page-header>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
|
<x-table>
|
|
<x-slot:header>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase text-left">Pegawai</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase text-left">Jenis Izin</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase text-left">Tanggal</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase text-center">Status</th>
|
|
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase text-right">Aksi</th>
|
|
</x-slot:header>
|
|
|
|
@forelse($izin as $item)
|
|
<tr class="hover:bg-slate-50 border-b border-slate-50 last:border-b-0">
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center gap-3">
|
|
<div
|
|
class="w-10 h-10 rounded-full bg-slate-100 flex items-center justify-center text-slate-500 font-bold text-xs border border-slate-200">
|
|
{{ substr($item->user->nama_lengkap ?? 'U', 0, 2) }}
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-slate-800 text-sm">{{ $item->user->nama_lengkap ?? '-' }}</div>
|
|
<div class="text-xs text-slate-500">{{ $item->user->nik ?? '-' }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
|
|
<span
|
|
class="px-2.5 py-1 rounded-full text-xs font-medium
|
|
{{ $item->id_jenis_izin == \App\Enums\JenisIzin::SAKIT ? 'bg-rose-50 text-rose-600' : 'bg-blue-50 text-blue-600' }}">
|
|
{{ $item->jenisIzin->nama_izin ?? 'Unknown' }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate-600">
|
|
<div class="flex flex-col">
|
|
<span>{{ \Carbon\Carbon::parse($item->tanggal_mulai)->format('d M Y') }}</span>
|
|
<span class="text-xs text-slate-400">s/d</span>
|
|
<span>{{ \Carbon\Carbon::parse($item->tanggal_selesai)->format('d M Y') }}</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">
|
|
@php
|
|
$statusName = $item->statusPengajuan->nama_status ?? '-';
|
|
$badgeColor = match ($item->id_status) {
|
|
\App\Enums\StatusPengajuan::DISETUJUI => 'green',
|
|
\App\Enums\StatusPengajuan::DITOLAK => 'red',
|
|
default => 'yellow'
|
|
};
|
|
@endphp
|
|
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->id_status == \App\Enums\StatusPengajuan::PENDING)
|
|
<x-badge color="blue">Via Surat Izin</x-badge>
|
|
@else
|
|
<x-badge color="{{ $badgeColor }}">{{ $statusName }}</x-badge>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 text-right">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button data-item="{{ json_encode([
|
|
'id_izin' => $item->id_izin,
|
|
'id_status' => $item->id_status,
|
|
'id_jenis_izin' => $item->id_jenis_izin,
|
|
'tanggal_mulai' => $item->tanggal_mulai,
|
|
'tanggal_selesai' => $item->tanggal_selesai,
|
|
'alasan' => $item->alasan,
|
|
'alasan_penolakan' => $item->alasan_penolakan,
|
|
'bukti_file' => $item->bukti_file,
|
|
'user' => ['nama_lengkap' => $item->user?->nama_lengkap, 'nik' => $item->user?->nik],
|
|
'jenis_izin' => ['nama_izin' => $item->jenisIzin?->nama_izin],
|
|
]) }}"
|
|
onclick="openDetailModal(JSON.parse(this.getAttribute('data-item')))"
|
|
class="px-3 py-1.5 text-xs font-medium text-slate-600 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition shadow-sm">
|
|
Detail
|
|
</button>
|
|
@if($item->id_jenis_izin == \App\Enums\JenisIzin::CUTI && $item->suratIzin)
|
|
<a href="{{ route('surat-izin.show', $item->suratIzin->id_surat) }}"
|
|
class="px-3 py-1.5 text-xs font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition shadow-sm">
|
|
Lihat Surat →
|
|
</a>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<x-empty-state colspan="5" message="Belum ada data pengajuan izin." />
|
|
@endforelse
|
|
</x-table>
|
|
</div>
|
|
|
|
<x-pagination :paginator="$izin->appends(request()->query())" />
|
|
</div>
|
|
|
|
<x-modal name="create-izin" title="Buat Pengajuan Izin">
|
|
<form action="{{ route('izin.store') }}" method="POST" enctype="multipart/form-data" class="space-y-4">
|
|
@csrf
|
|
|
|
<div class="flex flex-col md:flex-row gap-4">
|
|
<div class="flex-1"><x-select id="filter-kantor-izin" label="Filter Kantor">
|
|
<option value="">Semua Kantor</option>
|
|
@foreach($kantor as $k)
|
|
<option value="{{ $k->id_kantor }}">{{ $k->nama_kantor }}</option>
|
|
@endforeach
|
|
</x-select></div>
|
|
|
|
<div class="flex-1"><x-select id="filter-divisi-izin" label="Filter Divisi">
|
|
<option value="">Semua Divisi</option>
|
|
@foreach($divisi as $d)
|
|
<option value="{{ $d->id_divisi }}">{{ $d->nama_divisi }}</option>
|
|
@endforeach
|
|
</x-select></div>
|
|
</div>
|
|
|
|
<div>
|
|
<x-select id="id_user_izin" label="Pilih Pegawai" name="id_user" required>
|
|
<option value="">-- Pilih Pegawai --</option>
|
|
@foreach($users as $user)
|
|
<option value="{{ $user->id }}" data-kantor="{{ $user->id_kantor }}" data-divisi="{{ $user->id_divisi }}" class="user-option">
|
|
{{ $user->nama_lengkap }} ({{ $user->nik }}) - {{ $user->kantor->nama_kantor ?? 'Kantor ?' }} / {{ $user->divisi->nama_divisi ?? 'Divisi ?' }}
|
|
</option>
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
|
|
<div>
|
|
<x-select label="Jenis Izin" name="id_jenis_izin" required>
|
|
@foreach($jenisIzinList as $j)
|
|
<option value="{{ $j->id_jenis_izin }}">{{ $j->nama_izin }}</option>
|
|
@endforeach
|
|
</x-select>
|
|
</div>
|
|
|
|
<div class="flex flex-col md:flex-row gap-4">
|
|
<div class="flex-1"><x-input type="date" label="Tanggal Mulai" name="tanggal_mulai" required /></div>
|
|
<div class="flex-1"><x-input type="date" label="Tanggal Selesai" name="tanggal_selesai" required /></div>
|
|
</div>
|
|
|
|
<div>
|
|
<x-textarea label="Alasan" name="alasan" required rows="3" />
|
|
</div>
|
|
|
|
<x-input type="file" label="Bukti Dokumen (Gambar/PDF)" name="bukti_file"
|
|
accept=".jpeg,.png,.jpg,.pdf" />
|
|
|
|
<div class="pt-4 flex justify-end gap-3">
|
|
<x-button type="button" variant="secondary" x-data
|
|
@click="$dispatch('close-modal', 'create-izin')">Batal</x-button>
|
|
<x-button type="submit" id="submitBtn">
|
|
<span id="submitSpinner" class="hidden animate-spin mr-2">
|
|
<svg class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4">
|
|
</circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
|
</svg>
|
|
</span>
|
|
<span id="submitText">Simpan Pengajuan</span>
|
|
</x-button>
|
|
</div>
|
|
</form>
|
|
</x-modal>
|
|
|
|
<x-modal name="detail-izin" title="Detail Pengajuan Izin">
|
|
<div class="space-y-4">
|
|
|
|
<div class="p-4 bg-slate-50 rounded-xl border border-slate-200 flex justify-between items-start">
|
|
<div>
|
|
<h4 class="font-bold text-slate-800" id="detail-nama">Nama Pegawai</h4>
|
|
<p class="text-xs text-slate-500" id="detail-tgl">Tanggal...</p>
|
|
</div>
|
|
<span class="px-2.5 py-1 rounded-full text-xs font-medium bg-slate-200 text-slate-700"
|
|
id="detail-jenis">Jenis</span>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Alasan</label>
|
|
<p class="text-sm text-slate-700 bg-slate-50 p-3 rounded-lg border border-slate-100" id="detail-alasan">...
|
|
</p>
|
|
</div>
|
|
|
|
<div id="alasan-penolakan-wrapper" class="hidden">
|
|
<label class="block text-xs font-bold text-red-500 uppercase tracking-wider mb-1">Alasan Penolakan</label>
|
|
<p class="text-sm text-red-700 bg-red-50 p-3 rounded-lg border border-red-100" id="detail-alasan-penolakan">-</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Bukti Dokumen</label>
|
|
<div
|
|
class="bg-slate-100 rounded-lg overflow-hidden border border-slate-200 flex items-center justify-center min-h-[150px] relative">
|
|
<img id="detail-bukti" src="" class="max-h-[300px] object-contain hidden">
|
|
<a id="link-download" href="#" target="_blank"
|
|
class="text-sm text-blue-600 hover:underline hidden">Download File</a>
|
|
<span id="no-bukti" class="text-xs text-slate-400">Tidak ada lampiran</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 pt-4 border-t border-slate-100 mt-4">
|
|
|
|
<div id="cuti-info-banner" class="hidden p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700 flex items-start gap-2">
|
|
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<div>
|
|
<p class="font-bold">Pengajuan Cuti diproses via Surat Izin</p>
|
|
<p class="text-xs mt-0.5">Approval cuti dilakukan melalui alur Surat Izin (Manajer → HRD).</p>
|
|
<a href="{{ route('surat-izin.index') }}" class="inline-flex items-center gap-1 text-xs font-semibold text-blue-700 hover:text-blue-900 mt-1.5 underline underline-offset-2">
|
|
Buka Menu Surat Izin →
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-3" id="action-buttons">
|
|
<x-button type="button" variant="secondary" x-data
|
|
@click="$dispatch('close-modal', 'detail-izin')">Tutup</x-button>
|
|
|
|
@auth
|
|
<form id="form-reject-izin" action="" method="POST" class="hidden">
|
|
@csrf
|
|
<input type="hidden" name="alasan_penolakan" id="input-alasan-penolakan">
|
|
</form>
|
|
<form id="form-approve-izin" action="" method="POST" class="hidden">@csrf</form>
|
|
|
|
<x-button id="btn-reject" variant="danger" onclick="submitRejectWithAlasan()">
|
|
Tolak
|
|
</x-button>
|
|
<x-button id="btn-approve" variant="primary"
|
|
onclick="confirmAction(event, 'form-approve-izin', 'Sistem akan update presensi otomatis!', '#10b981', 'Ya, Setujui')">
|
|
Setujui
|
|
</x-button>
|
|
@endauth
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-modal>
|
|
|
|
<input type="hidden" id="current-id-izin">
|
|
|
|
@endsection
|
|
|
|
@section('script')
|
|
<script>
|
|
const JENIS_CUTI = {{ \App\Enums\JenisIzin::CUTI }};
|
|
const STATUS_PENDING = {{ \App\Enums\StatusPengajuan::PENDING }};
|
|
const STATUS_DITOLAK = {{ \App\Enums\StatusPengajuan::DITOLAK }};
|
|
|
|
function openDetailModal(data) {
|
|
document.getElementById('current-id-izin').value = data.id_izin;
|
|
document.getElementById('detail-nama').innerText = data.user.nama_lengkap;
|
|
document.getElementById('detail-tgl').innerText = 'Mulai: ' + data.tanggal_mulai + ' | Selesai: ' + data.tanggal_selesai;
|
|
document.getElementById('detail-jenis').innerText = data.jenis_izin ? data.jenis_izin.nama_izin : 'Unknown';
|
|
document.getElementById('detail-alasan').innerText = data.alasan;
|
|
|
|
const formApprove = document.getElementById('form-approve-izin');
|
|
const formReject = document.getElementById('form-reject-izin');
|
|
if (formApprove && formReject) {
|
|
formApprove.action = `{{ url('izin') }}/${data.id_izin}/approve`;
|
|
formReject.action = `{{ url('izin') }}/${data.id_izin}/reject`;
|
|
}
|
|
|
|
const img = document.getElementById('detail-bukti');
|
|
const link = document.getElementById('link-download');
|
|
const noBukti = document.getElementById('no-bukti');
|
|
|
|
img.classList.add('hidden');
|
|
link.classList.add('hidden');
|
|
noBukti.classList.add('hidden');
|
|
|
|
if (data.bukti_file) {
|
|
const fileUrl = "{{ asset('storage') }}/" + data.bukti_file;
|
|
const isImage = data.bukti_file.match(/\.(jpeg|jpg|png|gif)$/i) || data.bukti_file.includes('placehold.co');
|
|
|
|
if (isImage) {
|
|
img.src = data.bukti_file.startsWith('http') ? data.bukti_file : fileUrl;
|
|
img.classList.remove('hidden');
|
|
} else {
|
|
link.href = fileUrl;
|
|
link.classList.remove('hidden');
|
|
}
|
|
} else {
|
|
noBukti.classList.remove('hidden');
|
|
}
|
|
|
|
const btnApprove = document.getElementById('btn-approve');
|
|
const btnReject = document.getElementById('btn-reject');
|
|
const cutiInfo = document.getElementById('cuti-info-banner');
|
|
const alasanPenolakanWrapper = document.getElementById('alasan-penolakan-wrapper');
|
|
|
|
if (btnApprove) btnApprove.classList.add('hidden');
|
|
if (btnReject) btnReject.classList.add('hidden');
|
|
if (cutiInfo) cutiInfo.classList.add('hidden');
|
|
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.add('hidden');
|
|
|
|
if (data.id_status == STATUS_DITOLAK && data.alasan_penolakan) {
|
|
document.getElementById('detail-alasan-penolakan').innerText = data.alasan_penolakan;
|
|
if (alasanPenolakanWrapper) alasanPenolakanWrapper.classList.remove('hidden');
|
|
}
|
|
|
|
if (data.id_status == STATUS_PENDING) {
|
|
if (data.id_jenis_izin == JENIS_CUTI) {
|
|
if (cutiInfo) cutiInfo.classList.remove('hidden');
|
|
} else {
|
|
if (btnApprove) btnApprove.classList.remove('hidden');
|
|
if (btnReject) btnReject.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
window.dispatchEvent(new CustomEvent('open-modal', { detail: 'detail-izin' }));
|
|
}
|
|
|
|
function submitRejectWithAlasan() {
|
|
const alasan = prompt('Masukkan alasan penolakan (opsional):');
|
|
if (alasan === null) return;
|
|
const inputAlasan = document.getElementById('input-alasan-penolakan');
|
|
if (inputAlasan) inputAlasan.value = alasan;
|
|
const form = document.getElementById('form-reject-izin');
|
|
if (form) form.submit();
|
|
}
|
|
|
|
function initCreateIzinForm() {
|
|
const createForm = document.querySelector('form[action="{{ route('izin.store') }}"]');
|
|
if (createForm) {
|
|
createForm.addEventListener('submit', function () {
|
|
const btn = document.getElementById('submitBtn');
|
|
const spinner = document.getElementById('submitSpinner');
|
|
const text = document.getElementById('submitText');
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.classList.add('opacity-75', 'cursor-not-allowed');
|
|
}
|
|
if (spinner) spinner.classList.remove('hidden');
|
|
if (text) text.textContent = 'Menyimpan...';
|
|
});
|
|
}
|
|
|
|
const filterKantor = document.getElementById('filter-kantor-izin');
|
|
const filterDivisi = document.getElementById('filter-divisi-izin');
|
|
const userSelect = document.getElementById('id_user_izin');
|
|
|
|
function filterUsers() {
|
|
if (!filterKantor || !filterDivisi || !userSelect) return;
|
|
|
|
const kantorId = filterKantor.value;
|
|
const divisiId = filterDivisi.value;
|
|
|
|
userSelect.value = "";
|
|
|
|
const options = userSelect.querySelectorAll('.user-option');
|
|
options.forEach(opt => {
|
|
const optKantor = opt.getAttribute('data-kantor');
|
|
const optDivisi = opt.getAttribute('data-divisi');
|
|
|
|
let show = true;
|
|
if (kantorId && optKantor !== kantorId) show = false;
|
|
if (divisiId && optDivisi !== divisiId) show = false;
|
|
|
|
opt.hidden = !show;
|
|
opt.disabled = !show;
|
|
});
|
|
}
|
|
|
|
if (filterKantor && filterDivisi) {
|
|
filterKantor.removeEventListener('change', filterUsers);
|
|
filterDivisi.removeEventListener('change', filterUsers);
|
|
|
|
filterKantor.addEventListener('change', filterUsers);
|
|
filterDivisi.addEventListener('change', filterUsers);
|
|
}
|
|
}
|
|
|
|
if (typeof window.__izinFilterAttached === 'undefined') {
|
|
window.__izinFilterAttached = true;
|
|
document.addEventListener('turbo:load', initCreateIzinForm);
|
|
}
|
|
|
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
setTimeout(initCreateIzinForm, 50);
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', initCreateIzinForm);
|
|
}
|
|
</script>
|
|
@endsection
|