using System.Diagnostics;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using CORP_MAUI.Models;
namespace CORP_MAUI.Services;
///
/// Конфигурация API клиента
///
public class ApiConfig
{
public string BaseUrl { get; set; } = "https://corp.robotail.pro";
public string TokenObtainPath { get; set; } = "/api/auth/token/";
public string TokenRefreshPath { get; set; } = "/api/auth/token/refresh/";
public int Timeout { get; set; } = 30;
public Dictionary? DefaultHeaders { get; init; }
}
///
/// Обёртка для HTTP ответов
///
public class ApiResponse
{
public bool Ok { get; set; }
public int Status { get; set; }
public T? Data { get; set; }
public string? Text { get; set; }
public string? ErrorMessage { get; set; }
}
public class ApiService : IDisposable
{
private readonly HttpClient _httpClient;
private readonly ApiConfig _config;
private string? _accessToken;
private string? _refreshToken;
private bool _tokensLoaded;
private readonly SemaphoreSlim _tokenLoadSemaphore = new(1, 1);
// Ключи для SecureStorage
private const string ACCESS_TOKEN_KEY = "api.jwt_access";
private const string REFRESH_TOKEN_KEY = "api.jwt_refresh";
public bool IsAuthenticated => !string.IsNullOrEmpty(_accessToken);
///
/// Создаёт платформенно-специфичный HttpMessageHandler
///
private static HttpMessageHandler CreatePlatformHandler()
{
// Используем стандартны�� HttpClientHandler с отключённой проверкой сертификатов для отладки
// В продакшене следует убрать ServerCertificateCustomValidationCallback
return new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
}
///
/// Получить текущий access токен (для WebSocket и других сервисов)
///
public string? GetAccessToken() => _accessToken;
public ApiService(ApiConfig? config = null)
{
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
Trace.WriteLine("║ [ApiService] INITIALIZATION STARTED");
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
_config = config ?? new ApiConfig();
Trace.WriteLine($"[ApiService] BaseURL: {_config.BaseUrl}");
Trace.WriteLine($"[ApiService] TokenObtainPath: {_config.TokenObtainPath}");
Trace.WriteLine($"[ApiService] TokenRefreshPath: {_config.TokenRefreshPath}");
// Создаём HttpClient с платформенным хэндлером для корректной работы на Android
HttpMessageHandler handler = CreatePlatformHandler();
_httpClient = new HttpClient(handler)
{
BaseAddress = new Uri(_config.BaseUrl),
Timeout = TimeSpan.FromSeconds(_config.Timeout)
};
// Устанавливаем дефолтные заголовки
if (_config.DefaultHeaders != null)
{
foreach (var header in _config.DefaultHeaders)
{
_httpClient.DefaultRequestHeaders.Add(header.Key, header.Value);
}
}
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
Trace.WriteLine($"[ApiService] HttpClient BaseAddress set to: {_httpClient.BaseAddress}");
Trace.WriteLine("[ApiService] Initialization complete\n");
// НЕ загружаем токены здесь - они будут загружены при первом запросе
}
///
/// Гарантирует, что токены загружены из SecureStorage перед выполнением запроса
///
private async Task EnsureTokensLoadedAsync()
{
if (_tokensLoaded) return;
await _tokenLoadSemaphore.WaitAsync();
try
{
if (_tokensLoaded) return;
Trace.WriteLine("[ApiService] Loading saved tokens...");
await LoadTokensAsync();
_tokensLoaded = true;
}
finally
{
_tokenLoadSemaphore.Release();
}
}
#region Debug & Logging
///
/// Детальный вывод информации о HTTP запросе и ответе для отладки
///
private void LogHttpDetails(string operation, HttpRequestMessage? request, HttpResponseMessage? response, string? additionalInfo = null)
{
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
Trace.WriteLine($"║ [ApiService] {operation}");
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
if (request != null)
{
Trace.WriteLine($"║ REQUEST:");
Trace.WriteLine($"║ Method: {request.Method}");
Trace.WriteLine($"║ URI: {request.RequestUri}");
Trace.WriteLine($"║ BaseAddress: {_httpClient.BaseAddress}");
Trace.WriteLine($"║ Headers:");
foreach (KeyValuePair> header in request.Headers)
{
string value = header.Key == "Authorization" ? "[HIDDEN]" : string.Join(", ", header.Value);
Trace.WriteLine($"║ {header.Key}: {value}");
}
if (request.Content != null)
{
Trace.WriteLine($"║ Content-Type: {request.Content.Headers.ContentType}");
}
}
if (response != null)
{
Trace.WriteLine($"║ RESPONSE:");
Trace.WriteLine($"║ Status: {(int)response.StatusCode} {response.StatusCode}");
Trace.WriteLine($"║ Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}");
Trace.WriteLine($"║ Content-Length: {response.Content.Headers.ContentLength ?? 0} bytes");
}
if (!string.IsNullOrEmpty(additionalInfo))
{
Trace.WriteLine($"║ INFO:");
Trace.WriteLine($"║ {additionalInfo}");
}
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
}
#endregion
#region Token Management
///
/// Загрузить токены из SecureStorage
///
private async Task LoadTokensAsync()
{
try
{
_accessToken = await SecureStorage.GetAsync(ACCESS_TOKEN_KEY);
_refreshToken = await SecureStorage.GetAsync(REFRESH_TOKEN_KEY);
if (!string.IsNullOrEmpty(_accessToken))
{
Trace.WriteLine($"[ApiService] ✅ Access token loaded from storage");
Trace.WriteLine($"[ApiService] Token (first 30 chars): {_accessToken.Substring(0, Math.Min(30, _accessToken.Length))}...");
Trace.WriteLine($"[ApiService] Token length: {_accessToken.Length}");
SetAuthorizationHeader(_accessToken);
}
else
{
Trace.WriteLine($"[ApiService] ℹ️ No saved access token found");
}
if (!string.IsNullOrEmpty(_refreshToken))
{
Trace.WriteLine($"[ApiService] ✅ Refresh token loaded from storage");
}
else
{
Trace.WriteLine($"[ApiService] ℹ️ No saved refresh token found");
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Error loading tokens: {ex.Message}");
}
}
///
/// Сохранить токены в SecureStorage
///
private async Task SaveTokensAsync(string accessToken, string refreshToken)
{
try
{
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, accessToken);
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, refreshToken);
_accessToken = accessToken;
_refreshToken = refreshToken;
Trace.WriteLine($"[ApiService] 💾 Tokens saved to SecureStorage");
Trace.WriteLine($"[ApiService] Access token length: {accessToken.Length}");
Trace.WriteLine($"[ApiService] Refresh token length: {refreshToken.Length}");
SetAuthorizationHeader(accessToken);
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Error saving tokens: {ex.Message}");
}
}
///
/// Установить заголовок авторизации
///
private void SetAuthorizationHeader(string token)
{
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
Trace.WriteLine($"[ApiService] 🔑 Default Authorization header set: Bearer [token]");
}
///
/// Очистить токены
///
private void ClearTokens()
{
Trace.WriteLine("\n[ApiService] 🧹 Clearing all tokens...");
_accessToken = null;
_refreshToken = null;
_httpClient.DefaultRequestHeaders.Authorization = null;
SecureStorage.Remove(ACCESS_TOKEN_KEY);
SecureStorage.Remove(REFRESH_TOKEN_KEY);
Trace.WriteLine("[ApiService] ✅ Tokens cleared, user is logged out\n");
}
#endregion
#region Authentication
///
/// Вход в систему с получением JWT токенов
///
public async Task LoginAsync(string email, string password)
{
try
{
LoginRequest loginData = new() { Email = email, Password = password };
// Используем относительный путь, так как BaseAddress уже установлен
string path = _config.TokenObtainPath.TrimStart('/');
Trace.WriteLine($"\n[ApiService] >>> Начинаем логин для {email}");
Trace.WriteLine($"[ApiService] >>> Путь токена: {_config.TokenObtainPath} → {path}");
Trace.WriteLine($"[ApiService] >>> BaseAddress: {_httpClient.BaseAddress}");
Trace.WriteLine($"[ApiService] >>> Ожидаемый URL: {_httpClient.BaseAddress}{path}\n");
// 🔓 Создаем запрос БЕЗ Authorization header (это запрос для получения токена!)
HttpRequestMessage request = new(HttpMethod.Post, path)
{
Content = JsonContent.Create(loginData)
};
// Убираем Authorization header если он был установлен
request.Headers.Authorization = null;
LogHttpDetails("LOGIN REQUEST", request, null, $"Email: {email}");
HttpResponseMessage response = await _httpClient.SendAsync(request);
LogHttpDetails("LOGIN RESPONSE", request, response);
if (!response.IsSuccessStatusCode)
{
string errorText = await response.Content.ReadAsStringAsync();
Trace.WriteLine($"\n[ApiService] ❌ Ошибка логина:");
Trace.WriteLine($" Status Code: {(int)response.StatusCode} {response.StatusCode}");
Trace.WriteLine($" Request URL: {response.RequestMessage?.RequestUri}");
Trace.WriteLine($" Error Body: {errorText}\n");
return new AuthResponse
{
Success = false,
Message = $"Ошибка авторизации ({response.StatusCode}): {errorText}\nURL: {response.RequestMessage?.RequestUri}"
};
}
// Проверяем Content-Type
string contentType = response.Content.Headers.ContentType?.MediaType ?? "";
if (!contentType.Contains("application/json"))
{
Trace.WriteLine($"[ApiService] ❌ Неверный Content-Type: {contentType}");
return new AuthResponse
{
Success = false,
Message = "Сервер вернул неверный формат ответа"
};
}
try
{
string responseBody = await response.Content.ReadAsStringAsync();
Trace.WriteLine($"\n[ApiService] Response Body: {responseBody}\n");
TokenResponse? tokenData = JsonSerializer.Deserialize(responseBody, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (tokenData == null || tokenData.Access == null || tokenData.Refresh == null)
{
Trace.WriteLine($"[ApiService] ❌ Токены не получены из ответа");
return new AuthResponse
{
Success = false,
Message = "Не получены токены доступа"
};
}
Trace.WriteLine($"[ApiService] ✅ Токены получены успешно");
Trace.WriteLine($" Access Token: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
Trace.WriteLine($" Refresh Token: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...\n");
// Сохраняем токены
await SaveTokensAsync(tokenData.Access, tokenData.Refresh);
// Получаем информацию о пользователе
Trace.WriteLine($"[ApiService] >>> Получаем информацию о пользователе...\n");
User? user = await GetCurrentUserAsync();
if (user != null)
{
Trace.WriteLine($"[ApiService] ✅ Пользователь: {user.Username} ({user.Email})\n");
}
return new AuthResponse
{
Success = true,
Token = tokenData.Access,
User = user,
Message = "Вход выполнен успешно"
};
}
catch (JsonException ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка разбора JSON: {ex.Message}");
return new AuthResponse
{
Success = false,
Message = $"Ошибка разбора ответа: {ex.Message}"
};
}
}
catch (HttpRequestException ex)
{
Trace.WriteLine($"[ApiService] ❌ HTTP ошибка: {ex.Message}");
return new AuthResponse
{
Success = false,
Message = $"Ошибка подключения: {ex.Message}"
};
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Неожиданная ошибка: {ex.Message}\n{ex.StackTrace}");
return new AuthResponse
{
Success = false,
Message = $"Неожиданная ошибка: {ex.Message}"
};
}
}
///
/// Выход из системы
///
public void Logout()
{
ClearTokens();
}
///
/// Обновить access токен используя refresh токен
///
private async Task RefreshTokensAsync()
{
if (string.IsNullOrEmpty(_refreshToken))
{
Trace.WriteLine("[ApiService] ❌ Cannot refresh: no refresh token available");
return false;
}
try
{
Trace.WriteLine("\n╔════════════════════════════════════════════════════════════════════════════════════");
Trace.WriteLine("║ [ApiService] TOKEN REFRESH PROCESS");
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
// Используем относительный путь, так как BaseAddress уже установлен
string path = _config.TokenRefreshPath.TrimStart('/');
RefreshRequest refreshData = new RefreshRequest { Refresh = _refreshToken };
Trace.WriteLine($"║ Request Path: {path}");
Trace.WriteLine($"║ Full URL: {_httpClient.BaseAddress}{path}");
Trace.WriteLine($"║ Refresh Token: {_refreshToken.Substring(0, Math.Min(20, _refreshToken.Length))}...");
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
// Создаем запрос БЕЗ авторизации (для refresh используется только refresh токен в теле)
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, path)
{
Content = JsonContent.Create(refreshData)
};
HttpResponseMessage response = await _httpClient.SendAsync(request);
Trace.WriteLine($"\n[ApiService] Refresh response: {(int)response.StatusCode} {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
string responseText = await response.Content.ReadAsStringAsync();
Trace.WriteLine($"[ApiService] Response body: {responseText}");
TokenResponse? tokenData = JsonSerializer.Deserialize(responseText, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (!string.IsNullOrEmpty(tokenData?.Access))
{
Trace.WriteLine($"\n[ApiService] ✅ New access token received: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
_accessToken = tokenData.Access;
try
{
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, tokenData.Access);
}
catch (Exception storageEx)
{
Trace.WriteLine($"[ApiService] ⚠️ Ошибка сохранения access token в SecureStorage: {storageEx.Message}");
}
SetAuthorizationHeader(tokenData.Access);
// Если пришёл новый refresh токен, сохраняем и его
if (!string.IsNullOrEmpty(tokenData.Refresh))
{
Trace.WriteLine($"[ApiService] ✅ New refresh token received: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...");
_refreshToken = tokenData.Refresh;
try
{
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, tokenData.Refresh);
}
catch (Exception storageEx)
{
Trace.WriteLine($"[ApiService] ⚠️ Ошибка сохранения refresh token в SecureStorage: {storageEx.Message}");
}
}
Trace.WriteLine("[ApiService] ✅ Tokens saved successfully\n");
return true;
}
else
{
Trace.WriteLine("[ApiService] ❌ No access token in response");
}
}
else
{
string errorText = await response.Content.ReadAsStringAsync();
Trace.WriteLine($"[ApiService] ❌ Refresh failed: {response.StatusCode}");
Trace.WriteLine($" Error: {errorText}");
}
// Не удалось обновить токены - очищаем их
Trace.WriteLine("[ApiService] ⚠️ Clearing all tokens due to refresh failure\n");
ClearTokens();
return false;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Exception during token refresh: {ex.Message}");
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
ClearTokens();
return false;
}
}
#endregion
#region HTTP Methods
///
/// Базовый метод для HTTP запросов с автоматическим refresh при 401/403
/// Автоматически добавляет Authorization header если есть токен
///
private async Task> RequestAsync(
HttpMethod method,
string path,
object? jsonData = null,
Dictionary? queryParams = null,
Dictionary? headers = null,
bool retry = true,
bool requiresAuth = true)
{
try
{
// ⚡ Гарантируем, что токены загружены перед запросом
if (requiresAuth)
{
await EnsureTokensLoadedAsync();
}
// Используем относительный путь, так как BaseAddress установлен
string relativePath = path.TrimStart('/');
// Добавляем query параметры
if (queryParams != null && queryParams.Count != 0)
{
string query = string.Join("&", queryParams.Select(kvp =>
$"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
relativePath = $"{relativePath}?{query}";
}
HttpRequestMessage request = new HttpRequestMessage(method, relativePath);
// 🔑 АВТОМАТИЧЕСКИ добавляем Authorization header если есть токен
if (requiresAuth && !string.IsNullOrEmpty(_accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
Trace.WriteLine($"[ApiService] 🔑 Authorization header added with token");
}
// Добавляем дополнительные заголовки
if (headers != null)
{
foreach (KeyValuePair header in headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
// Добавляем JSON body
if (jsonData != null)
{
request.Content = JsonContent.Create(jsonData);
}
Trace.WriteLine($"\n[ApiService] >>> {method} Request");
Trace.WriteLine($" Path: {path} → {relativePath}");
Trace.WriteLine($" Full URL: {_httpClient.BaseAddress}{relativePath}");
Trace.WriteLine($" Requires Auth: {requiresAuth}");
Trace.WriteLine($" Has Token: {!string.IsNullOrEmpty(_accessToken)}");
if (jsonData != null)
{
Trace.WriteLine($" Has Body: Yes");
}
HttpResponseMessage response = await _httpClient.SendAsync(request);
Trace.WriteLine($"[ApiService] <<< Response: {(int)response.StatusCode} {response.StatusCode}");
Trace.WriteLine($" Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}\n");
// 🔄 Проверяем на 401/403 и пробуем обновить токен
if ((response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
&& retry && !string.IsNullOrEmpty(_refreshToken))
{
Trace.WriteLine($"[ApiService] 🔄 Token expired or invalid, attempting refresh...");
Trace.WriteLine($" Current access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
Trace.WriteLine($" Refresh token available: Yes\n");
if (await RefreshTokensAsync())
{
Trace.WriteLine($"[ApiService] ✅ Token successfully refreshed!");
Trace.WriteLine($" New access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
Trace.WriteLine($" Retrying original request...\n");
// Токен обновлён, повторяем запрос (retry=false чтобы избежать бесконечного цикла)
return await RequestAsync(method, path, jsonData, queryParams, headers, retry: false, requiresAuth);
}
else
{
Trace.WriteLine($"[ApiService] ❌ Token refresh failed - tokens cleared");
Trace.WriteLine($" User needs to login again\n");
}
}
string responseText = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
Trace.WriteLine($"[ApiService] ❌ Request failed:");
Trace.WriteLine($" Status: {(int)response.StatusCode} {response.StatusCode}");
Trace.WriteLine($" URL: {response.RequestMessage?.RequestUri}");
Trace.WriteLine($" Body: {responseText.Substring(0, Math.Min(200, responseText.Length))}\n");
return new ApiResponse
{
Ok = false,
Status = (int)response.StatusCode,
Text = responseText,
ErrorMessage = $"HTTP {response.StatusCode}: {responseText}"
};
}
// Пытаемся распарсить JSON
T? data = default;
if (!string.IsNullOrWhiteSpace(responseText))
{
try
{
data = JsonSerializer.Deserialize(responseText, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
Trace.WriteLine($"[ApiService] ✅ Data parsed successfully\n");
}
catch (JsonException ex)
{
Trace.WriteLine($"[ApiService] ⚠️ JSON parse warning: {ex.Message}\n");
// Не JSON ответ, игнорируем
}
}
return new ApiResponse
{
Ok = true,
Status = (int)response.StatusCode,
Data = data,
Text = responseText
};
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Request exception: {ex.Message}");
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
return new ApiResponse
{
Ok = false,
Status = 0,
ErrorMessage = $"Ошибка запроса: {ex.Message}"
};
}
}
///
/// GET запрос
///
public Task> GetAsync(string path, Dictionary? queryParams = null)
{
return RequestAsync(HttpMethod.Get, path, queryParams: queryParams);
}
///
/// POST запрос
///
public Task> PostAsync(string path, object? data = null)
{
return RequestAsync(HttpMethod.Post, path, jsonData: data);
}
///
/// PUT запрос
///
public Task> PutAsync(string path, object? data = null)
{
return RequestAsync(HttpMethod.Put, path, jsonData: data);
}
///
/// PATCH запрос
///
public Task> PatchAsync(string path, object? data = null)
{
return RequestAsync(HttpMethod.Patch, path, jsonData: data);
}
///
/// DELETE запрос
///
public Task> DeleteAsync(string path)
{
return RequestAsync(HttpMethod.Delete, path);
}
#endregion
#region API Methods
// ============================================
// EMPLOYEES (Сотрудники)
// ============================================
///
/// Получить список сотрудников с пагинацией
///
public async Task> GetEmployeesPaginatedAsync(int? page = null, int? pageSize = null, string? search = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
if (!string.IsNullOrEmpty(search)) queryParams["search"] = search;
ApiResponse> response = await GetAsync>("api/v1/employees/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения сотрудников: {ex.Message}");
return new PaginatedResponse();
}
}
///
/// Получить список всех активных сотрудников (все страницы)
///
public async Task> GetAllEmployeesAsync()
{
try
{
List allEmployees = new();
int currentPage = 1;
PaginatedResponse? response;
do
{
Dictionary queryParams = new Dictionary
{
["active"] = "true",
["page"] = currentPage.ToString()
};
ApiResponse> apiResponse = await GetAsync>("api/v1/employees/", queryParams);
response = apiResponse.Ok && apiResponse.Data != null ? apiResponse.Data : new PaginatedResponse();
if (response.Results.Any())
{
allEmployees.AddRange(response.Results);
currentPage++;
}
} while (!string.IsNullOrEmpty(response.Next));
Trace.WriteLine($"[ApiService] ✅ Загружено активных сотрудников: {allEmployees.Count}");
return allEmployees;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка получения всех активных сотрудников: {ex.Message}");
return new List();
}
}
///
/// Вернуть полный URL изображения, если передан относительный путь — добавить BaseUrl
///
public string? GetFullImageUrl(string? url)
{
if (string.IsNullOrEmpty(url)) return null;
// Если это data-uri — не трактуем как URL
if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return null;
// Protocol-relative
if (url.StartsWith("//")) return "https:" + url;
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return url;
// Относительный путь — приклеиваем базовый URL
return _config?.BaseUrl?.TrimEnd('/') + "/" + url.TrimStart('/');
}
///
/// Загрузить аватарки для списка пользователей
///
public Task LoadUsersAvatarsAsync(List? users)
{
if (users == null) return Task.CompletedTask;
foreach (var user in users)
{
user.AvatarUrl = GetFullImageUrl(user.AvatarUrl);
}
return Task.CompletedTask;
}
///
/// Получить информацию о конкретном пользователе по ID
///
public async Task GetUserByIdAsync(int userId)
{
try
{
ApiResponse response = await GetAsync($"api/v1/employees/{userId}/");
if (response.Ok && response.Data != null)
{
Trace.WriteLine($"[ApiService] ✅ Пользователь загружен: {response.Data.FullName}");
return response.Data;
}
Trace.WriteLine($"[ApiService] ❌ Пользователь с ID {userId} не найден");
return null;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка получения пользователя {userId}: {ex.Message}");
return null;
}
}
///
/// Получить информацию о текущем пользователе
/// Использует эндпоинт /api/v1/employees/me/
///
public async Task GetCurrentUserAsync()
{
try
{
Trace.WriteLine("\n╔═══════════════════════════════════════════════════════════════════════════════════");
Trace.WriteLine("║ [ApiService] FETCHING CURRENT USER PROFILE");
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
Trace.WriteLine($"║ Endpoint: api/v1/employees/me/");
Trace.WriteLine($"║ Has Access Token: {!string.IsNullOrEmpty(_accessToken)}");
if (!string.IsNullOrEmpty(_accessToken))
{
Trace.WriteLine($"║ Token (first 30 chars): {_accessToken.Substring(0, Math.Min(30, _accessToken.Length))}...");
}
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════\n");
ApiResponse response = await GetAsync("api/v1/employees/me/");
if (response.Ok && response.Data != null)
{
Trace.WriteLine($"[ApiService] ✅ Current user profile loaded successfully");
Trace.WriteLine($" User ID: {response.Data.Id}");
Trace.WriteLine($" Email: {response.Data.Email}");
Trace.WriteLine($" Name: {response.Data.FirstName} {response.Data.LastName}");
Trace.WriteLine($" Position: {response.Data.PositionName}");
Trace.WriteLine($" Departments: {response.Data.Departments?.Count ?? 0}");
Trace.WriteLine($" Skills: {response.Data.Skills?.Count ?? 0}\n");
return response.Data;
}
else
{
Trace.WriteLine($"[ApiService] ❌ Failed to load current user profile");
Trace.WriteLine($" Status: {response.Status}");
Trace.WriteLine($" Error: {response.ErrorMessage ?? "Unknown error"}");
Trace.WriteLine($" Response Text: {response.Text?.Substring(0, Math.Min(200, response.Text?.Length ?? 0))}\n");
return null;
}
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Exception while getting current user profile: {ex.Message}");
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
return null;
}
}
///
/// Получить текущего пользователя (alias для GetCurrentUserAsync)
///
public async Task GetUserAsync()
{
return await GetCurrentUserAsync();
}
///
/// Получить сотрудника по ID
///
public async Task GetEmployeeByIdAsync(int id)
{
try
{
ApiResponse response = await GetAsync($"api/v1/employees/{id}/");
return response.Ok ? response.Data : null;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения сотрудника {id}: {ex.Message}");
return null;
}
}
// ============================================
// DEPARTMENTS (Отделы)
// ============================================
///
/// Получить список отделов с пагинацией
///
public async Task> GetDepartmentsPaginatedAsync(int? page = null, int? pageSize = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/departments/", queryParams);
// Логируем сырой JSON ответ
if (response.Ok && !string.IsNullOrEmpty(response.Text))
{
Trace.WriteLine($"[ApiService] 📄 Raw JSON response (first 500 chars):");
Trace.WriteLine(response.Text.Substring(0, Math.Min(500, response.Text.Length)));
}
// Логируем первый отдел для проверки
if (response.Ok && response.Data != null && response.Data.Results.Any())
{
var firstDept = response.Data.Results.First();
Trace.WriteLine($"[ApiService] 📊 Первый загруженный отдел:");
Trace.WriteLine($" - ID: {firstDept.Id}");
Trace.WriteLine($" - Name: '{firstDept.Name}'");
Trace.WriteLine($" - Description: '{firstDept.Description}'");
Trace.WriteLine($" - EmployeesCount: {firstDept.EmployeesCount}");
Trace.WriteLine($" - Head: {firstDept.Head?.FullName ?? "null"}");
}
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения отделов: {ex.Message}");
return new PaginatedResponse();
}
}
///
/// Получить список всех отделов
///
public async Task> GetAllDepartmentsAsync()
{
try
{
List allDepartments = new List();
int currentPage = 1;
PaginatedResponse? response;
do
{
response = await GetDepartmentsPaginatedAsync(page: currentPage);
if (response.Results.Any())
{
allDepartments.AddRange(response.Results);
currentPage++;
}
} while (!string.IsNullOrEmpty(response.Next));
return allDepartments;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения всех отделов: {ex.Message}");
return new List();
}
}
///
/// Получить информацию о конкретном отделе
///
public async Task GetDepartmentByIdAsync(int departmentId)
{
try
{
ApiResponse response = await GetAsync($"api/v1/departments/{departmentId}/");
return response.Ok && response.Data != null ? response.Data : null;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения отдела {departmentId}: {ex.Message}");
return null;
}
}
///
/// Получить список сотрудников отдела
///
public async Task> GetDepartmentEmployeesAsync(int departmentId)
{
try
{
Dictionary queryParams = new Dictionary
{
{ "department", departmentId.ToString() }
};
List allEmployees = new List();
int currentPage = 1;
do
{
queryParams["page"] = currentPage.ToString();
var apiResponse = await GetAsync>("api/v1/employees/", queryParams);
if (!apiResponse.Ok || apiResponse.Data == null)
break;
var response = apiResponse.Data;
if (response.Results.Any())
{
allEmployees.AddRange(response.Results);
currentPage++;
}
if (string.IsNullOrEmpty(response.Next))
break;
} while (true);
return allEmployees;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения сотрудников отдела {departmentId}: {ex.Message}");
return new List();
}
}
// ============================================
// POSITIONS (Должности)
// ============================================
///
/// Получить список всех должностей
///
public async Task> GetPositionsAsync()
{
try
{
ApiResponse> response = await GetAsync>("api/v1/positions/");
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения должностей: {ex.Message}");
return new List();
}
}
// ============================================
// SKILLS (Навыки)
// ============================================
///
/// Получить список всех навыков
///
public async Task> GetSkillsAsync()
{
try
{
ApiResponse> response = await GetAsync>("api/v1/skills/");
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения навыков: {ex.Message}");
return new List();
}
}
// ============================================
// CALENDAR EVENTS (События календаря)
// ============================================
///
/// Получить события календаря
///
public async Task> GetCalendarEventsAsync()
{
try
{
ApiResponse> response = await GetAsync>("api/v1/calendar/events/");
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения событий календаря: {ex.Message}");
return new List();
}
}
// ============================================
// DOCUMENTS (Документы)
// ============================================
///
/// Получить список документов с пагинацией
///
public async Task> GetDocumentsPaginatedAsync(int? page = null, int? pageSize = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/documents/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения документов: {ex.Message}");
return new PaginatedResponse();
}
}
///
/// Получить все документы (с автоматической пагинацией)
///
public async Task> GetAllDocumentsAsync()
{
List allDocuments = new List();
int page = 1;
bool hasMore = true;
try
{
Trace.WriteLine("[ApiService] >>> Загрузка всех документов...");
while (hasMore)
{
PaginatedResponse response = await GetDocumentsPaginatedAsync(page);
if (response.Results != null && response.Results.Any())
{
allDocuments.AddRange(response.Results);
Trace.WriteLine($"[ApiService] >>> Загружено документов: {allDocuments.Count} / {response.Count}");
hasMore = !string.IsNullOrEmpty(response.Next);
page++;
}
else
{
hasMore = false;
}
}
Trace.WriteLine($"[ApiService] ✅ Всего загружено документов: {allDocuments.Count}");
return allDocuments;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки документов: {ex.Message}");
return allDocuments;
}
}
// ============================================
// REQUESTS (Запросы)
// ============================================
///
/// Получить список запросов с пагинацией
///
public async Task> GetRequestsPaginatedAsync(int? page = null, int? pageSize = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/requests/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения запросов: {ex.Message}");
return new PaginatedResponse();
}
}
///
/// Получить все заявления (с автоматической пагинацией)
///
public async Task> GetAllRequestsAsync()
{
List allRequests = new List();
int page = 1;
bool hasMore = true;
try
{
Trace.WriteLine("[ApiService] >>> Загрузка всех заявлений...");
while (hasMore)
{
PaginatedResponse response = await GetRequestsPaginatedAsync(page);
if (response.Results != null && response.Results.Any())
{
allRequests.AddRange(response.Results);
Trace.WriteLine($"[ApiService] >>> Загружено заявлений: {allRequests.Count} / {response.Count}");
hasMore = !string.IsNullOrEmpty(response.Next);
page++;
}
else
{
hasMore = false;
}
}
Trace.WriteLine($"[ApiService] ✅ Всего загружено заявлений: {allRequests.Count}");
return allRequests;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки заявлений: {ex.Message}");
return allRequests;
}
}
// ============================================
// EMPLOYEE ACTIONS (Действия сотрудников)
// ============================================
///
/// Получить историю действий сотрудников
///
public async Task> GetEmployeeActionsAsync()
{
try
{
ApiResponse> response = await GetAsync>("api/v1/employee-actions/");
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения действий сотрудников: {ex.Message}");
return new List();
}
}
// ============================================
// POSTS (Посты/Новости)
// ============================================
///
/// Получить список постов с пагинацией
///
public async Task> GetPostsPaginatedAsync(int? page = null, int? pageSize = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/posts/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения постов: {ex.Message}");
return new PaginatedResponse();
}
}
// ============================================
// COMMENTS (Комментарии)
// ============================================
///
/// Получить список комментариев с пагинацией
///
public async Task> GetCommentsPaginatedAsync(int? page = null, int? pageSize = null, int? postId = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
if (postId.HasValue) queryParams["post"] = postId.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/comments/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения комментариев: {ex.Message}");
return new PaginatedResponse();
}
}
// ============================================
// DEPARTMENT ROLES (Роли отделов)
// ============================================
///
/// Получить список ролей в отделах с пагинацией
///
public async Task> GetDepartmentRolesPaginatedAsync(int? page = null, int? pageSize = null)
{
try
{
Dictionary queryParams = new Dictionary();
if (page.HasValue) queryParams["page"] = page.Value.ToString();
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/department-roles/", queryParams);
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения ролей отделов: {ex.Message}");
return new PaginatedResponse();
}
}
// ============================================
// LEGACY/COMPATIBILITY METHODS
// ============================================
///
/// Получить список сотрудников (устаревший метод для обратной совместимости)
///
[Obsolete("Используйте GetAllEmployeesAsync() или GetEmployeesPaginatedAsync()")]
public Task> GetEmployeesAsync() => GetAllEmployeesAsync();
///
/// Получить историю сообщений (если есть эндпоинт)
///
public async Task> GetMessagesAsync(int? userId = null)
{
try
{
string path = userId.HasValue
? $"api/v1/messages/?user_id={userId.Value}"
: "api/v1/messages/";
ApiResponse> response = await GetAsync>(path);
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения сообщений: {ex.Message}");
return new List();
}
}
///
/// Отправить сообщение через REST API (резервный метод)
///
public async Task SendMessageAsync(Message message)
{
try
{
ApiResponse response = await PostAsync("api/v1/messages/", message);
return response.Ok;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка отправки сообщения: {ex.Message}");
return false;
}
}
///
/// Поиск пользователей
///
public async Task> SearchUsersAsync(string query)
{
try
{
Dictionary queryParams = new() { { "q", query } };
ApiResponse> response = await GetAsync>("api/v1/users/search/", queryParams);
return response.Ok && response.Data != null ? response.Data : new List();
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка поиска: {ex.Message}");
return new List();
}
}
///
/// Обновить статус пользователя
///
public async Task UpdateStatusAsync(bool isOnline)
{
try
{
ApiResponse