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 response = await PutAsync("api/v1/users/status/", new { isOnline }); return response.Ok; } catch (Exception ex) { Trace.WriteLine($"Ошибка обновления статуса: {ex.Message}"); return false; } } /// /// Загрузить изображение (в виде байтов) с авторизацией /// public async Task GetImageBytesAsync(string url) { try { if (string.IsNullOrEmpty(url)) return null; // Если URL относительный - добавляем базу if (!url.StartsWith("http")) { url = _config.BaseUrl.TrimEnd('/') + "/" + url.TrimStart('/'); } await EnsureTokensLoadedAsync(); // Добавляем токен, если есть HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); if (!string.IsNullOrEmpty(_accessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken); } HttpResponseMessage response = await _httpClient.SendAsync(request); if (response.IsSuccessStatusCode) { string? mediaType = response.Content.Headers.ContentType?.MediaType; if (mediaType != null && !mediaType.StartsWith("image/")) { Trace.WriteLine($"[ApiService] ⚠️ URL {url} вернул не изображение, а {mediaType}"); return null; } byte[] bytes = await response.Content.ReadAsByteArrayAsync(); Trace.WriteLine($"[ApiService] ✅ Загружено изображение {url} ({bytes.Length} bytes)"); return bytes; } Trace.WriteLine($"[ApiService] Ошибка загрузки изображения {url}: {response.StatusCode}"); return null; } catch (Exception ex) { Trace.WriteLine($"[ApiService] Исключение при загрузке изображения: {ex.Message}"); return null; } } /// /// Предзагрузить аватары для списка пользователей /// public async Task PreloadUserAvatarsAsync(IEnumerable users) { var userList = users.ToList(); var tasks = new List(); foreach (var user in userList) { // Пропускаем, если аватар уже загружен if (user.IsAvatarLoaded) { continue; } // Если Base64 - добавляем в очередь загрузки if (user.IsAvatarBase64) { tasks.Add(LoadUserAvatarAsync(user)); continue; } string? url = user.DisplayAvatarUrl; if (string.IsNullOrEmpty(url)) { continue; } // Проверяем файловый кэш string? cachedPath = ImageCacheService.Instance.GetCachedFilePath(url); if (!string.IsNullOrEmpty(cachedPath) && File.Exists(cachedPath)) { // Устанавливаем путь к файлу user.SetCachedAvatarFilePath(cachedPath); } else { // Добавляем в очередь загрузки tasks.Add(LoadUserAvatarAsync(user)); } } if (tasks.Count > 0) { Trace.WriteLine($"[ApiService] Загрузка {tasks.Count} аватаров..."); await Task.WhenAll(tasks); Trace.WriteLine($"[ApiService] ✅ Аватары загружены"); } } /// /// Загрузить аватар для одного пользователя (файловый кэш) /// public async Task LoadUserAvatarAsync(User user) { try { // Проверяем, если аватар уже загружен if (user.IsAvatarLoaded) { Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}: Already loaded"); return; } // Сначала проверяем Base64 формат if (user.IsAvatarBase64) { Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}: Base64 avatar detected"); byte[]? imageBytes = user.GetAvatarBytesFromBase64(); if (imageBytes != null && imageBytes.Length > 0) { // Генерируем уникальный путь для файла string cacheDir = Path.Combine(FileSystem.CacheDirectory, "avatars"); if (!Directory.Exists(cacheDir)) Directory.CreateDirectory(cacheDir); string filePath = Path.Combine(cacheDir, $"avatar_{user.Id}.jpg"); await File.WriteAllBytesAsync(filePath, imageBytes); user.SetCachedAvatarFilePath(filePath); Trace.WriteLine($"[ApiService] ✅ Base64 аватар сохранён: {filePath} ({imageBytes.Length} bytes)"); return; } else { Trace.WriteLine($"[ApiService] ⚠️ Не удалось декодировать Base64 аватар для {user.FullName}"); } } // Далее проверяем URL string? url = user.DisplayAvatarUrl; Trace.WriteLine($"[ApiService] LoadUserAvatarAsync for {user.FullName}, URL: {url}"); if (string.IsNullOrEmpty(url)) { Trace.WriteLine($"[ApiService] ⚠️ URL пустой для {user.FullName}"); return; } // Загружаем через файловый кэш string? loadedFilePath = await ImageCacheService.Instance.GetOrLoadImageFileAsync(url, GetImageBytesAsync); Trace.WriteLine($"[ApiService] FilePath result: {loadedFilePath}"); if (!string.IsNullOrEmpty(loadedFilePath) && File.Exists(loadedFilePath)) { user.SetCachedAvatarFilePath(loadedFilePath); Trace.WriteLine($"[ApiService] ✅ Аватар установлен: {loadedFilePath}"); } else { Trace.WriteLine($"[ApiService] ⚠️ Файл не существует или путь пустой"); } } catch (Exception ex) { Trace.WriteLine($"[ApiService] ❌ Ошибка предзагрузки аватара для {user.FullName}: {ex.Message}"); } } #endregion #region IDisposable public void Dispose() { _httpClient.Dispose(); GC.SuppressFinalize(this); } #endregion } /// /// Модель для получения JWT токенов /// internal class TokenResponse { public string? Access { get; set; } public string? Refresh { get; set; } } internal class LoginRequest { [JsonPropertyName("email")] public string Email { get; set; } = string.Empty; [JsonPropertyName("password")] public string Password { get; set; } = string.Empty; } internal class RefreshRequest { [JsonPropertyName("refresh")] public string Refresh { get; set; } = string.Empty; }