363 lines
13 KiB
Dart
363 lines
13 KiB
Dart
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
|
class AbsensiApi {
|
|
static const String baseUrl = 'https://ta.myhost.id/E31230906/api';
|
|
|
|
static const String absenMasukEndpoint = '/absensi/absen-masuk';
|
|
static const String absenKeluarEndpoint = '/absensi/absen-keluar';
|
|
static const String checkStatusEndpoint = '/absensi/check-status';
|
|
static const String riwayatEndpoint = '/absensi/riwayat';
|
|
static const String kalenderEndpoint = '/absensi/kalender';
|
|
static const String rekapEndpoint = '/absensi/rekap';
|
|
static const Duration timeoutDuration = Duration(seconds: 30);
|
|
|
|
Future<String?> _getToken() async {
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return prefs.getString('access_token');
|
|
} catch (e) {
|
|
print('Error getting token: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<Map<String, String>> _getMultipartHeaders() async {
|
|
final token = await _getToken();
|
|
return {
|
|
'Accept': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
}
|
|
|
|
Future<Map<String, String>> _getHeaders() async {
|
|
final token = await _getToken();
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
}
|
|
|
|
/// Absen masuk dengan support Web & Mobile
|
|
Future<Map<String, dynamic>> absenMasuk({
|
|
required int idTeknisi,
|
|
XFile? fotoAbsenMasuk,
|
|
String? status,
|
|
String? keterangan,
|
|
double? latitude,
|
|
double? longitude,
|
|
}) async {
|
|
try {
|
|
print('=== ABSEN MASUK REQUEST ===');
|
|
print('ID Teknisi: $idTeknisi');
|
|
print('Status: ${status ?? "null"}');
|
|
print('Keterangan: ${keterangan ?? "null"}');
|
|
print('Foto: ${fotoAbsenMasuk != null ? "Ada (${fotoAbsenMasuk.name})" : "TIDAK ADA"}');
|
|
|
|
var request = http.MultipartRequest(
|
|
'POST',
|
|
Uri.parse('$baseUrl$absenMasukEndpoint'),
|
|
);
|
|
|
|
request.headers.addAll(await _getMultipartHeaders());
|
|
request.fields['id_teknisi'] = idTeknisi.toString();
|
|
|
|
if (status != null && status.isNotEmpty) {
|
|
request.fields['status'] = status;
|
|
}
|
|
|
|
if (keterangan != null && keterangan.isNotEmpty) {
|
|
request.fields['keterangan'] = keterangan;
|
|
}
|
|
|
|
if (latitude != null) {
|
|
request.fields['latitude'] = latitude.toString();
|
|
}
|
|
|
|
if (longitude != null) {
|
|
request.fields['longitude'] = longitude.toString();
|
|
}
|
|
|
|
if (fotoAbsenMasuk != null) {
|
|
if (kIsWeb) {
|
|
final bytes = await fotoAbsenMasuk.readAsBytes();
|
|
print('📸 Foto size (Web): ${bytes.length} bytes');
|
|
request.files.add(http.MultipartFile.fromBytes(
|
|
'foto_absen_masuk', bytes,
|
|
filename: fotoAbsenMasuk.name,
|
|
));
|
|
} else {
|
|
print('📸 Foto path (Mobile): ${fotoAbsenMasuk.path}');
|
|
request.files.add(await http.MultipartFile.fromPath(
|
|
'foto_absen_masuk', fotoAbsenMasuk.path,
|
|
));
|
|
}
|
|
}
|
|
|
|
print('📤 Sending request to: $baseUrl$absenMasukEndpoint');
|
|
final streamedResponse = await request.send().timeout(timeoutDuration);
|
|
final response = await http.Response.fromStream(streamedResponse);
|
|
|
|
print('=== ABSEN MASUK RESPONSE ===');
|
|
print('Status Code: ${response.statusCode}');
|
|
print('Response Body: ${response.body}');
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 201 && data['success'] == true) {
|
|
print('✅ ABSEN MASUK SUKSES!');
|
|
return {'success': true, 'message': data['message'], 'data': data['data']};
|
|
} else if (response.statusCode == 400) {
|
|
print('❌ Error 400: ${data['message']}');
|
|
return {'success': false,
|
|
'message': data['message'] ?? 'Anda sudah melakukan absen masuk hari ini'};
|
|
} else if (response.statusCode == 422) {
|
|
print('❌ Validation Error: ${data['errors']}');
|
|
String errorMessage = 'Validasi gagal';
|
|
if (data['errors'] != null) {
|
|
final errors = data['errors'] as Map<String, dynamic>;
|
|
errorMessage = errors.values.first[0];
|
|
}
|
|
return {'success': false, 'message': errorMessage, 'errors': data['errors']};
|
|
} else {
|
|
print('❌ Unknown Error: ${response.statusCode}');
|
|
return {'success': false,
|
|
'message': data['message'] ?? 'Gagal melakukan absen masuk'};
|
|
}
|
|
} catch (e, stackTrace) {
|
|
print('❌ EXCEPTION di absenMasuk: $e');
|
|
print(stackTrace);
|
|
return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'};
|
|
}
|
|
}
|
|
|
|
/// Absen keluar dengan support Web & Mobile
|
|
Future<Map<String, dynamic>> absenKeluar({
|
|
required int idTeknisi,
|
|
XFile? fotoAbsenKeluar,
|
|
String? status,
|
|
String? keterangan,
|
|
double? latitude,
|
|
double? longitude,
|
|
}) async {
|
|
try {
|
|
print('=== ABSEN KELUAR REQUEST ===');
|
|
print('ID Teknisi: $idTeknisi');
|
|
print('Status: ${status ?? "null"}');
|
|
print('Keterangan: ${keterangan ?? "null"}');
|
|
print('Foto: ${fotoAbsenKeluar != null ? "Ada (${fotoAbsenKeluar.name})" : "TIDAK ADA"}');
|
|
|
|
var request = http.MultipartRequest(
|
|
'POST',
|
|
Uri.parse('$baseUrl$absenKeluarEndpoint'),
|
|
);
|
|
|
|
request.headers.addAll(await _getMultipartHeaders());
|
|
request.fields['id_teknisi'] = idTeknisi.toString();
|
|
|
|
if (status != null && status.isNotEmpty) {
|
|
request.fields['status'] = status;
|
|
}
|
|
|
|
if (keterangan != null && keterangan.isNotEmpty) {
|
|
request.fields['keterangan'] = keterangan;
|
|
}
|
|
|
|
if (latitude != null) {
|
|
request.fields['latitude'] = latitude.toString();
|
|
}
|
|
|
|
if (longitude != null) {
|
|
request.fields['longitude'] = longitude.toString();
|
|
}
|
|
|
|
if (fotoAbsenKeluar != null) {
|
|
if (kIsWeb) {
|
|
final bytes = await fotoAbsenKeluar.readAsBytes();
|
|
print('📸 Foto size (Web): ${bytes.length} bytes');
|
|
request.files.add(http.MultipartFile.fromBytes(
|
|
'foto_absen_keluar', bytes,
|
|
filename: fotoAbsenKeluar.name,
|
|
));
|
|
} else {
|
|
print('📸 Foto path (Mobile): ${fotoAbsenKeluar.path}');
|
|
request.files.add(await http.MultipartFile.fromPath(
|
|
'foto_absen_keluar', fotoAbsenKeluar.path,
|
|
));
|
|
}
|
|
}
|
|
|
|
print('📤 Sending request to: $baseUrl$absenKeluarEndpoint');
|
|
final streamedResponse = await request.send().timeout(timeoutDuration);
|
|
final response = await http.Response.fromStream(streamedResponse);
|
|
|
|
print('=== ABSEN KELUAR RESPONSE ===');
|
|
print('Status Code: ${response.statusCode}');
|
|
print('Response Body: ${response.body}');
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200 && data['success'] == true) {
|
|
print('✅ ABSEN KELUAR SUKSES!');
|
|
return {'success': true, 'message': data['message'], 'data': data['data']};
|
|
} else if (response.statusCode == 400) {
|
|
print('❌ Error 400: ${data['message']}');
|
|
return {'success': false,
|
|
'message': data['message'] ?? 'Belum melakukan absen masuk atau sudah absen keluar'};
|
|
} else if (response.statusCode == 422) {
|
|
print('❌ Validation Error: ${data['errors']}');
|
|
String errorMessage = 'Validasi gagal';
|
|
if (data['errors'] != null) {
|
|
final errors = data['errors'] as Map<String, dynamic>;
|
|
errorMessage = errors.values.first[0];
|
|
}
|
|
return {'success': false, 'message': errorMessage, 'errors': data['errors']};
|
|
} else {
|
|
print('❌ Unknown Error: ${response.statusCode}');
|
|
return {'success': false,
|
|
'message': data['message'] ?? 'Gagal melakukan absen keluar'};
|
|
}
|
|
} catch (e, stackTrace) {
|
|
print('❌ EXCEPTION di absenKeluar: $e');
|
|
print(stackTrace);
|
|
return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'};
|
|
}
|
|
}
|
|
|
|
/// Cek status absensi hari ini
|
|
Future<Map<String, dynamic>> checkStatus(int idTeknisi) async {
|
|
try {
|
|
final token = await _getToken();
|
|
final response = await http.get(
|
|
Uri.parse('$baseUrl$checkStatusEndpoint/$idTeknisi'),
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
},
|
|
).timeout(timeoutDuration);
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200 && data['success'] == true) {
|
|
return {'success': true, 'message': data['message'], 'data': data['data']};
|
|
} else if (response.statusCode == 404) {
|
|
return {'success': false, 'message': 'Teknisi tidak ditemukan'};
|
|
} else {
|
|
return {'success': false,
|
|
'message': data['message'] ?? 'Gagal mengecek status absensi'};
|
|
}
|
|
} catch (e) {
|
|
return {'success': false, 'message': 'Terjadi kesalahan: ${e.toString()}'};
|
|
}
|
|
}
|
|
|
|
// ── Method baru ────────────────────────────────────────────────────────────
|
|
|
|
/// Ambil riwayat absensi per bulan
|
|
/// Endpoint: GET /api/absensi/riwayat?id_teknisi=1&bulan=3&tahun=2026
|
|
Future<Map<String, dynamic>> getRiwayat({
|
|
required int idTeknisi,
|
|
required int bulan,
|
|
required int tahun,
|
|
}) async {
|
|
try {
|
|
final url = Uri.parse(
|
|
'$baseUrl$riwayatEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun');
|
|
print('📋 GET Riwayat: $url');
|
|
|
|
final response = await http.get(url,
|
|
headers: await _getHeaders()).timeout(timeoutDuration);
|
|
|
|
print('Riwayat status: ${response.statusCode}');
|
|
print('Riwayat body: ${response.body}');
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200 && data['success'] == true) {
|
|
return {'success': true, 'data': data['data'] ?? []};
|
|
}
|
|
return {
|
|
'success': false,
|
|
'message': data['message'] ?? 'Gagal memuat riwayat',
|
|
'data': [],
|
|
};
|
|
} catch (e) {
|
|
print('❌ getRiwayat error: $e');
|
|
return {'success': false, 'message': e.toString(), 'data': []};
|
|
}
|
|
}
|
|
|
|
/// Ambil status absensi per tanggal dalam 1 bulan (untuk kalender)
|
|
/// Endpoint: GET /api/absensi/kalender?id_teknisi=1&bulan=3&tahun=2026
|
|
/// Response data: { "1": "hadir", "5": "izin", "10": "alpha", ... }
|
|
Future<Map<String, dynamic>> getKalender({
|
|
required int idTeknisi,
|
|
required int bulan,
|
|
required int tahun,
|
|
}) async {
|
|
try {
|
|
final url = Uri.parse(
|
|
'$baseUrl$kalenderEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun');
|
|
print('📅 GET Kalender: $url');
|
|
|
|
final response = await http.get(url,
|
|
headers: await _getHeaders()).timeout(timeoutDuration);
|
|
|
|
print('Kalender status: ${response.statusCode}');
|
|
print('Kalender body: ${response.body}');
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200 && data['success'] == true) {
|
|
return {'success': true, 'data': data['data'] ?? {}};
|
|
}
|
|
return {
|
|
'success': false,
|
|
'message': data['message'] ?? 'Gagal memuat kalender',
|
|
'data': {},
|
|
};
|
|
} catch (e) {
|
|
print('❌ getKalender error: $e');
|
|
return {'success': false, 'message': e.toString(), 'data': {}};
|
|
}
|
|
}
|
|
|
|
/// Ambil rekap absensi bulanan
|
|
/// Endpoint: GET /api/absensi/rekap?id_teknisi=1&bulan=3&tahun=2026
|
|
/// Response data: { "bulan": "Maret 2026", "hadir": 14, "izin": 1, ... }
|
|
Future<Map<String, dynamic>> getRekap({
|
|
required int idTeknisi,
|
|
required int bulan,
|
|
required int tahun,
|
|
}) async {
|
|
try {
|
|
final url = Uri.parse(
|
|
'$baseUrl$rekapEndpoint?id_teknisi=$idTeknisi&bulan=$bulan&tahun=$tahun');
|
|
print('📊 GET Rekap: $url');
|
|
|
|
final response = await http.get(url,
|
|
headers: await _getHeaders()).timeout(timeoutDuration);
|
|
|
|
print('Rekap status: ${response.statusCode}');
|
|
print('Rekap body: ${response.body}');
|
|
|
|
final data = jsonDecode(response.body);
|
|
|
|
if (response.statusCode == 200 && data['success'] == true) {
|
|
return {'success': true, 'data': data['data'] ?? {}};
|
|
}
|
|
return {
|
|
'success': false,
|
|
'message': data['message'] ?? 'Gagal memuat rekap',
|
|
'data': {},
|
|
};
|
|
} catch (e) {
|
|
print('❌ getRekap error: $e');
|
|
return {'success': false, 'message': e.toString(), 'data': {}};
|
|
}
|
|
}
|
|
} |