SIPDAM/samooapk/laravel/app/Http/Controllers/PenggajianController.php

907 lines
37 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Penggajian;
use App\Models\DetailPenggajian;
use App\Models\Teknisi;
use App\Models\TarifPekerjaan;
use App\Models\Absensi;
use App\Models\Penugasan;
use App\Models\Kasbon;
use App\Exports\PenggajianExport;
use Maatwebsite\Excel\Facades\Excel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
class PenggajianController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
$query = Penggajian::with(['teknisi'])
->latest('periode_tahun')
->latest('periode_bulan');
if ($request->filled('periode_bulan')) {
$query->where('periode_bulan', $request->periode_bulan);
}
if ($request->filled('periode_tahun')) {
$query->where('periode_tahun', $request->periode_tahun);
}
if ($request->filled('status_pembayaran')) {
$query->where('status_pembayaran', $request->status_pembayaran);
}
if ($request->filled('id_teknisi')) {
$query->where('id_teknisi', $request->id_teknisi);
}
if ($request->expectsJson()) {
$penggajians = $query->get();
$rows = $penggajians->map(function ($g) {
return [
'id_penggajian' => $g->id_penggajian,
'nama_teknisi' => $g->teknisi->nama ?? 'N/A',
'periode_label' => Carbon::create()->month($g->periode_bulan)->format('M') . ' ' . $g->periode_tahun,
'tanggal_penggajian' => $g->tanggal_penggajian->format('d/m/Y'),
'jumlah_hari_kerja' => $g->jumlah_hari_kerja,
'total_ongkos_pekerjaan' => $g->total_ongkos_pekerjaan,
'biaya_makan' => $g->biaya_makan,
'total_kasbon' => $g->total_kasbon,
'gaji_bersih' => $g->gaji_bersih,
'status_pembayaran' => $g->status_pembayaran,
];
});
$summary = $penggajians->count() > 0 ? [
'total_teknisi' => $penggajians->count(),
'total_gaji' => $penggajians->sum('total_ongkos_pekerjaan'),
'total_kasbon' => $penggajians->sum('total_kasbon') + $penggajians->sum('biaya_makan'),
'gaji_bersih' => $penggajians->sum('gaji_bersih'),
] : null;
return response()->json(['rows' => $rows, 'summary' => $summary]);
}
$teknisiList = Teknisi::where('status', 'aktif')->orderBy('nama')->get();
return view('Admin.Gaji.Penggajian', compact('teknisiList'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$teknisis = Teknisi::where('status', 'aktif')->get();
$tarifs = TarifPekerjaan::where('is_active', true)->get();
return view('admin.penggajian.create', compact('teknisis', 'tarifs'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'id_teknisi' => 'required|exists:teknisis,id_teknisi',
'periode_bulan' => 'required|integer|between:1,12',
'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1),
'tanggal_penggajian' => 'required|date',
'biaya_makan' => 'nullable|numeric|min:0',
'total_kasbon' => 'nullable|numeric|min:0',
'details' => 'required|array|min:1',
'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif',
'details.*.jumlah_unit' => 'required|integer|min:1',
'details.*.tarif_per_unit' => 'required|numeric|min:0',
]);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
$exists = Penggajian::isPeriodeExists(
$request->id_teknisi,
$request->periode_bulan,
$request->periode_tahun
);
if ($exists) {
return back()->withErrors([
'periode' => 'Penggajian untuk teknisi ini pada periode tersebut sudah ada.'
])->withInput();
}
DB::beginTransaction();
try {
$gaji_kotor = 0;
foreach ($request->details as $detail) {
$gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit'];
}
$penggajian = Penggajian::create([
'id_teknisi' => $request->id_teknisi,
'periode_bulan' => $request->periode_bulan,
'periode_tahun' => $request->periode_tahun,
'tanggal_penggajian' => $request->tanggal_penggajian,
'jumlah_hari_kerja' => $this->calculateHariKerja(
$request->id_teknisi,
$request->periode_bulan,
$request->periode_tahun
),
'gaji_kotor' => $gaji_kotor,
'biaya_makan' => $request->biaya_makan ?? 0,
'total_kasbon' => $request->total_kasbon ?? 0,
'total_potongan' => $request->total_kasbon ?? 0,
'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR,
]);
foreach ($request->details as $detail) {
DetailPenggajian::create([
'id_penggajian' => $penggajian->id_penggajian,
'id_teknisi' => $request->id_teknisi,
'id_tarif' => $detail['id_tarif'],
'jumlah_unit' => $detail['jumlah_unit'],
'tarif_per_unit' => $detail['tarif_per_unit'],
]);
}
DB::commit();
return redirect()->route('penggajian.index')
->with('success', 'Data penggajian berhasil dibuat.');
} catch (\Exception $e) {
DB::rollback();
return back()->withErrors(['error' => 'Gagal menyimpan data penggajian.'])->withInput();
}
}
/**
* Display the specified resource.
*/
public function show(Penggajian $penggajian)
{
$penggajian->load(['teknisi', 'detailPenggajian.penugasan']);
return view('admin.penggajian.show', compact('penggajian'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Penggajian $penggajian)
{
$penggajian->load(['teknisi', 'detailPenggajian.penugasan']);
$teknisis = Teknisi::where('status', 'aktif')->get();
$tarifs = TarifPekerjaan::where('is_active', true)->get();
return view('admin.penggajian.edit', compact('penggajian', 'teknisis', 'tarifs'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Penggajian $penggajian)
{
$validator = Validator::make($request->all(), [
'tanggal_penggajian' => 'required|date',
'biaya_makan' => 'nullable|numeric|min:0',
'total_kasbon' => 'nullable|numeric|min:0',
'details' => 'required|array|min:1',
'details.*.id_tarif' => 'required|exists:tarif_pekerjaans,id_tarif',
'details.*.jumlah_unit' => 'required|integer|min:1',
'details.*.tarif_per_unit' => 'required|numeric|min:0',
]);
if ($validator->fails()) {
return back()->withErrors($validator)->withInput();
}
DB::beginTransaction();
try {
$gaji_kotor = 0;
foreach ($request->details as $detail) {
$gaji_kotor += $detail['jumlah_unit'] * $detail['tarif_per_unit'];
}
$penggajian->update([
'tanggal_penggajian' => $request->tanggal_penggajian,
'total_ongkos_pekerjaan' => $gaji_kotor,
'biaya_makan' => $request->biaya_makan ?? 0,
'total_kasbon' => $request->total_kasbon ?? 0,
'total_potongan' => $request->total_kasbon ?? 0,
]);
$penggajian->detailPenggajian()->delete();
$gajiData = $this->calculateGajiTeknisi(
$penggajian->id_teknisi,
$penggajian->periode_bulan,
$penggajian->periode_tahun,
$penggajian->tanggal_penggajian
);
foreach ($gajiData['details'] as $detail) {
DetailPenggajian::create([
'id_penggajian' => $penggajian->id_penggajian,
'id_penugasan' => $detail['id_penugasan'],
'tanggal_selesai' => $detail['tanggal_selesai'],
'lokasi' => $detail['lokasi'],
'ongkos_penugasan' => $detail['ongkos_penugasan'],
'jumlah_tim' => $detail['jumlah_tim'],
'bagian_ongkos' => $detail['bagian_ongkos'],
]);
}
DB::commit();
return redirect()->route('penggajian.index')
->with('success', 'Data penggajian berhasil diperbarui.');
} catch (\Exception $e) {
DB::rollback();
return back()->withErrors(['error' => 'Gagal memperbarui data penggajian.'])->withInput();
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Penggajian $penggajian)
{
try {
DB::beginTransaction();
$penggajian->detailPenggajian()->delete();
$penggajian->delete();
DB::commit();
return response()->json(['success' => true, 'message' => 'Data penggajian berhasil dihapus.']);
} catch (\Exception $e) {
DB::rollback();
return response()->json(['success' => false, 'message' => 'Gagal menghapus data penggajian.'], 500);
}
}
/**
* Hitung gaji untuk periode tertentu.
*
* Tambahan parameter:
* force_recalculate (boolean, default false)
* → Jika true, data penggajian yang sudah ada akan dihapus dan dihitung ulang.
* Gunakan ini ketika ada penugasan baru yang selesai SETELAH gaji di-generate,
* sehingga semua anggota tim (termasuk yang tidak jadi teknisi utama) ikut terhitung.
* → Jika false (default), teknisi yang sudah punya data di periode itu akan di-skip.
*
* CATATAN: Penggajian yang sudah berstatus 'sudah_bayar' TIDAK akan di-recalculate
* meski force_recalculate = true, untuk mencegah perubahan data yang sudah dibayar.
*/
public function hitungGaji(Request $request)
{
$validator = Validator::make($request->all(), [
'id_teknisi' => 'nullable|exists:teknisis,id_teknisi',
'periode_bulan' => 'required|integer|between:1,12',
'periode_tahun' => 'required|integer|min:2020|max:' . (date('Y') + 1),
'tanggal_penggajian' => 'required|date',
'include_kasbon' => 'boolean',
'force_recalculate' => 'boolean',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Data tidak valid. ' . implode(' ', $validator->errors()->all()),
'errors' => $validator->errors()
], 422);
}
try {
DB::beginTransaction();
$periode_bulan = $request->periode_bulan;
$periode_tahun = $request->periode_tahun;
$tanggal_penggajian = $request->tanggal_penggajian
?? Carbon::create($periode_tahun, $periode_bulan)->endOfMonth()->format('Y-m-d');
$include_kasbon = $request->boolean('include_kasbon', true);
$force_recalculate = $request->boolean('force_recalculate', false);
$id_teknisi = $request->id_teknisi;
if ($id_teknisi) {
$teknisis = Teknisi::where('id_teknisi', $id_teknisi)->where('status', 'aktif')->get();
} else {
$teknisis = Teknisi::where('status', 'aktif')->get();
}
$created_count = 0;
$skipped_count = 0;
$recalculated_count = 0;
foreach ($teknisis as $teknisi) {
// Cek apakah sudah ada penggajian untuk periode ini
$existingPenggajian = Penggajian::where('id_teknisi', $teknisi->id_teknisi)
->where('periode_bulan', $periode_bulan)
->where('periode_tahun', $periode_tahun)
->first();
if ($existingPenggajian) {
if (!$force_recalculate) {
// Tidak force → skip seperti perilaku lama
$skipped_count++;
continue;
}
// Sudah dibayar → jangan recalculate, lindungi data yang sudah final
if ($existingPenggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR) {
$skipped_count++;
continue;
}
// Force recalculate → hapus detail dan penggajian lama, hitung ulang
$existingPenggajian->detailPenggajian()->delete();
$existingPenggajian->delete();
$recalculated_count++;
}
$gajiData = $this->calculateGajiTeknisi(
$teknisi->id_teknisi,
$periode_bulan,
$periode_tahun,
$tanggal_penggajian,
$include_kasbon
);
if ($gajiData['gaji_kotor'] > 0 || $gajiData['biaya_makan'] > 0 || !empty($gajiData['details'])) {
$total_penugasan = !empty($gajiData['details']) ? count($gajiData['details']) : 0;
$penggajian = Penggajian::create([
'id_teknisi' => $teknisi->id_teknisi,
'periode_bulan' => $periode_bulan,
'periode_tahun' => $periode_tahun,
'tanggal_penggajian' => $tanggal_penggajian,
'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'],
'jumlah_penugasan_selesai' => $total_penugasan,
'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'],
'biaya_makan' => $gajiData['biaya_makan'],
'total_kasbon' => $gajiData['total_kasbon'],
'total_potongan' => $gajiData['total_kasbon'],
'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - $gajiData['total_kasbon'],
'status_pembayaran' => Penggajian::STATUS_BELUM_BAYAR,
]);
foreach ($gajiData['details'] as $detail) {
DetailPenggajian::create([
'id_penggajian' => $penggajian->id_penggajian,
'id_penugasan' => $detail['id_penugasan'],
'tanggal_selesai' => $detail['tanggal_selesai'],
'lokasi' => $detail['lokasi'],
'ongkos_penugasan' => $detail['ongkos_penugasan'],
'jumlah_tim' => $detail['jumlah_tim'],
'bagian_ongkos' => $detail['bagian_ongkos'],
'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null,
]);
}
$created_count++;
}
}
DB::commit();
$message = "Berhasil menghitung gaji untuk {$created_count} teknisi.";
if ($recalculated_count > 0) {
$message .= " {$recalculated_count} teknisi dihitung ulang.";
}
if ($skipped_count > 0) {
$message .= " {$skipped_count} teknisi dilewati (sudah ada data atau sudah dibayar).";
}
return response()->json([
'success' => true,
'message' => $message,
'data' => [
'created' => $created_count,
'recalculated' => $recalculated_count,
'skipped' => $skipped_count,
]
]);
} catch (\Exception $e) {
DB::rollback();
return response()->json([
'success' => false,
'message' => 'Gagal menghitung gaji: ' . $e->getMessage()
], 500);
}
}
/**
* Hitung ulang penggajian untuk satu teknisi tertentu.
*
* Endpoint: POST /penggajian/{penggajian}/recalculate
*
* Berguna ketika:
* - Ada penugasan tim yang baru selesai setelah gaji di-generate
* - Anggota tim yang tidak jadi teknisi utama tidak muncul di rincian
* - Data detail penggajian tidak lengkap / tidak sesuai
*
* Penggajian yang sudah 'sudah_bayar' tidak bisa di-recalculate.
*/
public function recalculate(Penggajian $penggajian)
{
try {
DB::beginTransaction();
// Hapus detail lama
$penggajian->detailPenggajian()->delete();
// Jika sudah lunas, kita tidak ingin menarik data kasbon baru dari database
// karena kasbon aslinya mungkin sudah berstatus lunas.
// Kita gunakan nilai total_kasbon yang sudah tersimpan di record ini.
$isPaid = $penggajian->status_pembayaran === Penggajian::STATUS_SUDAH_BAYAR;
$gajiData = $this->calculateGajiTeknisi(
$penggajian->id_teknisi,
$penggajian->periode_bulan,
$penggajian->periode_tahun,
$penggajian->tanggal_penggajian,
!$isPaid // includeKasbon hanya jika belum bayar
);
$total_penugasan = count($gajiData['details']);
// Update header penggajian
$penggajian->update([
'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'],
'jumlah_penugasan_selesai' => $total_penugasan,
'total_ongkos_pekerjaan' => $gajiData['gaji_kotor'],
'biaya_makan' => $gajiData['biaya_makan'],
'total_kasbon' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'],
'total_potongan' => $isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon'],
'gaji_bersih' => $gajiData['gaji_kotor'] - $gajiData['biaya_makan'] - ($isPaid ? $penggajian->total_kasbon : $gajiData['total_kasbon']),
]);
// Insert detail baru
foreach ($gajiData['details'] as $detail) {
DetailPenggajian::create([
'id_penggajian' => $penggajian->id_penggajian,
'id_penugasan' => $detail['id_penugasan'],
'tanggal_selesai' => $detail['tanggal_selesai'],
'lokasi' => $detail['lokasi'],
'ongkos_penugasan' => $detail['ongkos_penugasan'],
'jumlah_tim' => $detail['jumlah_tim'],
'bagian_ongkos' => $detail['bagian_ongkos'],
'rincian_pekerjaan'=> $detail['rincian_pekerjaan'] ?? null,
]);
}
DB::commit();
return response()->json([
'success' => true,
'message' => "Penggajian berhasil dihitung ulang. Ditemukan {$total_penugasan} penugasan.",
'data' => [
'total_penugasan' => $total_penugasan,
'total_ongkos' => $gajiData['gaji_kotor'],
'jumlah_hari_kerja' => $gajiData['jumlah_hari_kerja'],
'biaya_makan' => $gajiData['biaya_makan'],
'total_kasbon' => $gajiData['total_kasbon'],
]
]);
} catch (\Exception $e) {
DB::rollback();
return response()->json([
'success' => false,
'message' => 'Gagal menghitung ulang: ' . $e->getMessage()
], 500);
}
}
/**
* Get detail penggajian for modal
*/
public function detail(Penggajian $penggajian)
{
$penggajian->load(['teknisi', 'detailPenggajian.penugasan']);
return view('Admin.Gaji.detail_penggajian', compact('penggajian'));
}
/**
* Process payment for specific penggajian
*/
public function prosesPembayaran(Penggajian $penggajian)
{
try {
if ($penggajian->isPaid()) {
return response()->json(['success' => false, 'message' => 'Gaji sudah dibayar sebelumnya.']);
}
$penggajian->markAsPaid();
// ── LOGIKA PELUNASAN KASBON (DENGAN SISTEM CICILAN/PARTIAL) ──
$sisaPotongan = (float) $penggajian->total_kasbon;
if ($sisaPotongan > 0) {
// Ambil semua kasbon belum lunas, urutkan dari yang paling lama
$kasbons = Kasbon::where('id_teknisi', $penggajian->id_teknisi)
->where('status', 'belum_lunas')
->whereDate('tanggal_kasbon', '<=', $penggajian->tanggal_penggajian)
->orderBy('tanggal_kasbon', 'asc')
->get();
foreach ($kasbons as $kb) {
if ($sisaPotongan <= 0) break;
$jumlahKb = (float) $kb->jumlah_kasbon;
if ($sisaPotongan >= $jumlahKb) {
// Kasbon ini terbayar penuh
$kb->update(['status' => 'lunas']);
$sisaPotongan -= $jumlahKb;
} else {
// Kasbon ini terbayar sebagian (CICILAN)
// 1. Kurangi nilai kasbon lama
$kb->update([
'jumlah_kasbon' => $jumlahKb - $sisaPotongan
]);
// 2. Buat record baru untuk bagian yang sudah lunas (untuk histori)
$newLunas = $kb->replicate();
$newLunas->jumlah_kasbon = $sisaPotongan;
$newLunas->status = 'lunas';
$newLunas->keperluan = $kb->keperluan . ' (Cicilan via Gaji ' . $penggajian->formatted_periode . ')';
$newLunas->save();
$sisaPotongan = 0;
}
}
}
return response()->json([
'success' => true,
'message' => 'Pembayaran berhasil diproses dan kasbon telah dilunasi.'
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500);
}
}
public function updateKasbon(Request $request, Penggajian $penggajian)
{
try {
$rawInput = $request->input('total_kasbon', 0);
$cleanInput = preg_replace('/[^0-9]/', '', $rawInput);
$newKasbon = (float) $cleanInput;
if ($newKasbon < 0) {
return response()->json(['success' => false, 'message' => 'Nominal kasbon tidak boleh negatif.'], 422);
}
$penggajian->update([
'total_kasbon' => $newKasbon,
'total_potongan' => $newKasbon,
'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $penggajian->biaya_makan - $newKasbon,
]);
return response()->json([
'success' => true,
'message' => 'Potongan kasbon berhasil diubah menjadi Rp ' . number_format($newKasbon, 0, ',', '.') . '. Gaji bersih akan dihitung ulang otomatis.'
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Gagal mengubah potongan: ' . $e->getMessage()], 500);
}
}
/**
* Update food deduction amount manually.
* Allows mandor to waive food costs for bereavement or emergencies.
*/
public function updateMakan(Request $request, Penggajian $penggajian)
{
try {
$rawInput = $request->input('biaya_makan', 0);
$cleanInput = preg_replace('/[^0-9]/', '', $rawInput);
$newMakan = (float) $cleanInput;
if ($newMakan < 0) {
return response()->json(['success' => false, 'message' => 'Nominal biaya makan tidak boleh negatif.'], 422);
}
$penggajian->update([
'biaya_makan' => $newMakan,
'gaji_bersih' => $penggajian->total_ongkos_pekerjaan - $newMakan - $penggajian->total_kasbon,
]);
return response()->json([
'success' => true,
'message' => 'Biaya makan berhasil diubah menjadi Rp ' . number_format($newMakan, 0, ',', '.') . '.'
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Gagal mengubah biaya makan: ' . $e->getMessage()], 500);
}
}
/**
* Process all unpaid payments
*/
public function prosesSemuaPembayaran()
{
try {
$unpaidCount = Penggajian::belumBayar()->count();
if ($unpaidCount == 0) {
return response()->json(['success' => false, 'message' => 'Tidak ada pembayaran yang perlu diproses.']);
}
Penggajian::belumBayar()->update(['status_pembayaran' => Penggajian::STATUS_SUDAH_BAYAR]);
return response()->json([
'success' => true,
'message' => "Berhasil memproses pembayaran untuk {$unpaidCount} teknisi."
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Gagal memproses pembayaran.'], 500);
}
}
/**
* Generate slip gaji
*/
public function slip(Penggajian $penggajian)
{
$penggajian->load(['teknisi', 'detailPenggajian.penugasan']);
return view('admin.Gaji.slip_penggajian', compact('penggajian'));
}
/**
* Export to Excel
*/
public function export(Request $request)
{
$query = Penggajian::with(['teknisi']);
if ($request->filled('periode_bulan')) $query->where('periode_bulan', $request->periode_bulan);
if ($request->filled('periode_tahun')) $query->where('periode_tahun', $request->periode_tahun);
if ($request->filled('status_pembayaran')) $query->where('status_pembayaran', $request->status_pembayaran);
$penggajians = $query->get();
return Excel::download(new PenggajianExport($penggajians), 'penggajian_' . date('Y-m-d') . '.xlsx');
}
// =====================================================================
// PRIVATE HELPERS
// =====================================================================
/**
* Calculate gaji untuk teknisi tertentu.
*
* Mencari semua penugasan di mana teknisi ini terlibat, baik sebagai:
* (a) Teknisi utama (kolom id_teknisi di tabel penugasans), ATAU
* (b) Anggota tim (tabel tim_teknisi_penugasans) dengan status_kehadiran = hadir
*
* Ongkos dibagi rata berdasarkan jumlah anggota tim yang hadir.
* Contoh: ongkos Rp 50.000, tim 2 orang → masing-masing Rp 25.000
*/
private function calculateGajiTeknisi($idTeknisi, $bulan, $tahun, $tanggalLimit = null, $includeKasbon = true)
{
$jumlah_hari_kerja = Absensi::where('id_teknisi', $idTeknisi)
->whereMonth('tanggal', $bulan)
->whereYear('tanggal', $tahun)
->where('status', 'hadir')
->count();
// Cari semua penugasan yang melibatkan teknisi ini di bulan/tahun tsb
$penugasans = Penugasan::where(function ($q) use ($idTeknisi) {
// (a) Teknisi utama penugasan
$q->where('id_teknisi', $idTeknisi)
// (b) Anggota tim yang hadir
->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) {
$sq->where('id_teknisi', $idTeknisi)
->where('status_kehadiran', 'hadir');
});
})
->where('status_pekerjaan', 'selesai')
->where(function ($q) use ($bulan, $tahun) {
// Filter berdasarkan bulan selesai
// Prioritas: tanggal_diselesaikan, fallback ke updated_at
$q->where(function ($q2) use ($bulan, $tahun) {
$q2->whereNotNull('tanggal_diselesaikan')
->whereMonth('tanggal_diselesaikan', $bulan)
->whereYear('tanggal_diselesaikan', $tahun);
})->orWhere(function ($q2) use ($bulan, $tahun) {
$q2->whereNull('tanggal_diselesaikan')
->whereMonth('updated_at', $bulan)
->whereYear('updated_at', $tahun);
});
})
->with(['items.tarif', 'timTeknisi'])
->get();
$gaji_kotor = 0;
$list_penugasan = [];
foreach ($penugasans as $penugasan) {
// Hitung jumlah anggota tim yang hadir
// Jika tidak ada record tim → teknisi kerja sendiri → jumlah = 1
$jumlahHadir = $penugasan->countTimHadir();
if ($jumlahHadir === 0) {
$jumlahHadir = 1;
}
// Hitung total ongkos dari penugasan_items
$totalOngkosTugas = 0;
if ($penugasan->items->count() > 0) {
foreach ($penugasan->items as $item) {
$itemTotal = (float) $item->total_nilai_pekerjaan;
if ($itemTotal <= 0) {
$itemTotal = $this->calculatePenugasanItemValue($item);
}
$totalOngkosTugas += $itemTotal;
}
}
// Fallback: ambil dari total_nilai_pekerjaan penugasan induk atau tarif
if ($totalOngkosTugas <= 0) {
$totalOngkosTugas = $this->calculatePenugasanValue($penugasan);
}
// Lewati jika ongkos masih 0 setelah semua fallback
if ($totalOngkosTugas <= 0) {
continue;
}
// Bagian ongkos = total ongkos dibagi jumlah anggota tim yang hadir
$bagianOngkos = $totalOngkosTugas / $jumlahHadir;
$gaji_kotor += $bagianOngkos;
$list_penugasan[] = [
'id_penugasan' => $penugasan->id_penugasan,
'tanggal_selesai' => $penugasan->tanggal_diselesaikan ?? $penugasan->tanggal_diberikan,
'lokasi' => $penugasan->alamat_lokasi
?? $penugasan->lokasi_pekerjaan
?? '-',
'ongkos_penugasan' => $totalOngkosTugas,
'jumlah_tim' => $jumlahHadir,
'bagian_ongkos' => $bagianOngkos,
'rincian_pekerjaan'=> $this->generateRincianLabel($penugasan),
];
}
$biaya_makan = $jumlah_hari_kerja * 25000;
$total_kasbon = 0;
if ($includeKasbon) {
$queryKasbon = Kasbon::where('id_teknisi', $idTeknisi)
->where('status', 'belum_lunas');
if ($tanggalLimit) {
$queryKasbon->whereDate('tanggal_kasbon', '<=', $tanggalLimit);
}
$total_kasbon = $queryKasbon->sum('jumlah_kasbon');
}
return [
'jumlah_hari_kerja' => $jumlah_hari_kerja,
'gaji_kotor' => $gaji_kotor,
'biaya_makan' => $biaya_makan,
'total_kasbon' => $total_kasbon,
'details' => $list_penugasan,
];
}
/**
* Calculate hari kerja dari absensi
*/
private function calculateHariKerja($idTeknisi, $bulan, $tahun)
{
return Absensi::where('id_teknisi', $idTeknisi)
->whereMonth('tanggal', $bulan)
->whereYear('tanggal', $tahun)
->where('status', 'hadir')
->count();
}
/**
* Calculate nilai penugasan dengan fallback ke tarif
*/
private function calculatePenugasanValue($penugasan): float
{
if ($penugasan->total_nilai_pekerjaan > 0) {
return (float) $penugasan->total_nilai_pekerjaan;
}
$tarif = $penugasan->tarif;
if (!$tarif) {
$query = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan)
->where('is_active', true);
if ($penugasan->dimensi_pipa) {
$query->where('dimensi_pipa', $penugasan->dimensi_pipa);
}
$tarif = $query->first();
}
if (!$tarif) return 0;
if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) {
return (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter;
}
if ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) {
return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit;
}
if ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) {
return (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik;
}
return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0);
}
/**
* Generate label rincian pekerjaan untuk slip gaji
*/
private function generateRincianLabel($penugasan): string
{
$rincian = [];
if ($penugasan->items && $penugasan->items->count() > 0) {
foreach ($penugasan->items as $item) {
$detail = $item->jenis_pekerjaan;
if ($item->jarak_meter > 0) {
$detail .= " ({$item->jarak_meter}m)";
} elseif ($item->jumlah_unit > 0) {
$detail .= " ({$item->jumlah_unit} Unit)";
} elseif ($item->jumlah_titik > 0) {
$detail .= " ({$item->jumlah_titik} Titik)";
}
$rincian[] = $detail;
}
} else {
// Legacy: penugasan belum punya penugasan_items
if ($penugasan->jarak_meter > 0) {
$rincian[] = "{$penugasan->jarak_meter} Meter";
} elseif ($penugasan->jumlah_unit > 0) {
$rincian[] = "{$penugasan->jumlah_unit} Unit";
} else {
$rincian[] = "Borongan";
}
}
return implode(', ', $rincian);
}
/**
* Calculate nilai untuk setiap PenugasanItem
*/
private function calculatePenugasanItemValue($item): float
{
if ($item->total_nilai_pekerjaan > 0) {
return (float) $item->total_nilai_pekerjaan;
}
$tarif = $item->tarif;
if (!$tarif) {
$query = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan)
->where('is_active', true);
if ($item->dimensi_pipa) {
$query->where('dimensi_pipa', $item->dimensi_pipa);
}
$tarif = $query->first();
}
if (!$tarif) return 0;
if ($item->jarak_meter > 0 && $tarif->tarif_per_meter) {
return (float) $tarif->tarif_per_meter * (float) $item->jarak_meter;
}
if ($item->jumlah_unit > 0 && $tarif->tarif_per_unit) {
return (float) $tarif->tarif_per_unit * (int) $item->jumlah_unit;
}
if ($item->jumlah_titik > 0 && $tarif->tarif_per_unit) {
return (float) $tarif->tarif_per_unit * (int) $item->jumlah_titik;
}
return (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0);
}
}