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); } }