import 'dart:io'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:image_picker/image_picker.dart'; class PenugasanApi { static const String baseUrl = 'https://ta.myhost.id/E31230906/api'; static final PenugasanApi _instance = PenugasanApi._internal(); factory PenugasanApi() => _instance; PenugasanApi._internal(); // ───────────────────────────────────────── // PRIVATE HELPERS // ───────────────────────────────────────── Future _getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('access_token'); // ← ganti ini } Future _getIdTeknisi() async { final prefs = await SharedPreferences.getInstance(); return prefs.getInt('id_teknisi'); } Future> _getHeaders() async { final token = await _getToken(); return { 'Accept': 'application/json', 'Content-Type': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }; } Map _handleResponse(http.Response response) { // Log response di mode debug if (const bool.fromEnvironment('dart.vm.product') == false) { print('[PenugasanApi] ${response.request?.method} ${response.request?.url}'); print('[PenugasanApi] Status: ${response.statusCode}'); if (response.statusCode >= 400) { print('[PenugasanApi] Error Body: ${response.body}'); } } final data = json.decode(response.body); if (response.statusCode >= 200 && response.statusCode < 300) { return data; } throw PenugasanApiException( message: data['message'] ?? 'Terjadi kesalahan', statusCode: response.statusCode, errors: data['errors'], ); } // ───────────────────────────────────────── // GET - Daftar Penugasan // ───────────────────────────────────────── /// [status] filter: 'belum_mulai' | 'dalam_proses' | 'selesai' | 'dibatalkan' /// /// Untuk tab "Progres Aktif" → kirim status: 'dalam_proses' /// Untuk tab "Riwayat Selesai" → kirim status: 'selesai' /// Tanpa status → ambil semua Future> getPenugasanList({ String? status, String? jenisPekerjaan, String? tanggalMulai, String? tanggalAkhir, int page = 1, }) async { final idTeknisi = await _getIdTeknisi(); final queryParams = { 'page': page.toString(), if (idTeknisi != null) 'id_teknisi': idTeknisi.toString(), }; if (status != null) queryParams['status'] = status; if (jenisPekerjaan != null) queryParams['jenis_pekerjaan'] = jenisPekerjaan; if (tanggalMulai != null) queryParams['tanggal_mulai'] = tanggalMulai; if (tanggalAkhir != null) queryParams['tanggal_akhir'] = tanggalAkhir; final uri = Uri.parse('$baseUrl/penugasan') .replace(queryParameters: queryParams); final response = await http.get(uri, headers: await _getHeaders()); return _handleResponse(response); } // ───────────────────────────────────────── // GET - Helper: Progres Aktif // ───────────────────────────────────────── /// Shortcut untuk mengambil penugasan dengan status 'dalam_proses' Future> getPenugasanProgresAktif({int page = 1}) async { return getPenugasanList(status: 'dalam_proses', page: page); } // ───────────────────────────────────────── // GET - Helper: Riwayat Selesai // ───────────────────────────────────────── /// Shortcut untuk mengambil penugasan dengan status 'selesai' Future> getPenugasanRiwayatSelesai({int page = 1}) async { return getPenugasanList(status: 'selesai', page: page); } // ───────────────────────────────────────── // GET - Detail Penugasan // ───────────────────────────────────────── Future> getPenugasanDetail(int id) async { final response = await http.get( Uri.parse('$baseUrl/penugasan/$id'), headers: await _getHeaders(), ); return _handleResponse(response); } // ───────────────────────────────────────── // GET - Statistik // ───────────────────────────────────────── Future> getStatistik() async { final idTeknisi = await _getIdTeknisi(); final uri = Uri.parse('$baseUrl/penugasan/statistik').replace( queryParameters: { if (idTeknisi != null) 'id_teknisi': idTeknisi.toString(), }, ); final response = await http.get(uri, headers: await _getHeaders()); return _handleResponse(response); } // ───────────────────────────────────────── // POST - Lengkapi Detail (pertama kali) // ───────────────────────────────────────── Future> lengkapiDetail({ required int idPenugasan, required List> items, required String tanggalMulai, String? detailPekerjaan, List? timTeknisi, XFile? fotoSebelum, XFile? fotoSesudah, }) async { final token = await _getToken(); var request = http.MultipartRequest( 'POST', Uri.parse('$baseUrl/penugasan/$idPenugasan/lengkapi-detail'), ); request.headers.addAll({ 'Accept': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }); request.fields['tanggal_mulai'] = tanggalMulai; // Selalu kirim detail_pekerjaan (meski kosong) // agar Laravel menyimpan nilai terbaru via $request->has() request.fields['detail_pekerjaan'] = detailPekerjaan ?? ''; for (int i = 0; i < items.length; i++) { final item = items[i]; request.fields['items[$i][jenis_pekerjaan]'] = item['jenis_pekerjaan']?.toString() ?? ''; if (item['dimensi_pipa'] != null) request.fields['items[$i][dimensi_pipa]'] = item['dimensi_pipa'].toString(); if (item['jarak_meter'] != null) request.fields['items[$i][jarak_meter]'] = item['jarak_meter'].toString(); if (item['jumlah_unit'] != null) request.fields['items[$i][jumlah_unit]'] = item['jumlah_unit'].toString(); if (item['jumlah_titik'] != null) request.fields['items[$i][jumlah_titik]'] = item['jumlah_titik'].toString(); if (item['pakai_pipa_besi'] != null) request.fields['items[$i][pakai_pipa_besi]'] = item['pakai_pipa_besi'] ? '1' : '0'; if (item['jenis_pengangkatan'] != null) request.fields['items[$i][jenis_pengangkatan]'] = item['jenis_pengangkatan'].toString(); } if (timTeknisi != null) { for (int i = 0; i < timTeknisi.length; i++) { request.fields['tim_teknisi[$i]'] = timTeknisi[i].toString(); } } // ✅ Upload foto sebelum (jika ada) if (fotoSebelum != null) { final bytes = await fotoSebelum.readAsBytes(); request.fields['foto_sebelum_base64'] = 'data:image/jpeg;base64,' + base64Encode(bytes); } // ✅ Upload foto sesudah (jika ada) if (fotoSesudah != null) { final bytes = await fotoSesudah.readAsBytes(); request.fields['foto_sesudah_base64'] = 'data:image/jpeg;base64,' + base64Encode(bytes); } final streamed = await request.send(); final response = await http.Response.fromStream(streamed); return _handleResponse(response); } // ───────────────────────────────────────── // PUT - Update Detail (edit yang sudah ada) // ───────────────────────────────────────── Future> updateDetail({ required int idPenugasan, required int idTeknisi, required List> items, // Selalu kirim detail_pekerjaan (default string kosong, bukan null) String detailPekerjaan = '', String? tanggalMulai, List? timTeknisi, }) async { // Bersihkan nilai null di dalam setiap item sebelum encode JSON final cleanedItems = items.map((item) { final cleaned = {}; item.forEach((key, value) { // id_penugasan_item & jenis_pekerjaan selalu dikirim meski null if (key == 'id_penugasan_item' || key == 'jenis_pekerjaan') { cleaned[key] = value; } else if (value != null) { cleaned[key] = value; } }); return cleaned; }).toList(); final body = { 'id_teknisi': idTeknisi, 'items': cleanedItems, // Selalu kirim detail_pekerjaan agar backend menyimpan nilai terbaru 'detail_pekerjaan': detailPekerjaan, if (tanggalMulai != null) 'tanggal_mulai': tanggalMulai, if (timTeknisi != null) 'tim_teknisi': timTeknisi, }; final response = await http.put( Uri.parse('$baseUrl/penugasan/$idPenugasan/update-detail'), headers: await _getHeaders(), body: json.encode(body), ); return _handleResponse(response); } // ───────────────────────────────────────── // POST - Tambah Item Progres // ───────────────────────────────────────── Future> addItem({ required int idPenugasan, required Map item, }) async { final response = await http.post( Uri.parse('$baseUrl/penugasan/$idPenugasan/add-item'), headers: await _getHeaders(), body: json.encode(item), ); return _handleResponse(response); } // ───────────────────────────────────────── // PUT - Update Status // ───────────────────────────────────────── Future> updateStatus({ required int idPenugasan, required String statusPekerjaan, String? tanggalDiselesaikan, }) async { final response = await http.put( Uri.parse('$baseUrl/penugasan/$idPenugasan/update-status'), headers: await _getHeaders(), body: json.encode({ 'status_pekerjaan': statusPekerjaan, if (tanggalDiselesaikan != null) 'tanggal_diselesaikan': tanggalDiselesaikan, }), ); return _handleResponse(response); } // ───────────────────────────────────────── // POST - Upload Foto Sebelum / Sesudah // ───────────────────────────────────────── /// [tipeFoto] : 'sebelum' atau 'sesudah' /// /// Field yang dikirim ke backend: /// - tipe_foto : 'sebelum' | 'sesudah' ← wajib (required di Laravel) /// - sebelum_base64 : base64 string ← jika tipeFoto == 'sebelum' /// - sesudah_base64 : base64 string ← jika tipeFoto == 'sesudah' Future> uploadFoto({ required int idPenugasan, required String tipeFoto, // 'sebelum' atau 'sesudah' required XFile foto, }) async { assert( tipeFoto == 'sebelum' || tipeFoto == 'sesudah', 'tipeFoto harus "sebelum" atau "sesudah"', ); final token = await _getToken(); var request = http.MultipartRequest( 'POST', Uri.parse('$baseUrl/penugasan/$idPenugasan/upload-foto'), ); request.headers.addAll({ 'Accept': 'application/json', if (token != null) 'Authorization': 'Bearer $token', }); // ✅ Field wajib: tipe_foto — dipakai backend untuk validasi & routing request.fields['tipe_foto'] = tipeFoto; // ✅ Field base64 dinamis sesuai tipe: // tipeFoto='sebelum' → key: 'sebelum_base64' // tipeFoto='sesudah' → key: 'sesudah_base64' final bytes = await foto.readAsBytes(); request.fields['${tipeFoto}_base64'] = 'data:image/jpeg;base64,' + base64Encode(bytes); final streamed = await request.send(); final response = await http.Response.fromStream(streamed); return _handleResponse(response); } // ───────────────────────────────────────── // GET - Tarif by Jenis Pekerjaan // ───────────────────────────────────────── Future> getTarifByJenis(String jenisPekerjaan) async { final response = await http.get( Uri.parse( '$baseUrl/penugasan/master/tarif-by-jenis?jenis_pekerjaan=$jenisPekerjaan'), headers: await _getHeaders(), ); return _handleResponse(response); } // ───────────────────────────────────────── // GET - List Teknisi // ───────────────────────────────────────── Future> getTeknisiList() async { final response = await http.get( Uri.parse('$baseUrl/penugasan/master/teknisi-list'), headers: await _getHeaders(), ); return _handleResponse(response); } } // =================================== // EXCEPTION // =================================== class PenugasanApiException implements Exception { final String message; final int? statusCode; final Map? errors; PenugasanApiException({ required this.message, this.statusCode, this.errors, }); @override String toString() => 'PenugasanApiException: $message (Status: $statusCode)'; /// Ambil pesan error pertama dari field validation errors String? getFirstError() { if (errors == null) return null; for (var key in errors!.keys) { final val = errors![key]; if (val is List && val.isNotEmpty) return val.first.toString(); } return null; } }