SIPDAM/samooapk/resources/views/Admin/Gaji/Penggajian.blade.php

547 lines
22 KiB
PHP

<x-app-layout>
@push('styles')
<link rel="stylesheet" href="{{ asset('css/penggajian.css') }}">
@endpush
<div class="pg-wrap">
{{-- ALERT --}}
<div id="pgAlert" style="display:none;" class="pg-alert"></div>
{{-- ── FORM PERIODE ── --}}
<div class="pg-form-card">
<div class="pg-form-title">
<i class="fas fa-calendar-alt" style="color:var(--primary);"></i>
Pilih Periode Penggajian
</div>
<div class="pg-form-row">
<div class="pg-form-group">
<label>Bulan</label>
<select id="fBulan" class="pg-select">
@for($i = 1; $i <= 12; $i++)
<option value="{{ $i }}" {{ $i == date('n') ? 'selected' : '' }}>
{{ DateTime::createFromFormat('!m', $i)->format('F') }}
</option>
@endfor
</select>
</div>
<div class="pg-form-group">
<label>Tahun</label>
<select id="fTahun" class="pg-select">
@for($y = date('Y'); $y >= date('Y')-3; $y--)
<option value="{{ $y }}" {{ $y == date('Y') ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
<div class="pg-form-group">
<label>Tanggal Mulai</label>
<input type="date" id="fTglMulai" class="pg-date-input">
</div>
<div class="pg-form-group">
<label>Tanggal Selesai</label>
<input type="date" id="fTglSelesai" class="pg-date-input">
</div>
<div class="pg-form-group">
<label>Nama Teknisi</label>
<select id="fTeknisi" class="pg-select" style="min-width:200px;">
<option value="">Semua Teknisi</option>
@foreach($teknisiList ?? [] as $t)
<option value="{{ $t->id_teknisi }}">{{ $t->nama }}</option>
@endforeach
</select>
</div>
<div class="pg-form-group" style="margin-left:auto; flex-direction:row; gap:12px;">
<button class="pg-btn-calc" onclick="hitungGaji()">
<i class="fas fa-magic"></i> Hitung Gaji Otomatis
</button>
<button class="pg-btn-reset" onclick="resetForm()">Reset</button>
</div>
</div>
</div>
{{-- ── SUMMARY ── --}}
<div class="pg-summary" id="pgSummary" style="display:none;">
<div class="pg-summary-item">
<div class="pg-summary-icon green"><i class="fas fa-users"></i></div>
<div>
<div class="pg-summary-val" id="sumTeknisi">0</div>
<div class="pg-summary-lbl">Total Teknisi</div>
</div>
</div>
<div class="pg-summary-item">
<div class="pg-summary-icon cyan"><i class="fas fa-hammer"></i></div>
<div>
<div class="pg-summary-val" id="sumGaji">Rp 0</div>
<div class="pg-summary-lbl">Total Ongkos Kerja</div>
</div>
</div>
<div class="pg-summary-item">
<div class="pg-summary-icon amber"><i class="fas fa-file-invoice-dollar"></i></div>
<div>
<div class="pg-summary-val" id="sumKasbon">Rp 0</div>
<div class="pg-summary-lbl">Potongan Kasbon</div>
</div>
</div>
<div class="pg-summary-item">
<div class="pg-summary-icon violet"><i class="fas fa-wallet"></i></div>
<div>
<div class="pg-summary-val" id="sumBersih">Rp 0</div>
<div class="pg-summary-lbl">Total Gaji Bersih</div>
</div>
</div>
</div>
{{-- ── TABLE CARD ── --}}
<div class="pg-table-card">
<div class="pg-table-head">
<div class="pg-table-head-title">
<i class="fas fa-list-ul" style="color:var(--primary);"></i>
Daftar Perhitungan Gaji
</div>
<button class="pg-btn-pay-all" onclick="prosesSemuaPembayaran()">
<i class="fas fa-check-double"></i> Proses Semua Pembayaran
</button>
</div>
{{-- Loading --}}
<div class="pg-loading" id="pgLoading">
<div class="pg-spinner"></div>
<div style="color:var(--t3);font-size:14px;font-weight:600;">Sedang menghitung gaji teknisi...</div>
</div>
{{-- Empty state --}}
<div class="pg-empty" id="pgEmpty">
<div class="pg-empty-icon">💸</div>
<div class="pg-empty-title">Belum ada data penggajian yang dimuat</div>
<div class="pg-empty-sub">Silakan pilih periode di atas lalu klik <strong>Hitung Gaji Otomatis</strong></div>
</div>
{{-- Table --}}
<div id="pgTableWrap" style="display:none; overflow-x:auto; min-height: 450px; padding-bottom: 100px;">
<table class="pg-table">
<thead>
<tr>
<th style="width: 50px;">#</th>
<th>Teknisi</th>
<th>Periode</th>
<th>Hari Kerja</th>
<th class="text-right">Ongkos Kerja</th>
<th class="text-right">Uang Makan</th>
<th class="text-right">Kasbon</th>
<th class="text-right">Gaji Bersih</th>
<th>Status</th>
<th style="width: 80px; text-align: center;">Aksi</th>
</tr>
</thead>
<tbody id="pgTbody"></tbody>
</table>
</div>
</div>
</div>
{{-- ── DETAIL MODAL ── --}}
<div class="pg-modal-overlay" id="detailModal">
<div class="pg-modal-box">
<div class="pg-modal-hd">
<div class="pg-modal-hd-title">
<i class="fas fa-file-invoice-dollar" style="color:var(--green);"></i>
Detail Perhitungan Gaji
</div>
<button class="pg-modal-close" onclick="closeDetail()"><i class="fas fa-times"></i></button>
</div>
<div class="pg-modal-body" id="detailBody">
<div style="text-align:center;padding:30px;color:var(--t3);">Memuat data…</div>
</div>
<div class="pg-modal-footer">
<button class="pg-btn-close-modal" style="width: 100%;" onclick="closeDetail()">Tutup</button>
</div>
</div>
</div>
@push('scripts')
<script>
let currentPrintId = null;
// ── Auto set tanggal mulai / selesai saat bulan berubah ──
function updateDates() {
const bEl = document.getElementById('fBulan');
const yEl = document.getElementById('fTahun');
if (!bEl || !yEl) return;
const b = parseInt(bEl.value);
const y = parseInt(yEl.value);
if (isNaN(b) || isNaN(y)) return;
const pad = n => String(n).padStart(2, '0');
const lastDay = new Date(y, b, 0).getDate();
const startStr = `${y}-${pad(b)}-01`;
const endStr = `${y}-${pad(b)}-${pad(lastDay)}`;
document.getElementById('fTglMulai').value = startStr;
document.getElementById('fTglSelesai').value = endStr;
}
// Inisialisasi saat halaman dimuat
document.addEventListener('DOMContentLoaded', () => {
updateDates();
document.getElementById('fBulan').addEventListener('change', updateDates);
document.getElementById('fTahun').addEventListener('change', updateDates);
});
// ── Format Rupiah ──
const rp = v => {
const val = parseInt(v || 0);
return 'Rp ' + val.toLocaleString('id-ID');
};
// ── Reset form ──
function resetForm() {
const now = new Date();
document.getElementById('fBulan').value = now.getMonth() + 1;
document.getElementById('fTahun').value = now.getFullYear();
if(document.getElementById('fTeknisi')) document.getElementById('fTeknisi').value = '';
updateDates();
document.getElementById('pgSummary').style.display = 'none';
document.getElementById('pgTableWrap').style.display = 'none';
document.getElementById('pgLoading').style.display = 'none';
document.getElementById('pgEmpty').style.display = 'block';
}
// ── Hitung Gaji ──
function hitungGaji() {
const bulan = document.getElementById('fBulan').value;
const tahun = document.getElementById('fTahun').value;
const teknisi = document.getElementById('fTeknisi').value;
const tglPengga = document.getElementById('fTglSelesai').value;
if (!tglPengga) {
showAlert('err', 'Pilih periode terlebih dahulu agar tanggal terisi.');
return;
}
document.getElementById('pgEmpty').style.display = 'none';
document.getElementById('pgTableWrap').style.display = 'none';
document.getElementById('pgLoading').style.display = 'block';
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('{{ route("penggajian.hitung") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'Accept': 'application/json',
},
body: JSON.stringify({
periode_bulan: bulan,
periode_tahun: tahun,
id_teknisi: teknisi || null,
tanggal_penggajian: tglPengga,
include_kasbon: 1,
})
})
.then(r => r.json())
.then(data => {
document.getElementById('pgLoading').style.display = 'none';
if (data.success) {
showAlert('ok', data.message);
loadTableData(bulan, tahun, teknisi);
} else {
showAlert('err', data.message || 'Terjadi kesalahan.');
document.getElementById('pgEmpty').style.display = 'block';
}
})
.catch(() => {
document.getElementById('pgLoading').style.display = 'none';
document.getElementById('pgEmpty').style.display = 'block';
showAlert('err', 'Terjadi kesalahan jaringan.');
});
}
// ── Load tabel dari server ──
function loadTableData(bulan, tahun, teknisi) {
const params = new URLSearchParams({ periode_bulan: bulan, periode_tahun: tahun });
if (teknisi) params.append('id_teknisi', teknisi);
fetch(`{{ route('penggajian.index') }}?${params.toString()}`, {
headers: { 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
if (!data.rows || data.rows.length === 0) {
document.getElementById('pgEmpty').style.display = 'block';
return;
}
renderTable(data.rows, data.summary);
})
.catch(() => {
// Fallback: reload page
window.location.reload();
});
}
// ── Render rows ──
function renderTable(rows, summary) {
let html = '';
rows.forEach((g, i) => {
const statusHtml = g.status_pembayaran === 'sudah_bayar'
? `<span class="pg-badge pg-badge-ok"><i class="fas fa-check-circle"></i> Lunas</span>`
: `<span class="pg-badge pg-badge-warn"><i class="fas fa-clock"></i> Belum Bayar</span>`;
html += `<tr>
<td>${i + 1}</td>
<td><div style="display:flex;align-items:center;gap:10px;">
<div class="pg-av">${(g.nama_teknisi || 'NA').substring(0,2).toUpperCase()}</div>
<div><div class="pg-name">${g.nama_teknisi || 'N/A'}</div></div>
</div></td>
<td><div class="pg-name">${g.periode_label}</div><div class="pg-sub">${g.tanggal_penggajian}</div></td>
<td><span class="pg-badge pg-badge-ok" style="background:rgba(0,200,255,.12);color:var(--cyan);">${g.jumlah_hari_kerja} Hari</span></td>
<td class="text-right"><span class="pg-money-pos">${rp(g.total_ongkos_pekerjaan)}</span></td>
<td class="text-right"><span class="pg-money-neu">${rp(g.biaya_makan)}</span></td>
<td class="text-right"><span class="pg-money-neg">${rp(g.total_kasbon)}</span></td>
<td class="text-right"><span class="pg-money-pos" style="font-size:15px;">${rp(g.gaji_bersih)}</span></td>
<td>${statusHtml}</td>
<td>
<div class="pg-action-wrap">
<button class="pg-action-btn" onclick="toggleMenu(${g.id_penggajian})">
<i class="fas fa-cog"></i> <i class="fas fa-chevron-down" style="font-size:9px;"></i>
</button>
<div class="pg-dropdown" id="menu_${g.id_penggajian}">
<div class="pg-dropdown-item" onclick="showDetail(${g.id_penggajian})">
<i class="fas fa-eye" style="color:var(--cyan);"></i> Detail
</div>
<div class="pg-dropdown-item" onclick="prosesBayar(${g.id_penggajian})">
<i class="fas fa-check" style="color:var(--green);"></i> Proses Bayar
</div>
<div class="pg-dropdown-item" onclick="editPotongan(${g.id_penggajian}, ${g.total_kasbon})">
<i class="fas fa-edit" style="color:var(--amber);"></i> Ubah Potongan
</div>
<div class="pg-dropdown-item" onclick="recalculateGaji(${g.id_penggajian})">
<i class="fas fa-sync-alt" style="color:var(--violet);"></i> Hitung Ulang
</div>
<div class="pg-dropdown-item" onclick="deleteGaji(${g.id_penggajian})">
<i class="fas fa-trash" style="color:#f87171;"></i> Hapus
</div>
</div>
</div>
</td>
</tr>`;
});
document.getElementById('pgTbody').innerHTML = html;
document.getElementById('pgTableWrap').style.display = 'block';
if (summary) {
document.getElementById('sumTeknisi').textContent = summary.total_teknisi || rows.length;
document.getElementById('sumGaji').textContent = rp(summary.total_gaji);
document.getElementById('sumKasbon').textContent = rp(summary.total_kasbon);
document.getElementById('sumBersih').textContent = rp(summary.gaji_bersih);
document.getElementById('pgSummary').style.display = 'grid';
}
}
// ── Toggle action dropdown ──
function toggleMenu(id) {
document.querySelectorAll('.pg-dropdown').forEach(d => {
if (d.id !== `menu_${id}`) d.classList.remove('show');
});
document.getElementById(`menu_${id}`).classList.toggle('show');
}
document.addEventListener('click', e => {
if (!e.target.closest('.pg-action-wrap')) {
document.querySelectorAll('.pg-dropdown').forEach(d => d.classList.remove('show'));
}
});
// ── Show Detail ──
function showDetail(id) {
document.querySelectorAll('.pg-dropdown').forEach(d => d.classList.remove('show'));
currentPrintId = id;
document.getElementById('detailBody').innerHTML = '<div style="text-align:center;padding:30px;color:var(--t3);"><div style="width:32px;height:32px;border:3px solid var(--line2);border-top-color:var(--green);border-radius:50%;animation:spin .8s linear infinite;margin:0 auto 12px;"></div>Memuat data…</div>';
document.getElementById('detailModal').classList.add('show');
fetch(`{{ url('penggajian') }}/${id}/detail`, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(r => r.text())
.then(html => {
const body = document.getElementById('detailBody');
body.innerHTML = html;
body.style.display = 'block';
body.style.opacity = '1';
body.style.visibility = 'visible';
console.log('Detail loaded');
})
.catch(err => {
console.error('Fetch error:', err);
document.getElementById('detailBody').innerHTML = '<div style="color:#f87171;padding:20px;text-align:center;">Gagal memuat detail.</div>';
});
}
function closeDetail() {
document.getElementById('detailModal').classList.remove('show');
currentPrintId = null;
}
function printSlip() {
if (currentPrintId) window.open(`{{ url('penggajian') }}/${currentPrintId}/slip`, '_blank');
}
// ── Proses Bayar ──
function prosesBayar(id) {
if (!confirm('Proses pembayaran gaji ini?')) return;
fetch(`{{ url('penggajian') }}/${id}/bayar`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) {
const b = document.getElementById('fBulan').value;
const y = document.getElementById('fTahun').value;
const t = document.getElementById('fTeknisi').value;
loadTableData(b, y, t);
}
;
});
}
// ── Proses Semua Pembayaran ──
function prosesSemuaPembayaran() {
if (!confirm('Proses semua pembayaran yang belum dibayar?')) return;
fetch('{{ route("penggajian.bayar-semua") }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) {
const b = document.getElementById('fBulan').value;
const y = document.getElementById('fTahun').value;
const t = document.getElementById('fTeknisi').value;
loadTableData(b, y, t);
}
;
});
}
// ── Ubah Potongan Kasbon ──
function editPotongan(id, currentVal) {
const newVal = prompt("Masukkan nominal potongan kasbon baru (Rp):", currentVal);
if (newVal === null || newVal === "") return;
fetch(`{{ url('penggajian') }}/${id}/update-kasbon`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ total_kasbon: newVal })
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) {
const b = document.getElementById('fBulan').value;
const y = document.getElementById('fTahun').value;
const t = document.getElementById('fTeknisi').value;
loadTableData(b, y, t);
// Jika modal sedang terbuka, refresh konten modalnya
if (document.getElementById('detailModal').classList.contains('show')) {
showDetail(id);
}
}
});
}
// ── Ubah Potongan Makan ──
function editPotonganMakan(id, currentVal) {
const newVal = prompt("Masukkan nominal potongan makan baru (Rp). Masukkan 0 jika ada musibah/duka:", currentVal);
if (newVal === null || newVal === "") return;
fetch(`{{ url('penggajian') }}/${id}/update-makan`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ biaya_makan: newVal })
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) {
const b = document.getElementById('fBulan').value;
const y = document.getElementById('fTahun').value;
const t = document.getElementById('fTeknisi').value;
loadTableData(b, y, t);
if (document.getElementById('detailModal').classList.contains('show')) {
showDetail(id);
}
}
});
}
// ── Hitung Ulang Gaji ──
function recalculateGaji(id) {
if (!confirm('Hitung ulang data gaji ini? Rincian tugas dan kasbon akan diperbarui.')) return;
fetch(`{{ url('penggajian') }}/${id}/recalculate`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) {
const b = document.getElementById('fBulan').value;
const y = document.getElementById('fTahun').value;
const t = document.getElementById('fTeknisi').value;
loadTableData(b, y, t);
}
});
}
// ── Hapus Gaji ──
function deleteGaji(id) {
if (!confirm('Hapus data gaji ini? Aksi tidak bisa dibatalkan.')) return;
fetch(`{{ url('penggajian') }}/${id}`, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json' }
})
.then(r => r.json())
.then(data => {
showAlert(data.success ? 'ok' : 'err', data.message);
if (data.success) window.location.reload();
});
}
// ── Alert ──
function showAlert(type, msg) {
const el = document.getElementById('pgAlert');
el.className = `pg-alert pg-alert-${type}`;
el.innerHTML = `<i class="fas fa-${type === 'ok' ? 'check-circle' : 'exclamation-circle'}"></i> ${msg}`;
el.style.display = 'flex';
setTimeout(() => { el.style.display = 'none'; }, 5000);
}
// Close modal on backdrop click
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) closeDetail();
});
</script>
@endpush
</x-app-layout>