907 lines
37 KiB
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);
|
|
}
|
|
} |