SIPDAM/samooflutter/lib/api/PenugasanApi.dart

393 lines
15 KiB
Dart

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<String?> _getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('access_token'); // ← ganti ini
}
Future<int?> _getIdTeknisi() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('id_teknisi');
}
Future<Map<String, String>> _getHeaders() async {
final token = await _getToken();
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
};
}
Map<String, dynamic> _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<Map<String, dynamic>> getPenugasanList({
String? status,
String? jenisPekerjaan,
String? tanggalMulai,
String? tanggalAkhir,
int page = 1,
}) async {
final idTeknisi = await _getIdTeknisi();
final queryParams = <String, String>{
'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<Map<String, dynamic>> getPenugasanProgresAktif({int page = 1}) async {
return getPenugasanList(status: 'dalam_proses', page: page);
}
// ─────────────────────────────────────────
// GET - Helper: Riwayat Selesai
// ─────────────────────────────────────────
/// Shortcut untuk mengambil penugasan dengan status 'selesai'
Future<Map<String, dynamic>> getPenugasanRiwayatSelesai({int page = 1}) async {
return getPenugasanList(status: 'selesai', page: page);
}
// ─────────────────────────────────────────
// GET - Detail Penugasan
// ─────────────────────────────────────────
Future<Map<String, dynamic>> getPenugasanDetail(int id) async {
final response = await http.get(
Uri.parse('$baseUrl/penugasan/$id'),
headers: await _getHeaders(),
);
return _handleResponse(response);
}
// ─────────────────────────────────────────
// GET - Statistik
// ─────────────────────────────────────────
Future<Map<String, dynamic>> 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<Map<String, dynamic>> lengkapiDetail({
required int idPenugasan,
required List<Map<String, dynamic>> items,
required String tanggalMulai,
String? detailPekerjaan,
List<int>? 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<Map<String, dynamic>> updateDetail({
required int idPenugasan,
required int idTeknisi,
required List<Map<String, dynamic>> items,
// Selalu kirim detail_pekerjaan (default string kosong, bukan null)
String detailPekerjaan = '',
String? tanggalMulai,
List<int>? timTeknisi,
}) async {
// Bersihkan nilai null di dalam setiap item sebelum encode JSON
final cleanedItems = items.map((item) {
final cleaned = <String, dynamic>{};
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 = <String, dynamic>{
'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<Map<String, dynamic>> addItem({
required int idPenugasan,
required Map<String, dynamic> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<String, dynamic>? 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;
}
}