input('id_teknisi'); if (!$idTeknisi) { return response()->json([ 'success' => false, 'message' => 'ID Teknisi tidak ditemukan' ], 401); } $query = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) ->where(function ($q) use ($idTeknisi) { $q->where('id_teknisi', $idTeknisi) ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { $sq->where('id_teknisi', $idTeknisi); }); }); if ($request->filled('status')) { $query->where('status_pekerjaan', $request->status); } if ($request->filled('jenis_pekerjaan')) { $query->where('jenis_pekerjaan', $request->jenis_pekerjaan); } if ($request->filled('tanggal_mulai')) { $query->whereDate('tanggal_diberikan', '>=', $request->tanggal_mulai); } if ($request->filled('tanggal_akhir')) { $query->whereDate('tanggal_diberikan', '<=', $request->tanggal_akhir); } $penugasan = $query->orderBy('tanggal_diberikan', 'desc')->paginate(15); $penugasan->getCollection()->transform(function ($item) { $namaTim = $item->timTeknisi->map(function ($tt) { return $tt->teknisi->nama ?? 'N/A'; })->implode(', '); $item->nama_tim = !empty($namaTim) ? $namaTim : ($item->teknisi->nama ?? 'N/A'); if ($item->teknisi) { $item->teknisi->nama = $item->nama_tim; } $item->foto_surat_url = $item->foto_surat_url; $item->foto_sebelum_url = $item->foto_sebelum_url; $item->foto_sesudah_url = $item->foto_sesudah_url; $item->label_jenis_pekerjaan = $item->label_jenis_pekerjaan; return $item; }); return response()->json([ 'success' => true, 'message' => 'Data penugasan berhasil diambil', 'data' => $penugasan ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal mengambil data: ' . $e->getMessage() ], 500); } } /** * GET - Detail penugasan */ public function show($id) { try { $penugasan = Penugasan::with(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']) ->findOrFail($id); $teamMembers = $penugasan->timTeknisi->map(function ($tt) { return $tt->teknisi->nama ?? null; })->filter()->unique()->values(); $namaTim = $teamMembers->implode(', '); $data = $penugasan->toArray(); $fullTeamNames = !empty($namaTim) ? $namaTim : ($penugasan->teknisi->nama ?? 'N/A'); if (isset($data['teknisi'])) { $data['teknisi']['nama'] = $fullTeamNames; } $prefix = !empty($namaTim) ? "[Tim: $namaTim] " : ""; $data['catatan_admin'] = $prefix . ($penugasan->catatan_admin ?? ''); $data['instruksi_tambahan'] = $data['catatan_admin']; $data['foto_surat_url'] = $penugasan->foto_surat_url; $data['foto_sebelum_url'] = $penugasan->foto_sebelum_url; $data['foto_sesudah_url'] = $penugasan->foto_sesudah_url; $data['label_jenis_pekerjaan'] = $penugasan->label_jenis_pekerjaan; $data['is_garansi_aktif'] = $penugasan->isGaransiAktif(); $data['sisa_hari_garansi'] = $penugasan->getSisaHariGaransi(); return response()->json([ 'success' => true, 'message' => 'Detail penugasan berhasil diambil', 'data' => $data ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Data tidak ditemukan: ' . $e->getMessage() ], 404); } } /** * POST - Teknisi melengkapi detail pekerjaan via mobile (pertama kali) */ public function lengkapiDetail(Request $request, $id) { try { $penugasan = Penugasan::findOrFail($id); $validator = Validator::make($request->all(), [ 'items' => 'required|array|min:1', 'items.*.jenis_pekerjaan' => 'required|string', 'items.*.dimensi_pipa' => 'nullable', 'items.*.jarak_meter' => 'nullable|numeric', 'items.*.jumlah_unit' => 'nullable|integer', 'items.*.jumlah_titik' => 'nullable|integer', 'items.*.pakai_pipa_besi' => 'nullable', 'items.*.jenis_pengangkatan' => 'nullable', 'detail_pekerjaan' => 'nullable|string', 'tanggal_mulai' => 'required|date', 'tim_teknisi' => 'nullable|array', 'foto_sebelum' => 'nullable|file|max:10240', 'foto_sesudah' => 'nullable|file|max:10240', 'foto_sebelum_base64' => 'nullable|string', 'foto_sesudah_base64' => 'nullable|string', ]); if ($validator->fails()) { \Illuminate\Support\Facades\Log::error('Validation Fail in lengkapiDetail', $validator->errors()->toArray()); return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } DB::beginTransaction(); \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); $fotoSebelum = $penugasan->foto_sebelum; if ($request->hasFile('foto_sebelum')) { $fotoSebelum = $request->file('foto_sebelum')->store('penugasan/foto-sebelum', 'public'); } elseif ($request->foto_sebelum_base64) { $fotoSebelum = $this->storeBase64($request->foto_sebelum_base64, 'penugasan/foto-sebelum'); } $fotoSesudah = $penugasan->foto_sesudah; if ($request->hasFile('foto_sesudah')) { $fotoSesudah = $request->file('foto_sesudah')->store('penugasan/foto-sesudah', 'public'); } elseif ($request->foto_sesudah_base64) { $fotoSesudah = $this->storeBase64($request->foto_sesudah_base64, 'penugasan/foto-sesudah'); } $totalNilaiPenugasan = 0; $hasSR = false; foreach ($request->items as $itemData) { $tarif = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) ->where('is_active', true); if (!empty($itemData['dimensi_pipa'])) { $tarif->where('dimensi_pipa', $itemData['dimensi_pipa']); } if (isset($itemData['pakai_pipa_besi'])) { $tarif->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); } $tarif = $tarif->first(); $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); $totalNilaiPenugasan += $nilaiItem; \App\Models\PenugasanItem::create([ 'id_penugasan' => $penugasan->id_penugasan, 'id_tarif' => $tarif ? $tarif->id_tarif : null, 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, 'jarak_meter' => $itemData['jarak_meter'] ?? null, 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, 'total_nilai_pekerjaan' => $nilaiItem, ]); if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; } $firstItem = $request->items[0]; $penugasan->update([ 'jenis_pekerjaan' => $firstItem['jenis_pekerjaan'], 'dimensi_pipa' => $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa, 'jarak_meter' => $firstItem['jarak_meter'] ?? $penugasan->jarak_meter, 'jumlah_unit' => $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit, 'pakai_pipa_besi' => array_key_exists('pakai_pipa_besi', $firstItem) ? $firstItem['pakai_pipa_besi'] : $penugasan->pakai_pipa_besi, 'status_pekerjaan' => $penugasan->status_pekerjaan === 'belum_mulai' ? 'dalam_proses' : $penugasan->status_pekerjaan, 'total_nilai_pekerjaan' => $totalNilaiPenugasan, 'detail_pekerjaan' => $request->has('detail_pekerjaan') ? $request->detail_pekerjaan : $penugasan->detail_pekerjaan, 'tanggal_mulai' => $request->tanggal_mulai, 'foto_sebelum' => $fotoSebelum, 'foto_sesudah' => $fotoSesudah, ]); if ($hasSR) { $penugasan->setGaransiMeteranAir($request->tanggal_mulai); $penugasan->save(); } if ($request->filled('tim_teknisi')) { foreach ($request->tim_teknisi as $idTeknisiTambahan) { $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) ->where('id_teknisi', $idTeknisiTambahan) ->exists(); if (!$exists) { TimTeknisiPenugasan::create([ 'id_penugasan' => $penugasan->id_penugasan, 'id_teknisi' => $idTeknisiTambahan, 'status_kehadiran' => 'hadir', ]); } } } DB::commit(); $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); return response()->json([ 'success' => true, 'message' => 'Detail pekerjaan berhasil dilengkapi!', 'data' => $penugasan ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal melengkapi detail: ' . $e->getMessage() ], 500); } } /** * PUT - Update / edit detail pekerjaan yang sudah diisi sebelumnya (teknisi via mobile) */ public function updateDetail(Request $request, $id) { try { $penugasan = Penugasan::with(['timTeknisi'])->findOrFail($id); $validator = Validator::make($request->all(), [ 'id_teknisi' => 'required|integer', 'items' => 'required|array|min:1', 'items.*.id_penugasan_item' => 'nullable|integer', 'items.*.jenis_pekerjaan' => 'required|string', 'items.*.dimensi_pipa' => 'nullable', 'items.*.jarak_meter' => 'nullable|numeric', 'items.*.jumlah_unit' => 'nullable|integer', 'items.*.jumlah_titik' => 'nullable|integer', 'items.*.pakai_pipa_besi' => 'nullable', 'items.*.jenis_pengangkatan' => 'nullable', 'detail_pekerjaan' => 'nullable|string', 'tanggal_mulai' => 'nullable|date', 'tim_teknisi' => 'nullable|array', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } $idTeknisiEditor = $request->id_teknisi; $isAssigned = ($penugasan->id_teknisi == $idTeknisiEditor) || $penugasan->timTeknisi->pluck('id_teknisi')->contains($idTeknisiEditor); if (!$isAssigned) { return response()->json([ 'success' => false, 'message' => 'Anda tidak berwenang mengedit penugasan ini' ], 403); } DB::beginTransaction(); \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan)->delete(); $hasSR = false; $processedItemIds = []; foreach ($request->items as $itemData) { $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $itemData['jenis_pekerjaan']) ->where('is_active', true); if (!empty($itemData['dimensi_pipa'])) { $tarifQuery->where('dimensi_pipa', $itemData['dimensi_pipa']); } if (isset($itemData['pakai_pipa_besi'])) { $tarifQuery->where('pakai_pipa_besi', $itemData['pakai_pipa_besi']); } $tarif = $tarifQuery->first(); $nilaiItem = $this->hitungNilaiItem($tarif, $itemData); $created = \App\Models\PenugasanItem::create([ 'id_penugasan' => $penugasan->id_penugasan, 'id_tarif' => $tarif ? $tarif->id_tarif : null, 'jenis_pekerjaan' => $itemData['jenis_pekerjaan'], 'dimensi_pipa' => $itemData['dimensi_pipa'] ?? null, 'jarak_meter' => $itemData['jarak_meter'] ?? null, 'jumlah_unit' => $itemData['jumlah_unit'] ?? null, 'jumlah_titik' => $itemData['jumlah_titik'] ?? null, 'pakai_pipa_besi' => $itemData['pakai_pipa_besi'] ?? null, 'jenis_pengangkatan' => $itemData['jenis_pengangkatan'] ?? null, 'total_nilai_pekerjaan' => $nilaiItem, ]); $processedItemIds[] = $created->id_penugasan_item; if ($itemData['jenis_pekerjaan'] === 'sr') $hasSR = true; } if ($request->has('detail_pekerjaan')) { $penugasan->detail_pekerjaan = $request->detail_pekerjaan; } if ($request->filled('tanggal_mulai')) { $penugasan->tanggal_mulai = $request->tanggal_mulai; } if ($request->filled('tim_teknisi')) { foreach ($request->tim_teknisi as $idTeknisiTambahan) { $exists = TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) ->where('id_teknisi', $idTeknisiTambahan) ->exists(); if (!$exists) { TimTeknisiPenugasan::create([ 'id_penugasan' => $penugasan->id_penugasan, 'id_teknisi' => $idTeknisiTambahan, 'status_kehadiran' => 'hadir', ]); } } } $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) ->sum('total_nilai_pekerjaan'); $firstItem = $request->items[0]; $penugasan->total_nilai_pekerjaan = $total; $penugasan->jenis_pekerjaan = $firstItem['jenis_pekerjaan'] ?? $penugasan->jenis_pekerjaan; $penugasan->dimensi_pipa = $firstItem['dimensi_pipa'] ?? $penugasan->dimensi_pipa; $penugasan->jarak_meter = $firstItem['jarak_meter'] ?? $penugasan->jarak_meter; $penugasan->jumlah_unit = $firstItem['jumlah_unit'] ?? $penugasan->jumlah_unit; $penugasan->pakai_pipa_besi = array_key_exists('pakai_pipa_besi', $firstItem) ? $firstItem['pakai_pipa_besi'] : $penugasan->pakai_pipa_besi; $statusBolehDiubah = ['belum_mulai']; if (in_array($penugasan->status_pekerjaan, $statusBolehDiubah)) { $penugasan->status_pekerjaan = 'dalam_proses'; } $penugasan->save(); if ($hasSR) { $penugasan->setGaransiMeteranAir($penugasan->tanggal_mulai ?? null); $penugasan->save(); } DB::commit(); $penugasan->load(['teknisi', 'tarif', 'timTeknisi.teknisi', 'items.tarif']); return response()->json([ 'success' => true, 'message' => 'Detail pekerjaan berhasil diperbarui!', 'data' => $penugasan ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal memperbarui detail: ' . $e->getMessage() ], 500); } } /** * POST - Tambah rincian pekerjaan baru di tengah progres */ public function addItem(Request $request, $id) { try { $penugasan = Penugasan::findOrFail($id); $validator = Validator::make($request->all(), [ 'jenis_pekerjaan' => 'required|string', 'dimensi_pipa' => 'nullable', 'jarak_meter' => 'nullable|numeric', 'jumlah_unit' => 'nullable|integer', 'jumlah_titik' => 'nullable|integer', 'pakai_pipa_besi' => 'nullable', 'jenis_pengangkatan' => 'nullable', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } DB::beginTransaction(); $tarif = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) ->where('is_active', true); if ($request->filled('dimensi_pipa')) { $tarif->where('dimensi_pipa', $request->dimensi_pipa); } if ($request->has('pakai_pipa_besi')) { $tarif->where('pakai_pipa_besi', $request->pakai_pipa_besi); } $tarif = $tarif->first(); $nilaiItem = $this->hitungNilaiItem($tarif, $request->all()); \App\Models\PenugasanItem::create([ 'id_penugasan' => $penugasan->id_penugasan, 'id_tarif' => $tarif ? $tarif->id_tarif : null, 'jenis_pekerjaan' => $request->jenis_pekerjaan, 'dimensi_pipa' => $request->dimensi_pipa, 'jarak_meter' => $request->jarak_meter, 'jumlah_unit' => $request->jumlah_unit, 'jumlah_titik' => $request->jumlah_titik, 'pakai_pipa_besi' => $request->pakai_pipa_besi, 'jenis_pengangkatan' => $request->jenis_pengangkatan, 'total_nilai_pekerjaan' => $nilaiItem, ]); $total = \App\Models\PenugasanItem::where('id_penugasan', $penugasan->id_penugasan) ->sum('total_nilai_pekerjaan'); $penugasan->total_nilai_pekerjaan = $total; $penugasan->save(); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Rincian pekerjaan berhasil ditambahkan!', 'data' => [ 'nilai_item' => $nilaiItem, 'total_baru' => $penugasan->total_nilai_pekerjaan ] ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal menambah rincian: ' . $e->getMessage() ], 500); } } /** * PUT - Update status pekerjaan * * ✅ FIX: Ketika status diubah menjadi 'selesai', semua anggota tim * otomatis dicatat status_kehadiran = 'hadir' di tabel tim_teknisi_penugasans. * Ini memastikan semua anggota tim terhitung gajinya meskipun * bukan dia yang menerima/menceklis tugas di awal. */ public function updateStatus(Request $request, $id) { try { $penugasan = Penugasan::findOrFail($id); $validator = Validator::make($request->all(), [ 'status_pekerjaan' => 'required|in:belum_mulai,dalam_proses,selesai,dibatalkan', 'tanggal_diselesaikan' => 'required_if:status_pekerjaan,selesai|nullable|date', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } DB::beginTransaction(); $updateData = ['status_pekerjaan' => $request->status_pekerjaan]; if ($request->status_pekerjaan === 'selesai') { $updateData['tanggal_diselesaikan'] = $request->tanggal_diselesaikan ?? now(); // ✅ FIX: Pastikan semua anggota tim tercatat hadir // sehingga gaji semua anggota tim terhitung dengan benar TimTeknisiPenugasan::where('id_penugasan', $penugasan->id_penugasan) ->update(['status_kehadiran' => 'hadir']); // Recalculate total_nilai_pekerjaan if it's empty or zero if (empty($penugasan->total_nilai_pekerjaan) || $penugasan->total_nilai_pekerjaan <= 0) { $penugasan->load(['items.tarif']); $totalNilai = 0; if ($penugasan->items && $penugasan->items->count() > 0) { foreach ($penugasan->items as $item) { $itemTotal = (float) $item->total_nilai_pekerjaan; if ($itemTotal <= 0) { $tarif = $item->tarif; if (!$tarif) { $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $item->jenis_pekerjaan) ->where('is_active', true); if ($item->dimensi_pipa) { $tarifQuery->where('dimensi_pipa', $item->dimensi_pipa); } $tarif = $tarifQuery->first(); } $itemTotal = $this->hitungNilaiItem($tarif, $item->toArray()); if ($itemTotal > 0) { $item->update(['total_nilai_pekerjaan' => $itemTotal]); } } $totalNilai += $itemTotal; } } if ($totalNilai <= 0 && $penugasan->jenis_pekerjaan) { $tarif = $penugasan->tarif; if (!$tarif) { $tarifQuery = TarifPekerjaan::where('jenis_pekerjaan', $penugasan->jenis_pekerjaan) ->where('is_active', true); if ($penugasan->dimensi_pipa) { $tarifQuery->where('dimensi_pipa', $penugasan->dimensi_pipa); } $tarif = $tarifQuery->first(); } if ($tarif) { if ($penugasan->jarak_meter > 0 && $tarif->tarif_per_meter) { $totalNilai = (float) $tarif->tarif_per_meter * (float) $penugasan->jarak_meter; } elseif ($penugasan->jumlah_unit > 0 && $tarif->tarif_per_unit) { $totalNilai = (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_unit; } elseif ($penugasan->jumlah_titik > 0 && $tarif->tarif_per_unit) { $totalNilai = (float) $tarif->tarif_per_unit * (int) $penugasan->jumlah_titik; } else { $totalNilai = (float) ($tarif->tarif_per_unit ?? $tarif->tarif_per_meter ?? 0); } } } if ($totalNilai > 0) { $updateData['total_nilai_pekerjaan'] = $totalNilai; } } } $penugasan->update($updateData); DB::commit(); return response()->json([ 'success' => true, 'message' => 'Status pekerjaan berhasil diupdate!', 'data' => $penugasan ]); } catch (Exception $e) { DB::rollBack(); return response()->json([ 'success' => false, 'message' => 'Gagal update status: ' . $e->getMessage() ], 500); } } /** * POST - Upload foto sebelum/sesudah */ public function uploadFoto(Request $request, $id) { try { $penugasan = Penugasan::findOrFail($id); \Illuminate\Support\Facades\Log::info('Upload Foto Request Received', [ 'id_penugasan' => $id, 'tipe_foto' => $request->tipe_foto, 'has_file' => $request->hasFile('foto'), ]); $validator = Validator::make($request->all(), [ 'tipe_foto' => 'required|in:sebelum,sesudah', 'foto' => 'nullable|image|mimes:jpeg,png,jpg|max:10240', 'sebelum_base64' => 'nullable|string', 'sesudah_base64' => 'nullable|string', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Validasi gagal', 'errors' => $validator->errors() ], 422); } $tipeFoto = $request->tipe_foto; $fotoPath = null; if ($request->hasFile('foto')) { $fotoPath = $request->file('foto')->store("penugasan/foto-{$tipeFoto}", 'public'); } elseif ($request->input($tipeFoto . '_base64')) { $fotoPath = $this->storeBase64($request->input($tipeFoto . '_base64'), "penugasan/foto-{$tipeFoto}"); } if (!$fotoPath) { return response()->json([ 'success' => false, 'message' => 'Tidak ada foto yang diupload' ], 422); } if ($tipeFoto === 'sebelum') { $penugasan->foto_sebelum = $fotoPath; } else { $penugasan->foto_sesudah = $fotoPath; } $penugasan->save(); return response()->json([ 'success' => true, 'message' => "Foto {$tipeFoto} berhasil diupload!", 'data' => [ 'foto_url' => asset("storage/{$fotoPath}"), 'foto_path' => $fotoPath ] ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal upload foto: ' . $e->getMessage() ], 500); } } /** * GET - Tarif berdasarkan jenis pekerjaan */ public function getTarifByJenis(Request $request) { try { $validator = Validator::make($request->all(), [ 'jenis_pekerjaan' => 'required|in:sr,pengembangan_jaringan_pipa,pengangkatan,pemasangan_gate_valve,gali_urug,perbaikan_jaringan_pipa,pengecatan_pipa_besi,penyempurnaan_jaringan_pipa', ]); if ($validator->fails()) { return response()->json([ 'success' => false, 'message' => 'Jenis pekerjaan tidak valid', 'errors' => $validator->errors() ], 422); } $tarifs = TarifPekerjaan::where('jenis_pekerjaan', $request->jenis_pekerjaan) ->where('is_active', true) ->get(); return response()->json([ 'success' => true, 'message' => 'Data tarif berhasil diambil', 'data' => $tarifs ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal mengambil data tarif: ' . $e->getMessage() ], 500); } } /** * GET - Statistik penugasan teknisi */ public function statistik(Request $request) { try { $idTeknisi = $request->input('id_teknisi'); if (!$idTeknisi) { return response()->json([ 'success' => false, 'message' => 'ID Teknisi tidak ditemukan' ], 401); } $baseQuery = Penugasan::where(function ($q) use ($idTeknisi) { $q->where('id_teknisi', $idTeknisi) ->orWhereHas('timTeknisi', function ($sq) use ($idTeknisi) { $sq->where('id_teknisi', $idTeknisi); }); }); $statistik = [ 'total_penugasan' => (clone $baseQuery)->count(), 'belum_mulai' => (clone $baseQuery)->where('status_pekerjaan', 'belum_mulai')->count(), 'dalam_proses' => (clone $baseQuery)->where('status_pekerjaan', 'dalam_proses')->count(), 'selesai' => (clone $baseQuery)->where('status_pekerjaan', 'selesai')->count(), 'menunggu_detail' => (clone $baseQuery)->whereNull('jenis_pekerjaan')->count(), 'detail_lengkap' => (clone $baseQuery)->whereNotNull('jenis_pekerjaan')->count(), ]; return response()->json([ 'success' => true, 'message' => 'Statistik berhasil diambil', 'data' => $statistik ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal mengambil statistik: ' . $e->getMessage() ], 500); } } /** * GET - Daftar teknisi aktif untuk tambah tim */ public function getTeknisiList() { try { $teknisi = Teknisi::where('status', 'aktif') ->orderBy('nama') ->get(['id_teknisi', 'nama', 'no_telepon', 'spesialisasi']); return response()->json([ 'success' => true, 'message' => 'Daftar teknisi berhasil diambil', 'data' => $teknisi ]); } catch (Exception $e) { return response()->json([ 'success' => false, 'message' => 'Gagal mengambil data teknisi: ' . $e->getMessage() ], 500); } } // =================================== // PRIVATE HELPER // =================================== private function hitungNilaiItem($tarif, array $data): float { if (!$tarif) return 0; if ($tarif->tarif_per_meter && !empty($data['jarak_meter'])) { return (float)$tarif->tarif_per_meter * (float)$data['jarak_meter']; } if ($tarif->tarif_per_unit && !empty($data['jumlah_unit'])) { return (float)$tarif->tarif_per_unit * (int)$data['jumlah_unit']; } if ($tarif->tarif_per_unit && !empty($data['jumlah_titik'])) { return (float)$tarif->tarif_per_unit * (int)$data['jumlah_titik']; } return (float)($tarif->tarif_per_unit ?? 0); } private function storeBase64($base64String, $folder) { try { if (preg_match("/^data:image\/(\w+);base64,/", $base64String, $type)) { $base64String = substr($base64String, strpos($base64String, ",") + 1); $type = strtolower($type[1]); } else { $type = "jpg"; } $image = base64_decode($base64String); if ($image === false) return null; $fileName = \Illuminate\Support\Str::random(40) . "." . $type; $path = $folder . "/" . $fileName; \Illuminate\Support\Facades\Storage::disk("public")->put($path, $image); return $path; } catch (Exception $e) { \Illuminate\Support\Facades\Log::error("Base64 Store Error: " . $e->getMessage()); return null; } } }