393 lines
15 KiB
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;
|
|
}
|
|
} |