- Implemented ContactProfileResponse and related models for handling contact details. - Created ContactTagResponse and ContactTag models for managing contact tags. - Added DirectoryCommentResponse and DirectoryComment models for comment management. - Developed DirectoryFilterBottomSheet for filtering contacts. - Introduced OrganizationListModel for organization data handling. - Updated routes to include DirectoryMainScreen. - Enhanced DashboardScreen to navigate to the new directory page. - Created ContactDetailScreen for displaying detailed contact information. - Developed DirectoryMainScreen for managing and displaying contacts. - Added dependencies for font_awesome_flutter and flutter_html in pubspec.yaml.
597 lines
19 KiB
Dart
597 lines
19 KiB
Dart
import 'dart:convert';
|
|
import 'package:get/get.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
import 'package:marco/helpers/services/auth_service.dart';
|
|
import 'package:marco/helpers/services/api_endpoints.dart';
|
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
|
|
|
import 'package:marco/helpers/services/app_logger.dart';
|
|
|
|
class ApiService {
|
|
static const Duration timeout = Duration(seconds: 30);
|
|
static const bool enableLogs = true;
|
|
static const Duration extendedTimeout = Duration(seconds: 60);
|
|
|
|
static Future<String?> _getToken() async {
|
|
final token = await LocalStorage.getJwtToken();
|
|
|
|
if (token == null) {
|
|
logSafe("No JWT token found.");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (JwtDecoder.isExpired(token)) {
|
|
logSafe("Access token is expired. Attempting refresh...");
|
|
final refreshed = await AuthService.refreshToken();
|
|
if (refreshed) {
|
|
return await LocalStorage.getJwtToken();
|
|
} else {
|
|
logSafe("Token refresh failed. Logging out...");
|
|
await LocalStorage.logout();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
final expirationDate = JwtDecoder.getExpirationDate(token);
|
|
final now = DateTime.now();
|
|
final difference = expirationDate.difference(now);
|
|
|
|
if (difference.inMinutes < 2) {
|
|
logSafe(
|
|
"Access token is about to expire in ${difference.inSeconds}s. Refreshing...");
|
|
final refreshed = await AuthService.refreshToken();
|
|
if (refreshed) {
|
|
return await LocalStorage.getJwtToken();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logSafe("Token decoding error: $e", level: LogLevel.error);
|
|
}
|
|
|
|
return token;
|
|
}
|
|
|
|
static Map<String, String> _headers(String token) => {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $token',
|
|
};
|
|
|
|
static void _log(String message) {
|
|
if (enableLogs) logSafe(message);
|
|
}
|
|
|
|
static dynamic _parseResponse(http.Response response, {String label = ''}) {
|
|
_log("$label Response: ${response.body}");
|
|
try {
|
|
final json = jsonDecode(response.body);
|
|
if (response.statusCode == 200 && json['success'] == true) {
|
|
return json['data'];
|
|
}
|
|
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
|
} catch (e) {
|
|
_log("Response parsing error [$label]: $e");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static dynamic _parseResponseForAllData(http.Response response,
|
|
{String label = ''}) {
|
|
_log("$label Response: ${response.body}");
|
|
|
|
try {
|
|
final body = response.body.trim();
|
|
if (body.isEmpty) throw FormatException("Empty response body");
|
|
|
|
final json = jsonDecode(body);
|
|
if (response.statusCode == 200 && json['success'] == true) {
|
|
return json;
|
|
}
|
|
|
|
_log("API Error [$label]: ${json['message'] ?? 'Unknown error'}");
|
|
} catch (e) {
|
|
_log("Response parsing error [$label]: $e");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static Future<http.Response?> _getRequest(
|
|
String endpoint, {
|
|
Map<String, String>? queryParams,
|
|
bool hasRetried = false,
|
|
}) async {
|
|
String? token = await _getToken();
|
|
if (token == null) return null;
|
|
|
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
|
.replace(queryParameters: queryParams);
|
|
logSafe("GET $uri");
|
|
|
|
try {
|
|
final response =
|
|
await http.get(uri, headers: _headers(token)).timeout(timeout);
|
|
if (response.statusCode == 401 && !hasRetried) {
|
|
logSafe("Unauthorized. Attempting token refresh...");
|
|
if (await AuthService.refreshToken()) {
|
|
return await _getRequest(endpoint,
|
|
queryParams: queryParams, hasRetried: true);
|
|
}
|
|
logSafe("Token refresh failed.");
|
|
}
|
|
return response;
|
|
} catch (e) {
|
|
logSafe("HTTP GET Exception: $e", level: LogLevel.error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Future<http.Response?> _postRequest(
|
|
String endpoint,
|
|
dynamic body, {
|
|
Duration customTimeout = timeout,
|
|
bool hasRetried = false,
|
|
}) async {
|
|
String? token = await _getToken();
|
|
if (token == null) return null;
|
|
|
|
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint");
|
|
logSafe("POST $uri\nHeaders: ${_headers(token)}\nBody: $body",
|
|
sensitive: true);
|
|
|
|
try {
|
|
final response = await http
|
|
.post(uri, headers: _headers(token), body: jsonEncode(body))
|
|
.timeout(customTimeout);
|
|
|
|
if (response.statusCode == 401 && !hasRetried) {
|
|
logSafe("Unauthorized POST. Attempting token refresh...");
|
|
if (await AuthService.refreshToken()) {
|
|
return await _postRequest(endpoint, body,
|
|
customTimeout: customTimeout, hasRetried: true);
|
|
}
|
|
}
|
|
return response;
|
|
} catch (e) {
|
|
logSafe("HTTP POST Exception: $e", level: LogLevel.error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// === Dashboard Endpoints ===
|
|
|
|
static Future<List<dynamic>?> getDashboardAttendanceOverview(
|
|
String projectId, int days) async {
|
|
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
|
|
if (days <= 0) throw ArgumentError('days must be greater than 0');
|
|
|
|
final endpoint =
|
|
"${ApiEndpoints.getDashboardAttendanceOverview}/$projectId?days=$days";
|
|
|
|
return _getRequest(endpoint).then((res) => res != null
|
|
? _parseResponse(res, label: 'Dashboard Attendance Overview')
|
|
: null);
|
|
}
|
|
|
|
/// Directly calling the API
|
|
static Future<List<dynamic>?> getDirectoryComments(String contactId) async {
|
|
final url = "${ApiEndpoints.getDirectoryNotes}/$contactId";
|
|
final response = await _getRequest(url);
|
|
final data = response != null
|
|
? _parseResponse(response, label: 'Directory Comments')
|
|
: null;
|
|
|
|
return data is List ? data : null;
|
|
}
|
|
|
|
static Future<bool> createContact(Map<String, dynamic> payload) async {
|
|
try {
|
|
logSafe("Submitting contact payload: $payload", sensitive: true);
|
|
|
|
final response = await _postRequest(ApiEndpoints.createContact, payload);
|
|
if (response != null) {
|
|
final json = jsonDecode(response.body);
|
|
if (json['success'] == true) {
|
|
logSafe("Contact created successfully.");
|
|
return true;
|
|
} else {
|
|
logSafe("Create contact failed: ${json['message']}",
|
|
level: LogLevel.warning);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logSafe("Error creating contact: $e", level: LogLevel.error);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static Future<List<String>> getOrganizationList() async {
|
|
try {
|
|
final response = await _getRequest(ApiEndpoints.getDirectoryOrganization);
|
|
if (response != null && response.statusCode == 200) {
|
|
final body = jsonDecode(response.body);
|
|
if (body['success'] == true && body['data'] is List) {
|
|
return List<String>.from(body['data']);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logSafe("Failed to fetch organization names: $e", level: LogLevel.error);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getContactCategoryList() async =>
|
|
_getRequest(ApiEndpoints.getDirectoryContactCategory).then((res) =>
|
|
res != null
|
|
? _parseResponseForAllData(res, label: 'Contact Category List')
|
|
: null);
|
|
|
|
static Future<Map<String, dynamic>?> getContactTagList() async =>
|
|
_getRequest(ApiEndpoints.getDirectoryContactTags).then((res) =>
|
|
res != null
|
|
? _parseResponseForAllData(res, label: 'Contact Tag List')
|
|
: null);
|
|
|
|
static Future<List<dynamic>?> getDirectoryData(
|
|
{required bool isActive}) async {
|
|
final queryParams = {
|
|
"active": isActive.toString(),
|
|
};
|
|
|
|
return _getRequest(ApiEndpoints.getDirectoryContacts,
|
|
queryParams: queryParams)
|
|
.then((res) =>
|
|
res != null ? _parseResponse(res, label: 'Directory Data') : null);
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getContactBucketList() async =>
|
|
_getRequest(ApiEndpoints.getDirectoryBucketList).then((res) => res != null
|
|
? _parseResponseForAllData(res, label: 'Contact Bucket List')
|
|
: null);
|
|
|
|
// === Attendance APIs ===
|
|
|
|
static Future<List<dynamic>?> getProjects() async =>
|
|
_getRequest(ApiEndpoints.getProjects).then(
|
|
(res) => res != null ? _parseResponse(res, label: 'Projects') : null);
|
|
|
|
static Future<List<dynamic>?> getGlobalProjects() async =>
|
|
_getRequest(ApiEndpoints.getGlobalProjects).then((res) =>
|
|
res != null ? _parseResponse(res, label: 'Global Projects') : null);
|
|
|
|
static Future<List<dynamic>?> getEmployeesByProject(String projectId) async =>
|
|
_getRequest(ApiEndpoints.getEmployeesByProject,
|
|
queryParams: {"projectId": projectId})
|
|
.then((res) =>
|
|
res != null ? _parseResponse(res, label: 'Employees') : null);
|
|
|
|
static Future<List<dynamic>?> getAttendanceLogs(
|
|
String projectId, {
|
|
DateTime? dateFrom,
|
|
DateTime? dateTo,
|
|
}) async {
|
|
final query = {
|
|
"projectId": projectId,
|
|
if (dateFrom != null)
|
|
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
|
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
|
|
};
|
|
return _getRequest(ApiEndpoints.getAttendanceLogs, queryParams: query).then(
|
|
(res) =>
|
|
res != null ? _parseResponse(res, label: 'Attendance Logs') : null);
|
|
}
|
|
|
|
static Future<List<dynamic>?> getAttendanceLogView(String id) async =>
|
|
_getRequest("${ApiEndpoints.getAttendanceLogView}/$id").then((res) =>
|
|
res != null ? _parseResponse(res, label: 'Log Details') : null);
|
|
|
|
static Future<List<dynamic>?> getRegularizationLogs(String projectId) async =>
|
|
_getRequest(ApiEndpoints.getRegularizationLogs,
|
|
queryParams: {"projectId": projectId})
|
|
.then((res) => res != null
|
|
? _parseResponse(res, label: 'Regularization Logs')
|
|
: null);
|
|
|
|
static Future<bool> uploadAttendanceImage(
|
|
String id,
|
|
String employeeId,
|
|
XFile? imageFile,
|
|
double latitude,
|
|
double longitude, {
|
|
required String imageName,
|
|
required String projectId,
|
|
String comment = "",
|
|
required int action,
|
|
bool imageCapture = true,
|
|
String? markTime,
|
|
}) async {
|
|
final now = DateTime.now();
|
|
final body = {
|
|
"id": id,
|
|
"employeeId": employeeId,
|
|
"projectId": projectId,
|
|
"markTime": markTime ?? DateFormat('hh:mm a').format(now),
|
|
"comment": comment,
|
|
"action": action,
|
|
"date": DateFormat('yyyy-MM-dd').format(now),
|
|
if (imageCapture) "latitude": '$latitude',
|
|
if (imageCapture) "longitude": '$longitude',
|
|
};
|
|
|
|
if (imageCapture && imageFile != null) {
|
|
try {
|
|
final bytes = await imageFile.readAsBytes();
|
|
final fileSize = await imageFile.length();
|
|
final contentType = "image/${imageFile.path.split('.').last}";
|
|
body["image"] = {
|
|
"fileName": imageName,
|
|
"contentType": contentType,
|
|
"fileSize": fileSize,
|
|
"description": "Employee attendance photo",
|
|
"base64Data": base64Encode(bytes),
|
|
};
|
|
} catch (e) {
|
|
logSafe("Image encoding error: $e", level: LogLevel.error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
final response = await _postRequest(
|
|
ApiEndpoints.uploadAttendanceImage,
|
|
body,
|
|
customTimeout: extendedTimeout,
|
|
);
|
|
if (response == null) return false;
|
|
|
|
final json = jsonDecode(response.body);
|
|
if (response.statusCode == 200 && json['success'] == true) return true;
|
|
|
|
logSafe("Failed to upload image: ${json['message'] ?? 'Unknown error'}");
|
|
return false;
|
|
}
|
|
|
|
static String generateImageName(String employeeId, int count) {
|
|
final now = DateTime.now();
|
|
final dateStr = DateFormat('yyyyMMdd_HHmmss').format(now);
|
|
final imageNumber = count.toString().padLeft(3, '0');
|
|
return "${employeeId}_${dateStr}_$imageNumber.jpg";
|
|
}
|
|
|
|
// === Employee APIs ===
|
|
|
|
static Future<List<dynamic>?> getAllEmployeesByProject(
|
|
String projectId) async {
|
|
if (projectId.isEmpty) throw ArgumentError('projectId must not be empty');
|
|
final endpoint = "${ApiEndpoints.getAllEmployeesByProject}/$projectId";
|
|
return _getRequest(endpoint).then((res) => res != null
|
|
? _parseResponse(res, label: 'Employees by Project')
|
|
: null);
|
|
}
|
|
|
|
static Future<List<dynamic>?> getAllEmployees() async =>
|
|
_getRequest(ApiEndpoints.getAllEmployees).then((res) =>
|
|
res != null ? _parseResponse(res, label: 'All Employees') : null);
|
|
|
|
static Future<List<dynamic>?> getRoles() async =>
|
|
_getRequest(ApiEndpoints.getRoles).then(
|
|
(res) => res != null ? _parseResponse(res, label: 'Roles') : null);
|
|
|
|
static Future<bool> createEmployee({
|
|
required String firstName,
|
|
required String lastName,
|
|
required String phoneNumber,
|
|
required String gender,
|
|
required String jobRoleId,
|
|
}) async {
|
|
final body = {
|
|
"firstName": firstName,
|
|
"lastName": lastName,
|
|
"phoneNumber": phoneNumber,
|
|
"gender": gender,
|
|
"jobRoleId": jobRoleId,
|
|
};
|
|
final response = await _postRequest(
|
|
ApiEndpoints.createEmployee,
|
|
body,
|
|
customTimeout: extendedTimeout,
|
|
);
|
|
|
|
if (response == null) return false;
|
|
final json = jsonDecode(response.body);
|
|
return response.statusCode == 200 && json['success'] == true;
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getEmployeeDetails(
|
|
String employeeId) async {
|
|
final url = "${ApiEndpoints.getEmployeeInfo}/$employeeId";
|
|
final response = await _getRequest(url);
|
|
final data = response != null
|
|
? _parseResponse(response, label: 'Employee Details')
|
|
: null;
|
|
return data is Map<String, dynamic> ? data : null;
|
|
}
|
|
|
|
// === Daily Task APIs ===
|
|
|
|
static Future<List<dynamic>?> getDailyTasks(
|
|
String projectId, {
|
|
DateTime? dateFrom,
|
|
DateTime? dateTo,
|
|
}) async {
|
|
final query = {
|
|
"projectId": projectId,
|
|
if (dateFrom != null)
|
|
"dateFrom": DateFormat('yyyy-MM-dd').format(dateFrom),
|
|
if (dateTo != null) "dateTo": DateFormat('yyyy-MM-dd').format(dateTo),
|
|
};
|
|
return _getRequest(ApiEndpoints.getDailyTask, queryParams: query).then(
|
|
(res) =>
|
|
res != null ? _parseResponse(res, label: 'Daily Tasks') : null);
|
|
}
|
|
|
|
static Future<bool> reportTask({
|
|
required String id,
|
|
required int completedTask,
|
|
required String comment,
|
|
required List<Map<String, dynamic>> checkList,
|
|
List<Map<String, dynamic>>? images,
|
|
}) async {
|
|
final body = {
|
|
"id": id,
|
|
"completedTask": completedTask,
|
|
"comment": comment,
|
|
"reportedDate": DateTime.now().toUtc().toIso8601String(),
|
|
"checkList": checkList,
|
|
if (images != null && images.isNotEmpty) "images": images,
|
|
};
|
|
|
|
final response = await _postRequest(
|
|
ApiEndpoints.reportTask,
|
|
body,
|
|
customTimeout: extendedTimeout,
|
|
);
|
|
|
|
if (response == null) return false;
|
|
final json = jsonDecode(response.body);
|
|
if (response.statusCode == 200 && json['success'] == true) {
|
|
Get.back();
|
|
return true;
|
|
}
|
|
logSafe("Failed to report task: ${json['message'] ?? 'Unknown error'}");
|
|
return false;
|
|
}
|
|
|
|
static Future<bool> commentTask({
|
|
required String id,
|
|
required String comment,
|
|
List<Map<String, dynamic>>? images,
|
|
}) async {
|
|
final body = {
|
|
"taskAllocationId": id,
|
|
"comment": comment,
|
|
"commentDate": DateTime.now().toUtc().toIso8601String(),
|
|
if (images != null && images.isNotEmpty) "images": images,
|
|
};
|
|
|
|
final response = await _postRequest(ApiEndpoints.commentTask, body);
|
|
if (response == null) return false;
|
|
final json = jsonDecode(response.body);
|
|
return response.statusCode == 200 && json['success'] == true;
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getDailyTasksDetails(
|
|
String projectId) async {
|
|
final url = "${ApiEndpoints.dailyTaskDetails}/$projectId";
|
|
final response = await _getRequest(url);
|
|
return response != null
|
|
? _parseResponseForAllData(response, label: 'Daily Task Details')
|
|
as Map<String, dynamic>?
|
|
: null;
|
|
}
|
|
|
|
static Future<bool> assignDailyTask({
|
|
required String workItemId,
|
|
required int plannedTask,
|
|
required String description,
|
|
required List<String> taskTeam,
|
|
DateTime? assignmentDate,
|
|
}) async {
|
|
final body = {
|
|
"workItemId": workItemId,
|
|
"plannedTask": plannedTask,
|
|
"description": description,
|
|
"taskTeam": taskTeam,
|
|
"assignmentDate":
|
|
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
|
|
};
|
|
final response = await _postRequest(ApiEndpoints.assignDailyTask, body);
|
|
if (response == null) return false;
|
|
final json = jsonDecode(response.body);
|
|
if (response.statusCode == 200 && json['success'] == true) {
|
|
Get.back();
|
|
return true;
|
|
}
|
|
logSafe(
|
|
"Failed to assign daily task: ${json['message'] ?? 'Unknown error'}");
|
|
return false;
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getWorkStatus() async {
|
|
final res = await _getRequest(ApiEndpoints.getWorkStatus);
|
|
if (res == null) {
|
|
logSafe('Work Status API returned null');
|
|
return null;
|
|
}
|
|
|
|
logSafe('Work Status raw response: ${res.body}');
|
|
return _parseResponseForAllData(res, label: 'Work Status')
|
|
as Map<String, dynamic>?;
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> getMasterWorkCategories() async =>
|
|
_getRequest(ApiEndpoints.getmasterWorkCategories).then((res) =>
|
|
res != null
|
|
? _parseResponseForAllData(res, label: 'Master Work Categories')
|
|
: null);
|
|
|
|
static Future<bool> approveTask({
|
|
required String id,
|
|
required String comment,
|
|
required String workStatus,
|
|
required int approvedTask,
|
|
List<Map<String, dynamic>>? images,
|
|
}) async {
|
|
final body = {
|
|
"id": id,
|
|
"workStatus": workStatus,
|
|
"approvedTask": approvedTask,
|
|
"comment": comment,
|
|
if (images != null && images.isNotEmpty) "images": images,
|
|
};
|
|
|
|
final response = await _postRequest(ApiEndpoints.approveReportAction, body);
|
|
if (response == null) return false;
|
|
|
|
final json = jsonDecode(response.body);
|
|
return response.statusCode == 200 && json['success'] == true;
|
|
}
|
|
|
|
static Future<bool> createTask({
|
|
required String parentTaskId,
|
|
required int plannedTask,
|
|
required String comment,
|
|
required String workAreaId,
|
|
required String activityId,
|
|
DateTime? assignmentDate,
|
|
required String categoryId,
|
|
}) async {
|
|
final body = [
|
|
{
|
|
"parentTaskId": parentTaskId,
|
|
"plannedWork": plannedTask,
|
|
"comment": comment,
|
|
"workAreaID": workAreaId,
|
|
"activityID": activityId,
|
|
"workCategoryId": categoryId,
|
|
'completedWork': 0,
|
|
}
|
|
];
|
|
|
|
final response = await _postRequest(ApiEndpoints.assignTask, body);
|
|
if (response == null) return false;
|
|
|
|
final json = jsonDecode(response.body);
|
|
if (response.statusCode == 200 && json['success'] == true) {
|
|
Get.back();
|
|
return true;
|
|
}
|
|
|
|
logSafe("Failed to create task: ${json['message'] ?? 'Unknown error'}");
|
|
return false;
|
|
}
|
|
}
|