import 'dart:convert'; import 'package:http/http.dart' as http; import 'settings.dart'; class ApiException implements Exception { final String message; final int? status; ApiException(this.message, [this.status]); @override String toString() => message; } class Profile { final String gid, fullName, role; final String? employeeCode, departmentName, designationTitle, reportingManagerName; final bool isManager; Profile.fromJson(Map j) : gid = j['gid'] ?? '', fullName = j['fullName'] ?? '', role = j['role'] ?? 'EMPLOYEE', employeeCode = j['employeeCode'], departmentName = j['departmentName'], designationTitle = j['designationTitle'], reportingManagerName = j['reportingManagerName'], isManager = j['isManager'] == true; } class Today { final String? status, checkIn, checkOut, date; final num? hours, otHours; final int? lateMinutes; final bool checkedIn; Today.fromJson(Map j) : status = j['status'], checkIn = j['checkIn'], checkOut = j['checkOut'], date = j['date'], hours = j['hours'], otHours = j['otHours'], lateMinutes = j['lateMinutes'], checkedIn = j['checkedIn'] == true; } class LeaveBalance { final String leaveTypeGid, leaveTypeName; final bool paid; final num allocated, used, available; LeaveBalance.fromJson(Map j) : leaveTypeGid = j['leaveTypeGid'] ?? '', leaveTypeName = j['leaveTypeName'] ?? '', paid = j['paid'] == true, allocated = j['allocated'] ?? 0, used = j['used'] ?? 0, available = j['available'] ?? 0; } class LeaveType { final String gid, name; final bool paid; LeaveType.fromJson(Map j) : gid = j['gid'] ?? '', name = j['name'] ?? '', paid = j['paid'] == true; } class LeaveRequest { final String gid, status; final String? leaveTypeName, fromDate, toDate, reason, employeeName; final num? days; LeaveRequest.fromJson(Map j) : gid = j['gid'] ?? '', status = j['status'] ?? '', leaveTypeName = j['leaveTypeName'], fromDate = j['fromDate'], toDate = j['toDate'], reason = j['reason'], employeeName = j['employeeName'], days = j['days']; } class Payslip { final String gid, periodMonth, status; final num gross, totalDeductions, netPay; Payslip.fromJson(Map j) : gid = j['gid'] ?? '', periodMonth = j['periodMonth'] ?? '', status = j['status'] ?? '', gross = j['gross'] ?? 0, totalDeductions = j['totalDeductions'] ?? 0, netPay = j['netPay'] ?? 0; } class TeamMember { final String employeeGid, employeeName, status; final String? employeeCode, checkIn, checkOut; final num? hours; final bool checkedIn; TeamMember.fromJson(Map j) : employeeGid = j['employeeGid'] ?? '', employeeName = j['employeeName'] ?? '', status = j['status'] ?? '—', employeeCode = j['employeeCode'], checkIn = j['checkIn'], checkOut = j['checkOut'], hours = j['hours'], checkedIn = j['checkedIn'] == true; } class ApiService { final Settings settings; ApiService(this.settings); Uri _uri(String p, [Map? q]) => Uri.parse('${settings.baseUrl}$p').replace(queryParameters: q); Map get _h => { 'Content-Type': 'application/json', if ((settings.token ?? '').isNotEmpty) 'Authorization': 'Bearer ${settings.token}', }; // ---- auth ---- Future login(String identifier, String pin) async { http.Response res; try { res = await http .post(_uri('/api/v1/employee/auth/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'identifier': identifier.trim(), 'pin': pin})) .timeout(const Duration(seconds: 20)); } catch (_) { throw ApiException('Cannot reach server. Check the URL and connection.'); } final body = _json(res); if (res.statusCode != 200 || body is! Map) { throw ApiException(_msg(body) ?? 'Invalid email or PIN', res.statusCode); } final p = Profile.fromJson(body['profile'] as Map); await settings.saveSession(token: body['accessToken'] as String, name: p.fullName, role: p.role); return p; } // ---- me ---- Future profile() async => Profile.fromJson(await _get('/api/v1/me/profile')); Future today() async => Today.fromJson(await _get('/api/v1/me/attendance/today')); Future checkIn({double? lat, double? lng, String? selfie}) => _post('/api/v1/me/attendance/check-in', _geo(lat, lng, selfie)); Future checkOut({double? lat, double? lng, String? selfie}) => _post('/api/v1/me/attendance/check-out', _geo(lat, lng, selfie)); Map _geo(double? lat, double? lng, String? selfie) => { if (lat != null) 'lat': lat, if (lng != null) 'lng': lng, if (selfie != null) 'selfie': selfie, }; Future> leaveTypes() async => (await _get('/api/v1/me/leave/types') as List).map((e) => LeaveType.fromJson(e)).toList(); Future> leaveBalances() async => (await _get('/api/v1/me/leave/balances') as List).map((e) => LeaveBalance.fromJson(e)).toList(); Future> myLeave() async => (await _get('/api/v1/me/leave/requests') as List).map((e) => LeaveRequest.fromJson(e)).toList(); Future applyLeave(String typeGid, String from, String to, String reason) => _post('/api/v1/me/leave/requests', {'leaveTypeGid': typeGid, 'fromDate': from, 'toDate': to, 'reason': reason}); Future> payslips() async => (await _get('/api/v1/me/payslips') as List).map((e) => Payslip.fromJson(e)).toList(); Future> team() async => (await _get('/api/v1/me/team') as List).map((e) => TeamMember.fromJson(e)).toList(); Future> teamLeave() async => (await _get('/api/v1/me/team/leave') as List).map((e) => LeaveRequest.fromJson(e)).toList(); Future approve(String gid) => _post('/api/v1/me/team/leave/$gid/approve', {}); Future reject(String gid) => _post('/api/v1/me/team/leave/$gid/reject', {}); // ---- helpers ---- Future _get(String p, [Map? q]) async { http.Response r; try { r = await http.get(_uri(p, q), headers: _h).timeout(const Duration(seconds: 20)); } catch (_) { throw ApiException('Network error'); } return _handle(r); } Future _post(String p, Map body) async { http.Response r; try { r = await http.post(_uri(p), headers: _h, body: jsonEncode(body)).timeout(const Duration(seconds: 20)); } catch (_) { throw ApiException('Network error'); } return _handle(r); } dynamic _handle(http.Response r) { if (r.statusCode == 401) throw ApiException('Session expired. Please sign in again.', 401); if (r.statusCode == 204 || r.body.isEmpty) return null; final body = _json(r); if (r.statusCode >= 400) throw ApiException(_msg(body) ?? 'Request failed (${r.statusCode})', r.statusCode); if (body is Map && body.containsKey('data') && body.containsKey('success')) return body['data']; return body; } dynamic _json(http.Response r) { try { return jsonDecode(r.body); } catch (_) { return null; } } String? _msg(dynamic b) => (b is Map && b['message'] is String) ? b['message'] as String : null; }