663 lines
28 KiB
PHP
663 lines
28 KiB
PHP
@extends('dashboard.base')
|
|
|
|
@section('title', 'Laporan')
|
|
|
|
@push('css')
|
|
<style>
|
|
.nav-tabs .nav-link {
|
|
color: #6c7293;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
background: transparent;
|
|
padding: 0.75rem 1.5rem;
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
color: #7da0fa;
|
|
border-bottom-color: #7da0fa;
|
|
background: transparent;
|
|
}
|
|
|
|
.nav-tabs .nav-link:hover {
|
|
border-bottom-color: #7da0fa;
|
|
color: #7da0fa;
|
|
}
|
|
|
|
.filter-card {
|
|
border: 1px solid #ebedf2;
|
|
border-radius: 0.375rem;
|
|
background: #fff;
|
|
}
|
|
|
|
.summary-card {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.summary-card-alt {
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
border-radius: 0.375rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.badge-status {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.btn-export {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border: none;
|
|
color: white;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.btn-export:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
color: white;
|
|
}
|
|
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
display: none;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.spinner-border-custom {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
border-width: 0.3em;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
}
|
|
</style>
|
|
|
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
|
@endpush
|
|
|
|
@section('content')
|
|
<div class="content-wrapper">
|
|
<div class="row">
|
|
<div class="col-md-12 grid-margin">
|
|
<div class="row">
|
|
<div class="col-12 col-xl-8 mb-4 mb-xl-0">
|
|
<h3 class="font-weight-bold">Laporan</h3>
|
|
<h6 class="font-weight-normal mb-0">Kelola dan unduh laporan absensi dan izin karyawan</h6>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="loading-overlay" id="loadingOverlay">
|
|
<div class="spinner-border spinner-border-custom text-primary" role="status">
|
|
<span class="sr-only">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12 grid-margin stretch-card">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<ul class="nav nav-tabs" id="reportTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link active" id="attendance-tab" data-toggle="tab" href="#attendance" role="tab" aria-controls="attendance" aria-selected="true">
|
|
<span class="material-icons align-middle">calendar_today</span>
|
|
Laporan Absensi
|
|
</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" id="permission-tab" data-toggle="tab" href="#permission" role="tab" aria-controls="permission" aria-selected="false">
|
|
<span class="material-icons align-middle">assignment_turned_in</span>
|
|
Laporan Izin
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content mt-4" id="reportTabsContent">
|
|
<div class="tab-pane fade show active" id="attendance" role="tabpanel" aria-labelledby="attendance-tab">
|
|
<div class="row">
|
|
<div class="col-md-12 mb-4">
|
|
<div class="filter-card p-4">
|
|
<h5 class="mb-3">Filter Laporan Absensi</h5>
|
|
<form id="attendanceFilterForm">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="attendance_start_date">Tanggal Mulai</label>
|
|
<input type="date" class="form-control" id="attendance_start_date" name="start_date" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="attendance_end_date">Tanggal Akhir</label>
|
|
<input type="date" class="form-control" id="attendance_end_date" name="end_date" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="attendance_user_id">Karyawan</label>
|
|
<select class="form-control" id="attendance_user_id" name="user_id">
|
|
<option value="">Semua Karyawan</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="attendance_status">Status</label>
|
|
<select class="form-control" id="attendance_status" name="status">
|
|
<option value="">Semua Status</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="accepted">Diterima</option>
|
|
<option value="rejected">Ditolak</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<button type="submit" class="btn btn-export">
|
|
<span class="material-icons align-middle">search</span> Filter
|
|
</button>
|
|
<button type="button" class="btn btn-export" id="exportAttendanceExcel" title="Export Excel">
|
|
<span class="material-icons align-middle">file_download</span> Export Excel
|
|
</button>
|
|
{{-- <button type="button" class="btn btn-export" id="exportAttendancePdf" title="Export PDF">
|
|
<span class="material-icons align-middle">print</span> Export PDF
|
|
</button> --}}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12 mb-4" id="attendanceSummarySection" style="display: none;">
|
|
<h5 class="mb-3">Ringkasan Absensi</h5>
|
|
<div class="row" id="attendanceSummaryCards">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12" id="attendanceDetailsSection" style="display: none;">
|
|
<h5 class="mb-3">Detail Absensi</h5>
|
|
<div class="table-responsive" style="overflow-x: scroll;">
|
|
<table class="table table-striped table-bordered" id="attendanceTable" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>No</th>
|
|
<th>Tanggal</th>
|
|
<th>Nama</th>
|
|
<th>Waktu</th>
|
|
<th>Tipe</th>
|
|
<th>Status</th>
|
|
<th>Lokasi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="permission" role="tabpanel" aria-labelledby="permission-tab">
|
|
<div class="row">
|
|
<div class="col-md-12 mb-4">
|
|
<div class="filter-card p-4">
|
|
<h5 class="mb-3">Filter Laporan Izin</h5>
|
|
<form id="permissionFilterForm">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="permission_start_date">Tanggal Mulai</label>
|
|
<input type="date" class="form-control" id="permission_start_date" name="start_date" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label for="permission_end_date">Tanggal Akhir</label>
|
|
<input type="date" class="form-control" id="permission_end_date" name="end_date" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label for="permission_user_id">Karyawan</label>
|
|
<select class="form-control" id="permission_user_id" name="user_id">
|
|
<option value="">Semua Karyawan</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label for="permission_status">Status</label>
|
|
<select class="form-control" id="permission_status" name="status">
|
|
<option value="">Semua Status</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="accepted">Diterima</option>
|
|
<option value="rejected">Ditolak</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="form-group">
|
|
<label for="permission_category">Kategori</label>
|
|
<select class="form-control" id="permission_category" name="category">
|
|
<option value="">Semua Kategori</option>
|
|
<option value="sakit">Sakit</option>
|
|
<option value="cuti">Cuti</option>
|
|
<option value="izin">Izin</option>
|
|
<option value="lainnya">Lainnya</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-md-12">
|
|
<button type="submit" class="btn btn-export">
|
|
<span class="material-icons align-middle">search</span> Filter
|
|
</button>
|
|
<button type="button" class="btn btn-export" id="exportPermissionExcel" title="Export Excel">
|
|
<span class="material-icons align-middle">file_download</span> Export Excel
|
|
</button>
|
|
{{-- <button type="button" class="btn btn-export" id="exportPermissionPdf" title="Export PDF">
|
|
<span class="material-icons align-middle">print</span> Export PDF
|
|
</button> --}}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12 mb-4" id="permissionSummarySection" style="display: none;">
|
|
<h5 class="mb-3">Ringkasan Izin per Kategori</h5>
|
|
<div class="row" id="permissionSummaryCards">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-12" id="permissionDetailsSection" style="display: none;">
|
|
<h5 class="mb-3">Detail Izin</h5>
|
|
<div class="table-responsive" style="overflow-x: scroll;">
|
|
<table class="table table-striped table-bordered" id="permissionTable" width="100%" cellspacing="0">
|
|
<thead>
|
|
<tr>
|
|
<th>No</th>
|
|
<th>Tanggal Pengajuan</th>
|
|
<th>Nama</th>
|
|
<th>Kategori</th>
|
|
<th>Tanggal Mulai</th>
|
|
<th>Tanggal Akhir</th>
|
|
<th>Durasi</th>
|
|
<th>Status</th>
|
|
<th>Approver</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('script')
|
|
<script>
|
|
$(document).ready(function() {
|
|
const today = new Date();
|
|
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
|
|
|
|
$('#attendance_start_date, #permission_start_date').val(formatDate(lastMonth));
|
|
$('#attendance_end_date, #permission_end_date').val(formatDate(today));
|
|
|
|
loadUsers();
|
|
|
|
initDataTables();
|
|
|
|
$('#attendanceFilterForm').on('submit', handleAttendanceFilter);
|
|
$('#permissionFilterForm').on('submit', handlePermissionFilter);
|
|
|
|
$('#exportAttendanceExcel').on('click', () => exportReport('attendance', 'excel'));
|
|
$('#exportAttendancePdf').on('click', () => exportReport('attendance', 'pdf'));
|
|
$('#exportPermissionExcel').on('click', () => exportReport('permission', 'excel'));
|
|
$('#exportPermissionPdf').on('click', () => exportReport('permission', 'pdf'));
|
|
});
|
|
|
|
function formatDate(date) {
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
function showLoading() {
|
|
$('#loadingOverlay').show();
|
|
}
|
|
|
|
function hideLoading() {
|
|
$('#loadingOverlay').hide();
|
|
}
|
|
|
|
function loadUsers() {
|
|
$.ajax({
|
|
url: '/api/admin/users',
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
const employees = response.data.filter(user => user.role === 'karyawan');
|
|
const options = employees.map(user =>
|
|
`<option value="${user.id}">${user.profile?.name || user.email}</option>`
|
|
).join('');
|
|
|
|
$('#attendance_user_id, #permission_user_id').append(options);
|
|
},
|
|
error: function(xhr) {
|
|
console.error('Failed to load users:', xhr);
|
|
}
|
|
});
|
|
}
|
|
|
|
function initDataTables() {
|
|
if ($.fn.dataTable.isDataTable('#attendanceTable')) {
|
|
$('#attendanceTable').DataTable().clear().destroy();
|
|
}
|
|
if ($.fn.dataTable.isDataTable('#permissionTable')) {
|
|
$('#permissionTable').DataTable().clear().destroy();
|
|
}
|
|
|
|
$('#attendanceTable').DataTable({
|
|
searching: false,
|
|
paging: true,
|
|
info: true,
|
|
lengthChange: true,
|
|
pageLength: 25,
|
|
});
|
|
|
|
$('#permissionTable').DataTable({
|
|
searching: false,
|
|
paging: true,
|
|
info: true,
|
|
lengthChange: true,
|
|
pageLength: 25,
|
|
});
|
|
}
|
|
|
|
function handleAttendanceFilter(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const params = new URLSearchParams(formData);
|
|
|
|
showLoading();
|
|
|
|
$.ajax({
|
|
url: '/api/admin/reports/attendance?' + params.toString(),
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
displayAttendanceReport(response.data);
|
|
hideLoading();
|
|
},
|
|
error: function(xhr) {
|
|
hideLoading();
|
|
Swal.fire('Error', xhr.responseJSON?.message || 'Gagal memuat laporan', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function handlePermissionFilter(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = new FormData(e.target);
|
|
const params = new URLSearchParams(formData);
|
|
|
|
showLoading();
|
|
|
|
$.ajax({
|
|
url: '/api/admin/reports/permission?' + params.toString(),
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
displayPermissionReport(response.data);
|
|
hideLoading();
|
|
},
|
|
error: function(xhr) {
|
|
hideLoading();
|
|
Swal.fire('Error', xhr.responseJSON?.message || 'Gagal memuat laporan', 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function displayAttendanceReport(data) {
|
|
displayAttendanceSummary(data.summary);
|
|
|
|
displayAttendanceDetails(data.details);
|
|
|
|
$('#attendanceSummarySection, #attendanceDetailsSection').show();
|
|
}
|
|
|
|
function displayAttendanceSummary(summary) {
|
|
let html = '';
|
|
|
|
summary.forEach((item, index) => {
|
|
const cardClass = index % 2 === 0 ? 'summary-card' : 'summary-card-alt';
|
|
html += `
|
|
<div class="col-md-4 mb-3">
|
|
<div class="card ${cardClass}">
|
|
<div class="card-body">
|
|
<h6 class="mb-2">${item.user.profile?.name || item.user.email}</h6>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<h4 class="mb-0">${item.total_days}</h4>
|
|
<small>Total Hari</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<h4 class="mb-0">${item.present_days}</h4>
|
|
<small>Hadir</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#attendanceSummaryCards').html(html);
|
|
}
|
|
|
|
function displayAttendanceDetails(details) {
|
|
const table = $('#attendanceTable').DataTable();
|
|
table.clear();
|
|
|
|
details.forEach((item, index) => {
|
|
const statusBadge = getStatusBadge(item.status);
|
|
const typeBadge = item.type === 'in' ? '<span class="badge badge-success">Masuk</span>' : '<span class="badge badge-warning">Keluar</span>';
|
|
|
|
table.row.add([
|
|
index + 1,
|
|
formatDateIndonesia(item.date),
|
|
item.user.profile?.name || item.user.email,
|
|
item.time,
|
|
typeBadge,
|
|
statusBadge,
|
|
item.latitude && item.longitude ? `${item.latitude}, ${item.longitude}` : '-'
|
|
]);
|
|
});
|
|
|
|
table.draw();
|
|
}
|
|
|
|
function displayPermissionReport(data) {
|
|
displayPermissionSummary(data.summary);
|
|
displayPermissionDetails(data.details);
|
|
$('#permissionSummarySection, #permissionDetailsSection').show();
|
|
}
|
|
|
|
function displayPermissionSummary(summary) {
|
|
let html = '';
|
|
|
|
summary.forEach((item, index) => {
|
|
const cardClass = index % 2 === 0 ? 'summary-card' : 'summary-card-alt';
|
|
html += `
|
|
<div class="col-md-3 mb-3">
|
|
<div class="card ${cardClass} h-100 shadow-sm">
|
|
<div class="card-body">
|
|
<h6 class="mb-3 text-capitalize text-wrap">${item.category}</h6>
|
|
<div class="row text-center mb-3">
|
|
<div class="col-6">
|
|
<h5 class="mb-0">${item.total}</h5>
|
|
<small>Total</small>
|
|
</div>
|
|
<div class="col-6">
|
|
<h5 class="mb-0">${item.total_days}</h5>
|
|
<small>Hari</small>
|
|
</div>
|
|
</div>
|
|
<ul class="list-unstyled text-center mb-0 small">
|
|
<li><span class="fw-bold">${item.approved}</span> Approved</li>
|
|
<li><span class="fw-bold">${item.pending}</span> Pending</li>
|
|
<li><span class="fw-bold">${item.rejected}</span> Rejected</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#permissionSummaryCards').html(html);
|
|
}
|
|
|
|
function displayPermissionDetails(details) {
|
|
const table = $('#permissionTable').DataTable();
|
|
table.clear();
|
|
|
|
details.forEach((item, index) => {
|
|
const statusBadge = getStatusBadge(item.status);
|
|
|
|
table.row.add([
|
|
index + 1,
|
|
formatDateIndonesia(item.created_at),
|
|
item.user.profile?.name || item.user.email,
|
|
item.category.charAt(0).toUpperCase() + item.category.slice(1),
|
|
formatDateIndonesia(item.start_date),
|
|
formatDateIndonesia(item.end_date),
|
|
item.duration + ' hari',
|
|
statusBadge,
|
|
item.approver?.profile?.name || '-'
|
|
]);
|
|
});
|
|
|
|
table.draw();
|
|
}
|
|
|
|
function getStatusBadge(status) {
|
|
const badges = {
|
|
'pending': '<span class="badge badge-warning">Pending</span>',
|
|
'accepted': '<span class="badge badge-success">Diterima</span>',
|
|
'rejected': '<span class="badge badge-danger">Ditolak</span>'
|
|
};
|
|
return badges[status] || '<span class="badge badge-secondary">Unknown</span>';
|
|
}
|
|
|
|
function formatDateIndonesia(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('id-ID', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
}
|
|
|
|
function exportReport(type, format) {
|
|
const formId = type === 'attendance' ? '#attendanceFilterForm' : '#permissionFilterForm';
|
|
const formData = new FormData(document.querySelector(formId));
|
|
const params = new URLSearchParams(formData);
|
|
|
|
const startDate = formData.get('start_date');
|
|
const endDate = formData.get('end_date');
|
|
|
|
if (!startDate || !endDate) {
|
|
Swal.fire('Error', 'Tanggal mulai dan akhir harus diisi', 'error');
|
|
return;
|
|
}
|
|
|
|
showLoading();
|
|
|
|
let endpoint = '';
|
|
|
|
if (type === 'attendance') {
|
|
endpoint = `/api/admin/reports/attendance/export/${format}?${params.toString()}`;
|
|
} else if (type === 'permission') {
|
|
endpoint = `/api/admin/reports/permission/export/${format}?${params.toString()}`;
|
|
}
|
|
|
|
fetch(endpoint, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
.then(async response => {
|
|
hideLoading();
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => null);
|
|
throw new Error(errorData?.message || 'Gagal mengunduh file');
|
|
}
|
|
const blob = await response.blob();
|
|
const disposition = response.headers.get('content-disposition');
|
|
let filename = `laporan_${type}_${startDate}_${endDate}.${format === 'excel' ? 'xlsx' : 'pdf'}`;
|
|
if (disposition && disposition.indexOf('filename=') !== -1) {
|
|
filename = disposition.split('filename=')[1].replace(/"/g, '');
|
|
}
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
Swal.fire('Berhasil', 'File berhasil diunduh', 'success');
|
|
})
|
|
.catch(error => {
|
|
hideLoading();
|
|
Swal.fire('Error', error.message, 'error');
|
|
});
|
|
}
|
|
</script>
|
|
@endpush
|
|
|