635 lines
24 KiB
PHP
635 lines
24 KiB
PHP
@extends('dashboard.base')
|
|
|
|
@section('title', 'Manajemen Absensi')
|
|
|
|
@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">Manajemen Absensi</h3>
|
|
<h6 class="font-weight-normal mb-0">Kelola data absensi karyawan</h6>
|
|
</div>
|
|
<div class="col-12 col-xl-4">
|
|
<button type="button" class="btn btn-info btn-sm float-right" onclick="showStatistics()">
|
|
<i class="mdi mdi-chart-line"></i> Statistik
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" id="statisticsCards" style="display: none;">
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-tale">
|
|
<div class="card-body">
|
|
<p class="mb-4">Total Hari Ini</p>
|
|
<p class="fs-30 mb-2" id="todayTotal">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-dark-blue">
|
|
<div class="card-body">
|
|
<p class="mb-4">Hadir Hari Ini</p>
|
|
<p class="fs-30 mb-2" id="todayPresent">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-light-blue">
|
|
<div class="card-body">
|
|
<p class="mb-4">Menunggu Persetujuan</p>
|
|
<p class="fs-30 mb-2" id="todayPending">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-light-danger">
|
|
<div class="card-body">
|
|
<p class="mb-4">Ditolak</p>
|
|
<p class="fs-30 mb-2" id="todayRejected">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row" id="monthlyStatsCards" style="display: none;">
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-tale">
|
|
<div class="card-body">
|
|
<p class="mb-4">Total Bulan Ini</p>
|
|
<p class="fs-30 mb-2" id="monthlyTotal">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-dark-blue">
|
|
<div class="card-body">
|
|
<p class="mb-4">Hadir Bulan Ini</p>
|
|
<p class="fs-30 mb-2" id="monthlyPresent">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-light-blue">
|
|
<div class="card-body">
|
|
<p class="mb-4">Pending Bulan Ini</p>
|
|
<p class="fs-30 mb-2" id="monthlyPending">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 grid-margin stretch-card">
|
|
<div class="card card-light-danger">
|
|
<div class="card-body">
|
|
<p class="mb-4">Ditolak Bulan Ini</p>
|
|
<p class="fs-30 mb-2" id="monthlyRejected">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12 grid-margin stretch-card">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>Filter Status:</label>
|
|
<select class="form-control" id="statusFilter">
|
|
<option value="">Semua Status</option>
|
|
<option value="pending">Menunggu</option>
|
|
<option value="accepted">Disetujui</option>
|
|
<option value="rejected">Ditolak</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>Filter Tipe:</label>
|
|
<select class="form-control" id="typeFilter">
|
|
<option value="">Semua Tipe</option>
|
|
<option value="in">Check In</option>
|
|
<option value="out">Check Out</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>Tanggal Mulai:</label>
|
|
<input type="date" class="form-control" id="startDateFilter">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="form-group">
|
|
<label>Tanggal Akhir:</label>
|
|
<input type="date" class="form-control" id="endDateFilter">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-striped" id="attendanceTable">
|
|
<thead>
|
|
<tr>
|
|
<th>No</th>
|
|
<th>Nama Karyawan</th>
|
|
<th>NIP</th>
|
|
<th>Tanggal</th>
|
|
<th>Waktu</th>
|
|
<th>Tipe</th>
|
|
<th>Lokasi</th>
|
|
<th>Status</th>
|
|
<th>Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="detailAttendanceModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Detail Absensi</h5>
|
|
<button type="button" class="close" data-dismiss="modal">
|
|
<span>×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Nama Karyawan:</strong></label>
|
|
<p id="detailEmployeeName">-</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>NIP:</strong></label>
|
|
<p id="detailEmployeeNip">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Tanggal:</strong></label>
|
|
<p id="detailDate">-</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Waktu:</strong></label>
|
|
<p id="detailTime">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Tipe:</strong></label>
|
|
<p id="detailType">-</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Status:</strong></label>
|
|
<p id="detailStatus">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Lokasi:</strong></label>
|
|
<p id="detailLocation">-</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Koordinat:</strong></label>
|
|
<p id="detailCoordinates">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Foto Absensi:</strong></label>
|
|
<div id="detailPhotoWrapper">
|
|
<span class="text-muted">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label><strong>Catatan:</strong></label>
|
|
<p id="detailNotes">-</p>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Dibuat Pada:</strong></label>
|
|
<p id="detailCreatedAt">-</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="form-group">
|
|
<label><strong>Diperbarui Pada:</strong></label>
|
|
<p id="detailUpdatedAt">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="approveAttendanceModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Persetujuan Absensi</h5>
|
|
<button type="button" class="close" data-dismiss="modal">
|
|
<span>×</span>
|
|
</button>
|
|
</div>
|
|
<form id="approveAttendanceForm">
|
|
<input type="hidden" name="attendance_id" id="approveAttendanceId">
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>Nama Karyawan:</label>
|
|
<p id="approveEmployeeName" class="font-weight-bold">-</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Tipe Absensi:</label>
|
|
<p id="approveType" class="font-weight-bold">-</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Tanggal & Waktu:</label>
|
|
<p id="approveDateTime" class="font-weight-bold">-</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Lokasi:</label>
|
|
<p id="approveLocation" class="font-weight-bold">-</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Keputusan <span class="text-danger">*</span></label>
|
|
<select class="form-control" name="status" required>
|
|
<option value="">Pilih Keputusan</option>
|
|
<option value="accepted">Setujui</option>
|
|
<option value="rejected">Tolak</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
|
<button type="submit" class="btn btn-primary">Simpan Keputusan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('script')
|
|
<script>
|
|
let attendanceTable;
|
|
|
|
$(document).ready(function() {
|
|
initDataTable();
|
|
loadStatistics();
|
|
|
|
$('#statusFilter, #typeFilter').change(function() {
|
|
attendanceTable.ajax.reload();
|
|
});
|
|
|
|
$('#startDateFilter, #endDateFilter').change(function() {
|
|
attendanceTable.ajax.reload();
|
|
});
|
|
});
|
|
|
|
function initDataTable() {
|
|
attendanceTable = $('#attendanceTable').DataTable({
|
|
processing: true,
|
|
serverSide: true,
|
|
responsive: true,
|
|
ajax: {
|
|
url: '/api/admin/attendances',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
data: function(d) {
|
|
d.status = $('#statusFilter').val();
|
|
d.type = $('#typeFilter').val();
|
|
d.start_date = $('#startDateFilter').val();
|
|
d.end_date = $('#endDateFilter').val();
|
|
}
|
|
},
|
|
columns: [
|
|
{
|
|
data: null,
|
|
orderable: false,
|
|
searchable: false,
|
|
render: function(data, type, row, meta) {
|
|
return meta.row + meta.settings._iDisplayStart + 1;
|
|
}
|
|
},
|
|
{data: 'user.name', name: 'user.name'},
|
|
{data: 'user.profile.nip', name: 'user.profile.nip'},
|
|
{
|
|
data: 'date',
|
|
name: 'date',
|
|
render: function(data) {
|
|
return new Date(data).toLocaleDateString('id-ID');
|
|
}
|
|
},
|
|
{data: 'time', name: 'time'},
|
|
{
|
|
data: 'type',
|
|
name: 'type',
|
|
render: function(data) {
|
|
if (data === 'check_in') {
|
|
return '<span class="badge badge-primary">Check In</span>';
|
|
} else if (data === 'check_out') {
|
|
return '<span class="badge badge-info">Check Out</span>';
|
|
}
|
|
return '<span class="badge badge-secondary">' + data + '</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'location',
|
|
name: 'location',
|
|
render: function(data) {
|
|
return data ? data.name : '-';
|
|
}
|
|
},
|
|
{
|
|
data: 'status',
|
|
name: 'status',
|
|
render: function(data) {
|
|
if (data === 'pending') {
|
|
return '<span class="badge badge-warning">Menunggu</span>';
|
|
} else if (data === 'accepted') {
|
|
return '<span class="badge badge-success">Disetujui</span>';
|
|
} else if (data === 'rejected') {
|
|
return '<span class="badge badge-danger">Ditolak</span>';
|
|
}
|
|
return '<span class="badge badge-secondary">' + data + '</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'id',
|
|
name: 'id',
|
|
orderable: false,
|
|
searchable: false,
|
|
render: function(data, type, row) {
|
|
let buttons = `
|
|
<button class="btn btn-sm btn-info" onclick="viewAttendance(${data})" title="Lihat Detail">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
`;
|
|
|
|
if (row.status === 'pending') {
|
|
buttons += `
|
|
<button class="btn btn-sm btn-success" onclick="approveAttendance(${data})" title="Persetujuan">
|
|
<i class="ti ti-check"></i>
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
// Tambahkan tombol hapus di sini
|
|
buttons += `
|
|
<button class="btn btn-sm btn-danger" onclick="deleteAttendance(${data})" title="Hapus">
|
|
<i class="ti ti-trash"></i>
|
|
</button>
|
|
`;
|
|
|
|
return buttons;
|
|
}
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
function loadStatistics() {
|
|
$.ajax({
|
|
url: '/api/admin/attendances/data/statistics',
|
|
type: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
const stats = response.data;
|
|
|
|
$('#todayTotal').text(stats.today.total);
|
|
$('#todayPresent').text(stats.today.present);
|
|
$('#todayPending').text(stats.today.pending);
|
|
|
|
$('#monthlyTotal').text(stats.this_month.total);
|
|
$('#monthlyPresent').text(stats.this_month.present);
|
|
$('#monthlyPending').text(stats.this_month.pending);
|
|
},
|
|
error: function(xhr) {
|
|
console.error('Failed to load statistics');
|
|
}
|
|
});
|
|
}
|
|
|
|
function showStatistics() {
|
|
$('#statisticsCards').toggle();
|
|
$('#monthlyStatsCards').toggle();
|
|
loadStatistics();
|
|
}
|
|
|
|
function viewAttendance(id) {
|
|
$.ajax({
|
|
url: `/api/admin/attendances/${id}`,
|
|
type: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
const attendance = response.data;
|
|
|
|
$('#detailEmployeeName').text(attendance.user.name);
|
|
$('#detailEmployeeNip').text(attendance.user.profile.nip);
|
|
$('#detailDate').text(new Date(attendance.date).toLocaleDateString('id-ID'));
|
|
$('#detailTime').text(attendance.time);
|
|
|
|
const typeLabels = {
|
|
'check_in': 'Check In',
|
|
'check_out': 'Check Out'
|
|
};
|
|
$('#detailType').text(typeLabels[attendance.type] || attendance.type);
|
|
|
|
let statusBadge = '';
|
|
if (attendance.status === 'pending') {
|
|
statusBadge = '<span class="badge badge-warning">Menunggu</span>';
|
|
} else if (attendance.status === 'accepted') {
|
|
statusBadge = '<span class="badge badge-success">Disetujui</span>';
|
|
} else if (attendance.status === 'rejected') {
|
|
statusBadge = '<span class="badge badge-danger">Ditolak</span>';
|
|
}
|
|
$('#detailStatus').html(statusBadge);
|
|
|
|
$('#detailLocation').text(attendance.location ? attendance.location.name : '-');
|
|
$('#detailCoordinates').text(attendance.latitude && attendance.longitude ?
|
|
`${attendance.latitude}, ${attendance.longitude}` : '-');
|
|
if (attendance.photo) {
|
|
const photoUrl = `/storage/${attendance.photo}`;
|
|
$('#detailPhotoWrapper').html(`
|
|
<a href="${photoUrl}" target="_blank">
|
|
<img src="${photoUrl}" alt="Foto Absensi" style="max-width: 100%; max-height: 300px; border-radius: 8px;">
|
|
</a>
|
|
`);
|
|
} else {
|
|
$('#detailPhotoWrapper').html(`<span class="text-muted">Tidak ada foto</span>`);
|
|
}
|
|
|
|
$('#detailNotes').text(attendance.notes || '-');
|
|
$('#detailCreatedAt').text(new Date(attendance.created_at).toLocaleDateString('id-ID'));
|
|
$('#detailUpdatedAt').text(new Date(attendance.updated_at).toLocaleDateString('id-ID'));
|
|
|
|
$('#detailAttendanceModal').modal('show');
|
|
},
|
|
error: function(xhr) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Gagal!',
|
|
text: 'Gagal mengambil detail absensi'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function approveAttendance(id) {
|
|
$.ajax({
|
|
url: `/api/admin/attendances/${id}`,
|
|
type: 'GET',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
const attendance = response.data;
|
|
|
|
$('#approveAttendanceId').val(attendance.id);
|
|
$('#approveEmployeeName').text(attendance.user.name);
|
|
|
|
const typeLabels = {
|
|
'check_in': 'Check In',
|
|
'check_out': 'Check Out'
|
|
};
|
|
$('#approveType').text(typeLabels[attendance.type] || attendance.type);
|
|
|
|
const dateTime = `${new Date(attendance.date).toLocaleDateString('id-ID')} ${attendance.time}`;
|
|
$('#approveDateTime').text(dateTime);
|
|
|
|
$('#approveLocation').text(attendance.location ? attendance.location.name : '-');
|
|
|
|
$('#approveAttendanceModal').modal('show');
|
|
},
|
|
error: function(xhr) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Gagal!',
|
|
text: 'Gagal mengambil data absensi'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteAttendance(id) {
|
|
Swal.fire({
|
|
title: 'Yakin ingin menghapus?',
|
|
text: 'Data absensi yang dihapus tidak dapat dikembalikan!',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#d33',
|
|
cancelButtonColor: '#6c757d',
|
|
confirmButtonText: 'Ya, hapus!',
|
|
cancelButtonText: 'Batal'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
$.ajax({
|
|
url: `/api/admin/attendances/${id}`,
|
|
type: 'DELETE',
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'Berhasil!',
|
|
text: response.message
|
|
});
|
|
attendanceTable.ajax.reload();
|
|
loadStatistics();
|
|
},
|
|
error: function(xhr) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Gagal!',
|
|
text: xhr.responseJSON?.message || 'Terjadi kesalahan.'
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
$('#approveAttendanceForm').submit(function(e) {
|
|
e.preventDefault();
|
|
|
|
const attendanceId = $('#approveAttendanceId').val();
|
|
const status = $(this).find('select[name="status"]').val();
|
|
|
|
$.ajax({
|
|
url: `/api/admin/attendances/${attendanceId}/approve`,
|
|
type: 'PUT',
|
|
data: {
|
|
status: status
|
|
},
|
|
headers: {
|
|
'Authorization': getAuthorizationHeader(),
|
|
'Accept': 'application/json'
|
|
},
|
|
success: function(response) {
|
|
$('#approveAttendanceModal').modal('hide');
|
|
$('#approveAttendanceForm')[0].reset();
|
|
attendanceTable.ajax.reload();
|
|
loadStatistics();
|
|
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'Berhasil!',
|
|
text: response.message
|
|
});
|
|
},
|
|
error: function(xhr) {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Gagal!',
|
|
text: xhr.responseJSON.message
|
|
});
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|