215 lines
7.5 KiB
Dart
215 lines
7.5 KiB
Dart
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, String>? q]) =>
|
|
Uri.parse('${settings.baseUrl}$p').replace(queryParameters: q);
|
|
|
|
Map<String, String> get _h => {
|
|
'Content-Type': 'application/json',
|
|
if ((settings.token ?? '').isNotEmpty) 'Authorization': 'Bearer ${settings.token}',
|
|
};
|
|
|
|
// ---- auth ----
|
|
Future<Profile> 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<String, dynamic>);
|
|
await settings.saveSession(token: body['accessToken'] as String, name: p.fullName, role: p.role);
|
|
return p;
|
|
}
|
|
|
|
// ---- me ----
|
|
Future<Profile> profile() async => Profile.fromJson(await _get('/api/v1/me/profile'));
|
|
Future<Today> today() async => Today.fromJson(await _get('/api/v1/me/attendance/today'));
|
|
|
|
Future<void> checkIn({double? lat, double? lng, String? selfie}) =>
|
|
_post('/api/v1/me/attendance/check-in', _geo(lat, lng, selfie));
|
|
Future<void> checkOut({double? lat, double? lng, String? selfie}) =>
|
|
_post('/api/v1/me/attendance/check-out', _geo(lat, lng, selfie));
|
|
Map<String, dynamic> _geo(double? lat, double? lng, String? selfie) => {
|
|
if (lat != null) 'lat': lat,
|
|
if (lng != null) 'lng': lng,
|
|
if (selfie != null) 'selfie': selfie,
|
|
};
|
|
|
|
Future<List<LeaveType>> leaveTypes() async =>
|
|
(await _get('/api/v1/me/leave/types') as List).map((e) => LeaveType.fromJson(e)).toList();
|
|
Future<List<LeaveBalance>> leaveBalances() async =>
|
|
(await _get('/api/v1/me/leave/balances') as List).map((e) => LeaveBalance.fromJson(e)).toList();
|
|
Future<List<LeaveRequest>> myLeave() async =>
|
|
(await _get('/api/v1/me/leave/requests') as List).map((e) => LeaveRequest.fromJson(e)).toList();
|
|
Future<void> applyLeave(String typeGid, String from, String to, String reason) =>
|
|
_post('/api/v1/me/leave/requests', {'leaveTypeGid': typeGid, 'fromDate': from, 'toDate': to, 'reason': reason});
|
|
|
|
Future<List<Payslip>> payslips() async =>
|
|
(await _get('/api/v1/me/payslips') as List).map((e) => Payslip.fromJson(e)).toList();
|
|
|
|
Future<List<TeamMember>> team() async =>
|
|
(await _get('/api/v1/me/team') as List).map((e) => TeamMember.fromJson(e)).toList();
|
|
Future<List<LeaveRequest>> teamLeave() async =>
|
|
(await _get('/api/v1/me/team/leave') as List).map((e) => LeaveRequest.fromJson(e)).toList();
|
|
Future<void> approve(String gid) => _post('/api/v1/me/team/leave/$gid/approve', {});
|
|
Future<void> reject(String gid) => _post('/api/v1/me/team/leave/$gid/reject', {});
|
|
|
|
// ---- helpers ----
|
|
Future<dynamic> _get(String p, [Map<String, String>? 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<dynamic> _post(String p, Map<String, dynamic> 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;
|
|
}
|