Update API documentation and fix warnings
This commit is contained in:
parent
036b9737b1
commit
674e470272
@ -1,6 +1,11 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApplication_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff22149983bac48248d71c7a379de87496f7620_003F5f_003F5808932b_003FApplication_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AApplyPropertiesVisitor_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3687bba8cb9dcb48ecc525e424af18af03833292dd05261903e25b49fb3a7ae_003FApplyPropertiesVisitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADebugProvider_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbe397279dcbf6827668054d71291b63f75cef2f116f65944ddffcca6d8d113f_003FDebugProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADebug_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F56b895832ba1dd5dcf6eab77be40a0537da9d1bf60cfbac19515378f1ac9ca13_003FDebug_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMauiWinUIApplication_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Feb4628845efd624a4bdad5d73b53774c81232eee27ca2aa293d869e5e566_003FMauiWinUIApplication_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APages_005FProfilePage_002Examl_002Esg_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2dc829dd3f62e98b28b779bb99a37aa7f9c521_003FPages_005FProfilePage_002Examl_002Esg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObject_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9a6b1457cbcf17db31a383ba49ef9bcc786cf3ef77146d997eee499b27a46d_003FObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APages_005FProfilePage_002Examl_002Esg_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2dc829dd3f62e98b28b779bb99a37aa7f9c521_003FPages_005FProfilePage_002Examl_002Esg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F876cd892fc66a9dc8f6afd3704c264acebdfc46aed08089463e8117c21a532_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||
@ -1,4 +1,6 @@
|
||||
using CORP_MAUI.Pages;
|
||||
using CORP_MAUI.Models;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CORP_MAUI;
|
||||
|
||||
@ -6,23 +8,161 @@ public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
Trace.WriteLine("╔═════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ [App] APPLICATION CONSTRUCTOR STARTED ║");
|
||||
Trace.WriteLine("╚═════════════════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
#if DEBUG
|
||||
Trace.WriteLine("[App] ✅ DEBUG MODE IS ACTIVE!");
|
||||
#else
|
||||
Trace.WriteLine("[App] ⚠️ DEBUG MODE IS NOT ACTIVE!");
|
||||
#endif
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Trace.WriteLine("[App] ✅ Application initialized");
|
||||
}
|
||||
|
||||
protected override Window CreateWindow(IActivationState? activationState)
|
||||
{
|
||||
// Проверяем, есть ли сохраненный токен
|
||||
var hasToken = SecureStorage.Default.GetAsync("auth_token").Result != null;
|
||||
|
||||
if (hasToken)
|
||||
Trace.WriteLine("[App] >>> CreateWindow called");
|
||||
|
||||
// Создаем окно с начальной страницей загрузки
|
||||
#if ANDROID || IOS || MACCATALYST || WINDOWS
|
||||
var loadingPage = new ContentPage
|
||||
{
|
||||
Content = new VerticalStackLayout
|
||||
{
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
Spacing = 20,
|
||||
Children =
|
||||
{
|
||||
new ActivityIndicator
|
||||
{
|
||||
IsRunning = true,
|
||||
Color = Colors.Blue
|
||||
},
|
||||
new Label
|
||||
{
|
||||
Text = "Загрузка...",
|
||||
HorizontalOptions = LayoutOptions.Center
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var window = new Window(loadingPage);
|
||||
#else
|
||||
// Фолбэк для платформ вне целевых (не должен использоваться в текущих TFM)
|
||||
var window = new Window();
|
||||
#endif
|
||||
|
||||
// Асинхронно проверяем токен и определяем начальную страницу
|
||||
_ = InitializeAppAsync(window);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
private async Task InitializeAppAsync(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Пользователь уже вошел в систему
|
||||
return new Window(new AppShell());
|
||||
await CheckAuthenticationAndNavigate(window);
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Показываем страницу входа
|
||||
return new Window(new NavigationPage(new LoginPage()));
|
||||
Trace.WriteLine($"[App] ❌ FATAL: InitializeAppAsync failed: {ex.Message}");
|
||||
Trace.WriteLine($"[App] Exception type: {ex.GetType().Name}");
|
||||
Trace.WriteLine($"[App] Stack trace: {ex.StackTrace}");
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
window.Page = new NavigationPage(new LoginPage());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckAuthenticationAndNavigate(Window window)
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.WriteLine("[App] >>> Checking authentication status...");
|
||||
|
||||
// Пытаемся получить данные текущего пользователя
|
||||
// Это автоматически загрузит токены из SecureStorage и проверит их валидность
|
||||
User? user = null;
|
||||
|
||||
try
|
||||
{
|
||||
user = await ServiceProvider.ApiService.GetCurrentUserAsync();
|
||||
}
|
||||
catch (Exception userEx)
|
||||
{
|
||||
Trace.WriteLine($"[App] ⚠️ GetCurrentUserAsync failed: {userEx.Message}");
|
||||
// Если не удалось получить пользователя, значит токен невалиден или отсутствует
|
||||
user = null;
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
// ✅ Токен валиден, пользователь авторизован
|
||||
Trace.WriteLine($"[App] ✅ User authenticated: {user.Username} (ID: {user.Id})");
|
||||
Trace.WriteLine($"[App] Email: {user.Email}");
|
||||
|
||||
// Подключаемся к WebSocket
|
||||
try
|
||||
{
|
||||
Trace.WriteLine("[App] >>> Connecting to WebSocket...");
|
||||
var token = ServiceProvider.ApiService.GetAccessToken();
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
await ServiceProvider.WebSocketService.ConnectAsync(token);
|
||||
Trace.WriteLine("[App] ✅ WebSocket connected successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine("[App] ⚠️ No access token available for WebSocket");
|
||||
}
|
||||
}
|
||||
catch (Exception wsEx)
|
||||
{
|
||||
Trace.WriteLine($"[App] ⚠️ WebSocket connection failed: {wsEx.Message}");
|
||||
// Продолжаем работу без WebSocket - это не критично
|
||||
}
|
||||
|
||||
// Переходим сразу на главную страницу (AppShell)
|
||||
Trace.WriteLine("[App] >>> Navigating to AppShell");
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
window.Page = new AppShell();
|
||||
Trace.WriteLine("[App] ✅ Navigation to AppShell completed");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// ❌ Токен отсутствует или невалиден, показываем страницу входа
|
||||
Trace.WriteLine("[App] ℹ️ No valid authentication found");
|
||||
Trace.WriteLine("[App] >>> Showing LoginPage");
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
window.Page = new NavigationPage(new LoginPage());
|
||||
Trace.WriteLine("[App] ✅ Navigation to LoginPage completed");
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[App] ❌ Authentication check failed: {ex.Message}");
|
||||
Trace.WriteLine($"[App] Exception type: {ex.GetType().Name}");
|
||||
Trace.WriteLine($"[App] >>> Fallback to LoginPage");
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
window.Page = new NavigationPage(new LoginPage());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,22 +3,18 @@
|
||||
x:Class="CORP_MAUI.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:pages="clr-namespace:CORP_MAUI.Pages"
|
||||
Title="Корпоративный портал"
|
||||
FlyoutBackgroundColor="{StaticResource Primary}">
|
||||
xmlns:pages="clr-namespace:CORP_MAUI.Pages">
|
||||
|
||||
<TabBar>
|
||||
<ShellContent
|
||||
Title="Новости"
|
||||
ContentTemplate="{DataTemplate pages:PostsPage}" />
|
||||
<ShellContent
|
||||
Title="Сотрудники"
|
||||
Icon="dotnet_bot.png"
|
||||
ContentTemplate="{DataTemplate pages:UsersPage}"
|
||||
Route="UsersPage" />
|
||||
|
||||
ContentTemplate="{DataTemplate pages:UsersPage}" />
|
||||
<ShellContent
|
||||
Title="Профиль"
|
||||
Icon="dotnet_bot.png"
|
||||
ContentTemplate="{DataTemplate pages:ProfilePage}"
|
||||
Route="ProfilePage" />
|
||||
ContentTemplate="{DataTemplate pages:ProfilePage}" />
|
||||
</TabBar>
|
||||
|
||||
</Shell>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using CORP_MAUI.Pages;
|
||||
using System.Diagnostics;
|
||||
using CORP_MAUI.Pages;
|
||||
|
||||
namespace CORP_MAUI;
|
||||
|
||||
@ -6,39 +7,103 @@ public partial class AppShell : Shell
|
||||
{
|
||||
public AppShell()
|
||||
{
|
||||
InitializeComponent();
|
||||
try
|
||||
{
|
||||
Trace.WriteLine("=== APPSHELL CONSTRUCTOR START ===");
|
||||
Trace.WriteLine("[AppShell] >>> Вызов InitializeComponent");
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Trace.WriteLine("[AppShell] >>> InitializeComponent завершен");
|
||||
Trace.WriteLine("[AppShell] >>> Регистрация маршрутов");
|
||||
|
||||
// Регистрация маршрутов для навигации
|
||||
Routing.RegisterRoute(nameof(ChatPage), typeof(ChatPage));
|
||||
// Регистрация маршрутов для навигации
|
||||
Routing.RegisterRoute(nameof(ChatPage), typeof(ChatPage));
|
||||
|
||||
// Инициализация при загрузке
|
||||
_ = InitializeAsync();
|
||||
Trace.WriteLine("[AppShell] ✅ AppShell инициализирован");
|
||||
Trace.WriteLine("=== APPSHELL CONSTRUCTOR END ===");
|
||||
|
||||
// Инициализация без ожидания (так await нельзя из синхронного конструктора)
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("=== APPSHELL CONSTRUCTOR ERROR ===");
|
||||
Trace.WriteLine($"[AppShell] ❌ Тип ошибки: {ex.GetType().FullName}");
|
||||
Trace.WriteLine($"[AppShell] ❌ Сообщение: {ex.Message}");
|
||||
Trace.WriteLine($"[AppShell] ❌ StackTrace: {ex.StackTrace}");
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Trace.WriteLine($"[AppShell] ❌ InnerException: {ex.InnerException.Message}");
|
||||
Trace.WriteLine($"[AppShell] ❌ InnerException StackTrace: {ex.InnerException.StackTrace}");
|
||||
}
|
||||
|
||||
// Показываем ошибку в диалоге
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
await MainThread.InvokeOnMainThreadAsync(async () =>
|
||||
{
|
||||
var mainPage = Application.Current?.Windows[0]?.Page;
|
||||
if (mainPage != null)
|
||||
{
|
||||
await mainPage.DisplayAlert("Ошибка AppShell",
|
||||
$"Ошибка инициализации AppShell:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Инициализируем сервисы
|
||||
Trace.WriteLine("[AppShell] >>> Начало InitializeAsync");
|
||||
|
||||
// Инициализация сервисов (если они не инициализированы)
|
||||
ServiceProvider.Initialize();
|
||||
|
||||
Trace.WriteLine($"[AppShell] IsAuthenticated: {ServiceProvider.ApiService.IsAuthenticated}");
|
||||
|
||||
// Проверяем, авторизован ли пользователь (токены загружаются автоматически в ApiService)
|
||||
if (ServiceProvider.ApiService.IsAuthenticated)
|
||||
{
|
||||
// Получаем токен для WebSocket
|
||||
var accessToken = await SecureStorage.GetAsync("api.jwt_access");
|
||||
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
Trace.WriteLine($"[AppShell] Access token exists: {!string.IsNullOrEmpty(accessToken)}");
|
||||
Trace.WriteLine($"[AppShell] WebSocket connected: {ServiceProvider.WebSocketService.IsConnected}");
|
||||
|
||||
if (!string.IsNullOrEmpty(accessToken) && !ServiceProvider.WebSocketService.IsConnected)
|
||||
{
|
||||
// Подключаем WebSocket
|
||||
Trace.WriteLine("[AppShell] >>> Подключение WebSocket из AppShell");
|
||||
// Подключаем WebSocket только если его еще не подключен
|
||||
await ServiceProvider.WebSocketService.ConnectAsync(accessToken);
|
||||
Trace.WriteLine("[AppShell] ✅ WebSocket подключен из AppShell");
|
||||
}
|
||||
else if (ServiceProvider.WebSocketService.IsConnected)
|
||||
{
|
||||
Trace.WriteLine("[AppShell] WebSocket уже подключен, пропускаем");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine("[AppShell] ⚠️ Пользователь не авторизован");
|
||||
}
|
||||
|
||||
Trace.WriteLine("[AppShell] ✅ InitializeAsync завершен");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[AppShell] ❌ Ошибка InitializeAsync: {ex.Message}");
|
||||
Trace.WriteLine($"[AppShell] StackTrace: {ex.StackTrace}");
|
||||
|
||||
await DisplayAlert("Ошибка", $"Ошибка инициализации: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -39,8 +39,13 @@
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||
<Configurations>Debug;Release;Android</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
|
||||
<!-- Путь к JDK для Android сборки -->
|
||||
<JavaSdkDirectory>D:\Applications\Java\JDK\Temurin\jdk-21.0.9+10</JavaSdkDirectory>
|
||||
|
||||
<!-- Define constants for Debug/Release -->
|
||||
<DefineConstants Condition="'$(Configuration)' == 'Debug'">DEBUG;TRACE</DefineConstants>
|
||||
<DefineConstants Condition="'$(Configuration)' == 'Release'">TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CORP_MAUI;
|
||||
|
||||
public static class DebugCheck
|
||||
{
|
||||
public static void PrintDebugStatus()
|
||||
{
|
||||
#if DEBUG
|
||||
Trace.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ ✅ DEBUG СИМВОЛ ОПРЕДЕЛЕН! Режим отладки АКТИВЕН! ║");
|
||||
Trace.WriteLine("╚═════════════════════════════════════════════════════════════════════════════════╝");
|
||||
#else
|
||||
Trace.WriteLine("╔═════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ ❌ DEBUG СИМВОЛ НЕ ОПРЕДЕЛЕН! Режим отладки НЕ АКТИВЕН! ║");
|
||||
Trace.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CORP_MAUI;
|
||||
|
||||
public static class DebugTest
|
||||
{
|
||||
static DebugTest()
|
||||
{
|
||||
#if DEBUG
|
||||
Trace.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ ✅ DEBUG IS DEFINED ✅ ║");
|
||||
Trace.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝");
|
||||
#else
|
||||
Trace.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ ❌ DEBUG IS NOT DEFINED ❌ ║");
|
||||
Trace.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝");
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Check()
|
||||
{
|
||||
// Пустой метод для вызова статического конструктора
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using CORP_MAUI.Services;
|
||||
using CORP_MAUI.Pages;
|
||||
|
||||
namespace CORP_MAUI;
|
||||
|
||||
@ -16,9 +18,26 @@ public static class MauiProgram
|
||||
});
|
||||
|
||||
#if DEBUG
|
||||
builder.Logging.AddDebug();
|
||||
Trace.Listeners.Clear();
|
||||
Trace.Listeners.Add(new DefaultTraceListener());
|
||||
Trace.AutoFlush = true;
|
||||
|
||||
Trace.WriteLine("╔══════════════════════════════════════════════════════════════════════════════════╗");
|
||||
Trace.WriteLine("║ [MauiProgram] DEBUG MODE ENABLED - TRACE LISTENER CONFIGURED ║");
|
||||
Trace.WriteLine("╚══════════════════════════════════════════════════════════════════════════════════╝");
|
||||
#endif
|
||||
|
||||
// Регистрируем сервисы
|
||||
builder.Services.AddSingleton<ApiService>();
|
||||
builder.Services.AddSingleton<WebSocketService>();
|
||||
|
||||
// Регистрируем страницы
|
||||
builder.Services.AddTransient<LoginPage>();
|
||||
builder.Services.AddTransient<UsersPage>();
|
||||
builder.Services.AddTransient<PostsPage>();
|
||||
builder.Services.AddTransient<ChatPage>();
|
||||
builder.Services.AddTransient<ProfilePage>();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель события календаря
|
||||
/// </summary>
|
||||
public class CalendarEvent
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public string Location { get; set; } = string.Empty;
|
||||
public List<User> Participants { get; set; } = new();
|
||||
public User? Creator { get; set; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель комментария
|
||||
/// </summary>
|
||||
public class Comment
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
public string? Body { get; set; }
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string DisplayContent
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Content)) return Content;
|
||||
if (!string.IsNullOrWhiteSpace(Body)) return Body;
|
||||
return Text ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public User? Author { get; set; }
|
||||
|
||||
[JsonPropertyName("post")]
|
||||
public int Post { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("date")]
|
||||
public DateTime? Date { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime DisplayDate
|
||||
{
|
||||
get
|
||||
{
|
||||
// Если CreatedAt валидный (больше 2000 года, чтоб отсечь дефолт), берем его
|
||||
if (CreatedAt.Year > 2000) return CreatedAt;
|
||||
// Иначе пробуем поле Date
|
||||
if (Date.HasValue) return Date.Value;
|
||||
// Иначе текущее время
|
||||
return DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("likes_count")]
|
||||
public int LikesCount { get; set; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель отдела
|
||||
/// </summary>
|
||||
public class Department
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int? Parent { get; set; }
|
||||
public int EmployeesCount { get; set; }
|
||||
public User? Head { get; set; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель роли в отделе
|
||||
/// </summary>
|
||||
public class DepartmentRole
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public Department? Department { get; set; }
|
||||
public List<string> Permissions { get; set; } = new();
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель документа
|
||||
/// </summary>
|
||||
public class Document
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public User? Author { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель действия сотрудника
|
||||
/// </summary>
|
||||
public class EmployeeAction
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public User? Employee { get; set; }
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
public object? Extra { get; set; }
|
||||
public List<object> History { get; set; } = new();
|
||||
public string ActionDisplay { get; set; } = string.Empty;
|
||||
public DateTime Date { get; set; }
|
||||
public string Action { get; set; } = string.Empty;
|
||||
public string DateDisplay { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Обобщенная модель для пагинированных ответов
|
||||
/// </summary>
|
||||
public class PaginatedResponse<T>
|
||||
{
|
||||
public int Count { get; set; }
|
||||
public string? Next { get; set; }
|
||||
public string? Previous { get; set; }
|
||||
public List<T> Results { get; set; } = new();
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель должности
|
||||
/// </summary>
|
||||
public class Position
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<int> Groups { get; set; } = new();
|
||||
public List<string> GroupsVerbose { get; set; } = new();
|
||||
}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель поста/новости
|
||||
/// </summary>
|
||||
public class Post
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
private string _content = string.Empty;
|
||||
[JsonPropertyName("body")]
|
||||
public string Content
|
||||
{
|
||||
get => !string.IsNullOrWhiteSpace(_content) ? _content : (Text ?? string.Empty);
|
||||
set => _content = value;
|
||||
}
|
||||
|
||||
[JsonPropertyName("image_url")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
[JsonPropertyName("image")]
|
||||
public string? Image { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? DisplayImageUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
var url = !string.IsNullOrEmpty(ImageUrl) ? ImageUrl : Image;
|
||||
if (string.IsNullOrEmpty(url)) return null;
|
||||
|
||||
// Если ссылка уже абсолютная - возвращаем как есть
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return url;
|
||||
|
||||
// Если ссылка относительная - добавляем домен
|
||||
// Обрабатываем случай, когда url начинается со / или без
|
||||
var cleanUrl = url.TrimStart('/');
|
||||
return $"https://corp.robotail.pro/{cleanUrl}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("date")]
|
||||
public DateTime? Date { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime DisplayDate => CreatedAt.Year > 2000 ? CreatedAt : (Date ?? DateTime.Now);
|
||||
|
||||
public User? Author { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("likes_count")]
|
||||
public int LikesCount { get; set; }
|
||||
|
||||
[JsonPropertyName("comments_count")]
|
||||
public int CommentsCount { get; set; }
|
||||
|
||||
public List<string> Tags { get; set; } = new();
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель запроса (отпуск, заявка и т.д.)
|
||||
/// </summary>
|
||||
public class Request
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public User? Requester { get; set; }
|
||||
public User? Approver { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Модель навыка
|
||||
/// </summary>
|
||||
public class Skill
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CORP_MAUI.Models;
|
||||
|
||||
public class User
|
||||
@ -5,10 +7,55 @@ public class User
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string FullName { get; set; } = string.Empty;
|
||||
public string Department { get; set; } = string.Empty;
|
||||
public string Position { get; set; } = string.Empty;
|
||||
public string AvatarUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("first_name")]
|
||||
public string FirstName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("last_name")]
|
||||
public string LastName { get; set; } = string.Empty;
|
||||
|
||||
// Может быть либо объектом Department, либо строкой (для обратной совместимости)
|
||||
public object? Department { get; set; }
|
||||
|
||||
// Может быть либо объектом Position, либо строкой (для обратной совместимости)
|
||||
public object? Position { get; set; }
|
||||
|
||||
public List<Skill>? Skills { get; set; }
|
||||
|
||||
[JsonPropertyName("is_online")]
|
||||
public bool IsOnline { get; set; }
|
||||
|
||||
[JsonPropertyName("avatar_url")]
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
// Вспомогательные свойства для удобства
|
||||
[JsonIgnore]
|
||||
public string FullName => $"{FirstName} {LastName}".Trim();
|
||||
|
||||
[JsonIgnore]
|
||||
public string DepartmentName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Department is Department dept)
|
||||
return dept.Name;
|
||||
if (Department is string str)
|
||||
return str;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string PositionName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Position is Position pos)
|
||||
return pos.Name;
|
||||
if (Position is string str)
|
||||
return str;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ public partial class ChatPage : ContentPage
|
||||
|
||||
// Подписываемся на события WebSocket
|
||||
ServiceProvider.WebSocketService.MessageReceived += OnMessageReceived;
|
||||
ServiceProvider.WebSocketService.MessageHistoryReceived += OnMessageHistoryReceived;
|
||||
ServiceProvider.WebSocketService.ConnectionStatusChanged += OnConnectionStatusChanged;
|
||||
|
||||
// Инициализация
|
||||
@ -40,11 +41,18 @@ public partial class ChatPage : ContentPage
|
||||
}
|
||||
|
||||
// Отображаем информацию о получателе
|
||||
RecipientName.Text = _recipient.FullName ?? _recipient.Username;
|
||||
RecipientName.Text = _recipient.FullName;
|
||||
RecipientStatus.Text = _recipient.IsOnline ? "В сети" : "Не в сети";
|
||||
|
||||
// Загружаем историю сообщений
|
||||
await LoadMessagesAsync();
|
||||
// Запрашиваем историю сообщений через WebSocket
|
||||
if (ServiceProvider.WebSocketService.IsConnected)
|
||||
{
|
||||
await ServiceProvider.WebSocketService.RequestMessageHistoryAsync(_recipient.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await DisplayAlert("Предупреждение", "WebSocket не подключен. История сообщений недоступна.", "OK");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -52,37 +60,6 @@ public partial class ChatPage : ContentPage
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMessagesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = await ServiceProvider.ApiService.GetMessagesAsync(_recipient.Id);
|
||||
|
||||
_messages.Clear();
|
||||
foreach (var message in messages.OrderBy(m => m.Timestamp))
|
||||
{
|
||||
_messages.Add(new MessageViewModel
|
||||
{
|
||||
Id = message.Id,
|
||||
SenderId = message.SenderId,
|
||||
SenderName = message.SenderName,
|
||||
Content = message.Content,
|
||||
Timestamp = message.Timestamp,
|
||||
IsMyMessage = message.SenderId == _currentUser?.Id
|
||||
});
|
||||
}
|
||||
|
||||
// Прокручиваем к последнему сообщению
|
||||
if (_messages.Any())
|
||||
{
|
||||
MessagesCollectionView.ScrollTo(_messages.Last(), position: ScrollToPosition.End, animate: false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DisplayAlert("Ошибка", $"Не удалось загрузить сообщения: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSendMessageClicked(object sender, EventArgs e)
|
||||
{
|
||||
@ -104,17 +81,15 @@ public partial class ChatPage : ContentPage
|
||||
Type = MessageType.Personal
|
||||
};
|
||||
|
||||
// Отправляем через WebSocket
|
||||
if (ServiceProvider.WebSocketService.IsConnected)
|
||||
// Отправляем только через WebSocket
|
||||
if (!ServiceProvider.WebSocketService.IsConnected)
|
||||
{
|
||||
await ServiceProvider.WebSocketService.SendMessageAsync(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Резервный вариант через REST API
|
||||
await ServiceProvider.ApiService.SendMessageAsync(message);
|
||||
await DisplayAlert("Ошибка", "WebSocket не подключен. Невозможно отправить сообщение.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
await ServiceProvider.WebSocketService.SendMessageAsync(message);
|
||||
|
||||
// Добавляем сообщение в список
|
||||
_messages.Add(new MessageViewModel
|
||||
{
|
||||
@ -181,6 +156,33 @@ public partial class ChatPage : ContentPage
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessageHistoryReceived(object? sender, List<Message> history)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
_messages.Clear();
|
||||
|
||||
foreach (var message in history.OrderBy(m => m.Timestamp))
|
||||
{
|
||||
_messages.Add(new MessageViewModel
|
||||
{
|
||||
Id = message.Id,
|
||||
SenderId = message.SenderId,
|
||||
SenderName = message.SenderName,
|
||||
Content = message.Content,
|
||||
Timestamp = message.Timestamp,
|
||||
IsMyMessage = message.SenderId == _currentUser?.Id
|
||||
});
|
||||
}
|
||||
|
||||
// Прокручиваем к последнему сообщению
|
||||
if (_messages.Any())
|
||||
{
|
||||
MessagesCollectionView.ScrollTo(_messages.Last(), position: ScrollToPosition.End, animate: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(object? sender, bool isConnected)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
@ -195,6 +197,7 @@ public partial class ChatPage : ContentPage
|
||||
|
||||
// Отписываемся от событий
|
||||
ServiceProvider.WebSocketService.MessageReceived -= OnMessageReceived;
|
||||
ServiceProvider.WebSocketService.MessageHistoryReceived -= OnMessageHistoryReceived;
|
||||
ServiceProvider.WebSocketService.ConnectionStatusChanged -= OnConnectionStatusChanged;
|
||||
|
||||
_typingTimer?.Dispose();
|
||||
|
||||
@ -7,21 +7,23 @@ public partial class LoginPage : ContentPage
|
||||
public LoginPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
Debug.WriteLine("[LoginPage] Страница входа инициализирована");
|
||||
Trace.WriteLine("[LoginPage] Страница входа инициализирована");
|
||||
}
|
||||
|
||||
private async void OnLoginClicked(object sender, EventArgs e)
|
||||
{
|
||||
Trace.WriteLine("=== LOGIN BUTTON CLICKED ===");
|
||||
|
||||
var username = UsernameEntry.Text?.Trim();
|
||||
var password = PasswordEntry.Text?.Trim();
|
||||
|
||||
Debug.WriteLine($"\n[LoginPage] >>> Кнопка входа нажата");
|
||||
Debug.WriteLine($"[LoginPage] Username: {username}");
|
||||
Trace.WriteLine($"[LoginPage] >>> Кнопка входа нажата");
|
||||
Trace.WriteLine($"[LoginPage] Username: {username}");
|
||||
|
||||
// Валидация
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ❌ Валидация не прошла - пустые поля");
|
||||
Trace.WriteLine($"[LoginPage] ❌ Валидация не прошла - пустые поля");
|
||||
await ShowError("Пожалуйста, заполните все поля");
|
||||
return;
|
||||
}
|
||||
@ -31,18 +33,18 @@ public partial class LoginPage : ContentPage
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] >>> Вызываем ApiService.LoginAsync...\n");
|
||||
Trace.WriteLine($"[LoginPage] >>> Вызываем ApiService.LoginAsync...");
|
||||
|
||||
// Выполняем вход
|
||||
var result = await ServiceProvider.ApiService.LoginAsync(username, password);
|
||||
|
||||
Debug.WriteLine($"\n[LoginPage] <<< Получен результат логина");
|
||||
Debug.WriteLine($"[LoginPage] Success: {result.Success}");
|
||||
Debug.WriteLine($"[LoginPage] Message: {result.Message}");
|
||||
Trace.WriteLine($"[LoginPage] <<< Получен результат логина");
|
||||
Trace.WriteLine($"[LoginPage] Success: {result.Success}");
|
||||
Trace.WriteLine($"[LoginPage] Message: {result.Message}");
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ✅ Логин успешен! Переходим к WebSocket...");
|
||||
Trace.WriteLine($"[LoginPage] ✅ Логин успешен! Переходим к WebSocket...");
|
||||
|
||||
// Подключаемся к WebSocket
|
||||
try
|
||||
@ -52,24 +54,21 @@ public partial class LoginPage : ContentPage
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ❌ Токен отсутствует!");
|
||||
Trace.WriteLine($"[LoginPage] ❌ Токен отсутствует!");
|
||||
await DisplayAlert("Ошибка", "Не удалось получить токен авторизации", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[LoginPage] >>> Подключение к WebSocket...");
|
||||
Debug.WriteLine($"[LoginPage] >>> Token (first 30 chars): {token.Substring(0, Math.Min(30, token.Length))}...");
|
||||
Debug.WriteLine($"[LoginPage] >>> Token length: {token.Length}");
|
||||
Trace.WriteLine($"[LoginPage] >>> Подключение к WebSocket...");
|
||||
|
||||
await ServiceProvider.WebSocketService.ConnectAsync(token);
|
||||
|
||||
Debug.WriteLine($"[LoginPage] ✅ WebSocket подключен!");
|
||||
Trace.WriteLine($"[LoginPage] ✅ WebSocket подключен!");
|
||||
}
|
||||
catch (Exception wsEx)
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ❌ WebSocket ошибка: {wsEx.GetType().Name}");
|
||||
Debug.WriteLine($"[LoginPage] Message: {wsEx.Message}");
|
||||
Debug.WriteLine($"[LoginPage] StackTrace:\n{wsEx.StackTrace}");
|
||||
Trace.WriteLine($"[LoginPage] ❌ WebSocket ошибка: {wsEx.GetType().Name}");
|
||||
Trace.WriteLine($"[LoginPage] Message: {wsEx.Message}");
|
||||
|
||||
// Показываем предупреждение, но продолжаем работу
|
||||
await DisplayAlert("Предупреждение",
|
||||
@ -80,28 +79,61 @@ public partial class LoginPage : ContentPage
|
||||
// Сохраняем данные пользователя (токены уже сохранены в ApiService)
|
||||
if (result.User != null)
|
||||
{
|
||||
await SecureStorage.SetAsync("user_id", result.User.Id.ToString());
|
||||
await SecureStorage.SetAsync("username", result.User.Username);
|
||||
try
|
||||
{
|
||||
await SecureStorage.SetAsync("user_id", result.User.Id.ToString());
|
||||
await SecureStorage.SetAsync("username", result.User.Username);
|
||||
Trace.WriteLine($"[LoginPage] ✅ Данные пользователя сохранены в SecureStorage");
|
||||
}
|
||||
catch (Exception storageEx)
|
||||
{
|
||||
Trace.WriteLine($"[LoginPage] ⚠️ Ошибка сохранения в SecureStorage: {storageEx.Message}");
|
||||
// Продолжаем работу, даже если не удалось сохранить в SecureStorage
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[LoginPage] >>> Переход на главную страницу");
|
||||
Trace.WriteLine($"[LoginPage] >>> ПЕРЕХОД НА APPSHELL");
|
||||
// Переходим на главную страницу
|
||||
if (Application.Current?.Windows.Count > 0)
|
||||
try
|
||||
{
|
||||
Application.Current.Windows[0].Page = new AppShell();
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
Trace.WriteLine($"[LoginPage] >>> Создание AppShell...");
|
||||
var app = Application.Current;
|
||||
if (app != null && app.Windows.Count > 0)
|
||||
{
|
||||
app.Windows[0].Page = new AppShell();
|
||||
Trace.WriteLine($"[LoginPage] >>> AppShell установлен через Windows[0].Page");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"[LoginPage] ⚠️ Нет активных окон приложения");
|
||||
}
|
||||
});
|
||||
Trace.WriteLine($"[LoginPage] ✅ Успешный переход на AppShell");
|
||||
}
|
||||
catch (Exception navEx)
|
||||
{
|
||||
Trace.WriteLine($"[LoginPage] ❌❌❌ ОШИБКА НАВИГАЦИИ ❌❌❌");
|
||||
Trace.WriteLine($"[LoginPage] Type: {navEx.GetType().Name}");
|
||||
Trace.WriteLine($"[LoginPage] Message: {navEx.Message}");
|
||||
Trace.WriteLine($"[LoginPage] StackTrace: {navEx.StackTrace}");
|
||||
await DisplayAlert("Ошибка навигации", $"{navEx.Message}\n\n{navEx.StackTrace}", "OK");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ❌ Логин неуспешен");
|
||||
Trace.WriteLine($"[LoginPage] ❌ Логин неуспешен");
|
||||
await ShowError(result.Message ?? "Ошибка входа. Проверьте учетные данные.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[LoginPage] ❌ Exception: {ex.GetType().Name}");
|
||||
Debug.WriteLine($"[LoginPage] Message: {ex.Message}");
|
||||
Debug.WriteLine($"[LoginPage] StackTrace: {ex.StackTrace}");
|
||||
Trace.WriteLine($"[LoginPage] ❌❌❌ EXCEPTION ❌❌❌");
|
||||
Trace.WriteLine($"[LoginPage] Type: {ex.GetType().Name}");
|
||||
Trace.WriteLine($"[LoginPage] Message: {ex.Message}");
|
||||
Trace.WriteLine($"[LoginPage] StackTrace: {ex.StackTrace}");
|
||||
await ShowError($"Ошибка подключения: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
@ -130,3 +162,4 @@ public partial class LoginPage : ContentPage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
135
CORP-MAUI/Pages/PostDetailsPage.xaml
Normal file
135
CORP-MAUI/Pages/PostDetailsPage.xaml
Normal file
@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:viewModels="clr-namespace:CORP_MAUI.ViewModels"
|
||||
xmlns:models="clr-namespace:CORP_MAUI.Models"
|
||||
x:Class="CORP_MAUI.Pages.PostDetailsPage"
|
||||
x:DataType="viewModels:PostDetailsViewModel"
|
||||
Title="Публикация"
|
||||
BackgroundColor="{StaticResource Gray100}">
|
||||
|
||||
<Grid>
|
||||
<CollectionView
|
||||
x:Name="CommentsCollectionView"
|
||||
ItemsSource="{Binding Comments}"
|
||||
SelectionMode="None"
|
||||
Margin="0,0,0,20">
|
||||
|
||||
<CollectionView.Header>
|
||||
<VerticalStackLayout Spacing="15" Padding="15" BackgroundColor="White">
|
||||
<!-- Заголовок -->
|
||||
<Label Text="{Binding PostViewModel.Post.Title}"
|
||||
FontSize="22"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{StaticResource Gray900}"
|
||||
Margin="0,20,0,0"/>
|
||||
|
||||
<!-- Автор и дата -->
|
||||
<HorizontalStackLayout Spacing="10">
|
||||
<Label Text="{Binding PostViewModel.Post.Author.FullName}"
|
||||
FontSize="14"
|
||||
TextColor="{StaticResource Primary}"/>
|
||||
<Label Text="{Binding PostViewModel.Post.DisplayDate, StringFormat='{0:dd.MM.yyyy HH:mm}'}"
|
||||
FontSize="14"
|
||||
TextColor="{StaticResource Gray500}"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Image Aspect="AspectFill"
|
||||
HeightRequest="250"
|
||||
BackgroundColor="#F5F5F5"
|
||||
Source="{Binding PostViewModel.DisplayImageSource}"
|
||||
IsVisible="{Binding PostViewModel.IsImageVisible}">
|
||||
<Image.Clip>
|
||||
<RoundRectangleGeometry CornerRadius="10"/>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
|
||||
<!-- Полный текст (Content) -->
|
||||
<!-- Используем Post.Content напрямую, так как ViewModel.PreviewContent обрезан -->
|
||||
<Label Text="{Binding PostViewModel.Post.Content}"
|
||||
FontSize="16"
|
||||
TextColor="{StaticResource Gray900}"
|
||||
LineHeight="1.2"/>
|
||||
|
||||
<!-- Статистика -->
|
||||
<HorizontalStackLayout Spacing="20" Margin="0,10,0,0">
|
||||
<HorizontalStackLayout Spacing="5">
|
||||
<Label Text="❤️" />
|
||||
<Label Text="{Binding PostViewModel.Post.LikesCount}"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout Spacing="5">
|
||||
<Label Text="💬" />
|
||||
<Label Text="{Binding PostViewModel.Post.CommentsCount}"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
</HorizontalStackLayout>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<BoxView HeightRequest="1" Color="{StaticResource Gray200}" Margin="0,10" />
|
||||
|
||||
<Label Text="Комментарии" FontSize="18" FontAttributes="Bold" TextColor="{StaticResource Gray900}"/>
|
||||
|
||||
<!-- Индикатор загрузки комментариев -->
|
||||
<ActivityIndicator IsRunning="{Binding IsLoadingComments}"
|
||||
IsVisible="{Binding IsLoadingComments}"
|
||||
Color="{StaticResource Primary}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</CollectionView.Header>
|
||||
|
||||
<CollectionView.EmptyView>
|
||||
<VerticalStackLayout Padding="20" Spacing="10" IsVisible="{Binding IsNotLoadingComments}">
|
||||
<Label Text="Нет комментариев"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="{StaticResource Gray500}"/>
|
||||
</VerticalStackLayout>
|
||||
</CollectionView.EmptyView>
|
||||
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:Comment">
|
||||
<Border
|
||||
Padding="10"
|
||||
Margin="15,5"
|
||||
StrokeShape="RoundRectangle 8"
|
||||
BackgroundColor="White"
|
||||
StrokeThickness="0">
|
||||
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<HorizontalStackLayout Spacing="10">
|
||||
<Label Text="{Binding Author.FullName}"
|
||||
FontSize="13"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
<Label Text="{Binding DisplayDate, StringFormat='{0:dd.MM.yyyy HH:mm}'}"
|
||||
FontSize="11"
|
||||
TextColor="{StaticResource Gray500}"
|
||||
VerticalOptions="Center"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<Label Text="{Binding DisplayContent}"
|
||||
FontSize="14"
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
<Button Text="✕"
|
||||
FontSize="20"
|
||||
FontAttributes="Bold"
|
||||
BackgroundColor="#80FFFFFF"
|
||||
TextColor="{StaticResource Gray900}"
|
||||
HorizontalOptions="End"
|
||||
VerticalOptions="Start"
|
||||
Margin="15"
|
||||
WidthRequest="40"
|
||||
HeightRequest="40"
|
||||
CornerRadius="20"
|
||||
Padding="0"
|
||||
Clicked="OnCloseClicked"/>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
56
CORP-MAUI/Pages/PostDetailsPage.xaml.cs
Normal file
56
CORP-MAUI/Pages/PostDetailsPage.xaml.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.Diagnostics;
|
||||
using CORP_MAUI.ViewModels;
|
||||
|
||||
namespace CORP_MAUI.Pages;
|
||||
|
||||
public partial class PostDetailsPage : ContentPage
|
||||
{
|
||||
private readonly PostDetailsViewModel _viewModel;
|
||||
|
||||
public PostDetailsPage(PostViewModel postViewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
_viewModel = new PostDetailsViewModel(postViewModel);
|
||||
BindingContext = _viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (_viewModel.Comments.Count == 0)
|
||||
{
|
||||
await LoadCommentsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCommentsAsync()
|
||||
{
|
||||
_viewModel.IsLoadingComments = true;
|
||||
try
|
||||
{
|
||||
var response = await ServiceProvider.ApiService.GetCommentsPaginatedAsync(postId: _viewModel.PostViewModel.Post.Id);
|
||||
|
||||
_viewModel.Comments.Clear();
|
||||
if (response.Results != null)
|
||||
{
|
||||
foreach (var comment in response.Results)
|
||||
{
|
||||
_viewModel.Comments.Add(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[PostDetailsPage] Error loading comments: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_viewModel.IsLoadingComments = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCloseClicked(object sender, EventArgs e)
|
||||
{
|
||||
await Navigation.PopAsync();
|
||||
}
|
||||
}
|
||||
116
CORP-MAUI/Pages/PostsPage.xaml
Normal file
116
CORP-MAUI/Pages/PostsPage.xaml
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:models="clr-namespace:CORP_MAUI.ViewModels"
|
||||
x:Class="CORP_MAUI.Pages.PostsPage"
|
||||
Title="Новости">
|
||||
|
||||
<Grid>
|
||||
<ActivityIndicator
|
||||
x:Name="LoadingIndicator"
|
||||
IsRunning="False"
|
||||
IsVisible="False"
|
||||
HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
Color="{StaticResource Primary}"/>
|
||||
|
||||
<RefreshView
|
||||
x:Name="PostsRefreshView"
|
||||
IsRefreshing="False"
|
||||
Refreshing="OnRefreshing">
|
||||
|
||||
<CollectionView
|
||||
x:Name="PostsCollectionView"
|
||||
SelectionMode="Single"
|
||||
SelectionChanged="OnPostSelected">
|
||||
|
||||
<CollectionView.EmptyView>
|
||||
<VerticalStackLayout
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="Center"
|
||||
Padding="50">
|
||||
<Label
|
||||
Text="Нет новостей"
|
||||
FontSize="16"
|
||||
TextColor="{StaticResource Gray600}"
|
||||
HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</CollectionView.EmptyView>
|
||||
|
||||
<CollectionView.Header>
|
||||
<Label Text="Последние новости"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
Margin="20,10,20,0" />
|
||||
</CollectionView.Header>
|
||||
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:PostViewModel">
|
||||
<Border
|
||||
Padding="15"
|
||||
Margin="10,5"
|
||||
StrokeShape="RoundRectangle 10"
|
||||
BackgroundColor="White"
|
||||
Shadow="{Shadow Brush=Black, Opacity=0.1, Offset='0,2', Radius=4}">
|
||||
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<!-- Заголовок и метаданные -->
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label Text="{Binding Post.Title}"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
|
||||
<HorizontalStackLayout Spacing="10">
|
||||
<Label Text="{Binding Post.Author.FullName}"
|
||||
FontSize="12"
|
||||
TextColor="{StaticResource Primary}"/>
|
||||
<Label Text="{Binding Post.DisplayDate, StringFormat='{0:dd.MM.yyyy HH:mm}'}"
|
||||
FontSize="12"
|
||||
TextColor="{StaticResource Gray500}"/>
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
|
||||
<!-- Изображение -->
|
||||
<Image Aspect="AspectFill"
|
||||
HeightRequest="200"
|
||||
Margin="0,5"
|
||||
BackgroundColor="#F5F5F5"
|
||||
Source="{Binding DisplayImageSource}"
|
||||
IsVisible="{Binding IsImageVisible}">
|
||||
<Image.Clip>
|
||||
<RoundRectangleGeometry CornerRadius="10"/>
|
||||
</Image.Clip>
|
||||
</Image>
|
||||
|
||||
<!-- Контент -->
|
||||
<Label Text="{Binding PreviewContent}"
|
||||
FontSize="14"
|
||||
TextColor="{StaticResource Gray900}"
|
||||
LineBreakMode="TailTruncation"
|
||||
MaxLines="3"/>
|
||||
|
||||
<!-- Футер с лайками и комментариями -->
|
||||
<HorizontalStackLayout Spacing="20" Margin="0,5,0,0">
|
||||
<HorizontalStackLayout Spacing="5">
|
||||
<Label Text="❤️" />
|
||||
<Label Text="{Binding Post.LikesCount}"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<HorizontalStackLayout Spacing="5">
|
||||
<Label Text="💬" />
|
||||
<Label Text="{Binding Post.CommentsCount}"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
</HorizontalStackLayout>
|
||||
</HorizontalStackLayout>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
|
||||
</CollectionView>
|
||||
</RefreshView>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
167
CORP-MAUI/Pages/PostsPage.xaml.cs
Normal file
167
CORP-MAUI/Pages/PostsPage.xaml.cs
Normal file
@ -0,0 +1,167 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CORP_MAUI.ViewModels;
|
||||
|
||||
namespace CORP_MAUI.Pages;
|
||||
|
||||
public partial class PostsPage
|
||||
{
|
||||
private readonly ObservableCollection<PostViewModel> _posts;
|
||||
private int _currentPage = 1;
|
||||
private bool _isLoading;
|
||||
|
||||
public PostsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_posts = new ObservableCollection<PostViewModel>();
|
||||
PostsCollectionView.ItemsSource = _posts;
|
||||
}
|
||||
|
||||
// Добавляем метод OnAppearing, чтобы обновлять данные при показе страницы, если нужно
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (_posts.Count == 0)
|
||||
{
|
||||
_ = LoadPostsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPostsAsync(bool refresh = false)
|
||||
{
|
||||
if (_isLoading) return;
|
||||
_isLoading = true;
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
_currentPage = 1;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LoadingIndicator.IsVisible = true;
|
||||
LoadingIndicator.IsRunning = true;
|
||||
|
||||
var response = await ServiceProvider.ApiService.GetPostsPaginatedAsync(_currentPage);
|
||||
|
||||
if (refresh)
|
||||
{
|
||||
_posts.Clear();
|
||||
}
|
||||
|
||||
if (response.Results != null)
|
||||
{
|
||||
foreach (var post in response.Results)
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] Post {post.Id}: '{post.Title}'");
|
||||
Trace.WriteLine($" CreatedAt: {post.CreatedAt}");
|
||||
Trace.WriteLine($" DisplayImageUrl: {post.DisplayImageUrl}");
|
||||
Trace.WriteLine($" Content length: {post.Content?.Length ?? 0}");
|
||||
|
||||
var vm = new PostViewModel(post);
|
||||
_posts.Add(vm);
|
||||
|
||||
// Асинхронно загружаем изображение с токеном
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(post.DisplayImageUrl))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Используем имя файла из URL как ключ кэша
|
||||
var uri = new Uri(post.DisplayImageUrl);
|
||||
var fileName = Path.GetFileName(uri.LocalPath);
|
||||
// Добавляем префикс, чтобы избежать коллизий
|
||||
var cacheFileName = $"cache_{post.Id}_{fileName}";
|
||||
var cacheFilePath = Path.Combine(FileSystem.CacheDirectory, cacheFileName);
|
||||
|
||||
// Проверяем кэш
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(cacheFilePath);
|
||||
if (fileInfo.Length > 0)
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] >> Image found in cache: {cacheFilePath}");
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
vm.LocalImagePath = cacheFilePath;
|
||||
});
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Удаляем пустой файл
|
||||
File.Delete(cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем, если нет в кэше
|
||||
var bytes = await ServiceProvider.ApiService.GetImageBytesAsync(post.DisplayImageUrl);
|
||||
if (bytes != null && bytes.Length > 0)
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] >> Image loaded ({bytes.Length} bytes), saving to {cacheFilePath}");
|
||||
|
||||
// Сразу показываем
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
vm.AuthorizedImageSource = ImageSource.FromStream(() => new MemoryStream(bytes));
|
||||
});
|
||||
|
||||
await File.WriteAllBytesAsync(cacheFilePath, bytes);
|
||||
|
||||
// Обновляем путь (на всякий случай, для будущего)
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
vm.LocalImagePath = cacheFilePath;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] >> Image failed to load for post {post.Id} (null/empty)");
|
||||
}
|
||||
}
|
||||
catch(Exception imgEx)
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] >> Exception loading image for post {post.Id}: {imgEx.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"[PostsPage] >> No image URL for post {post.Id}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка загрузки постов: {ex.Message}");
|
||||
await DisplayAlert("Ошибка", "Не удалось загрузить новости", "OK");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
LoadingIndicator.IsVisible = false;
|
||||
LoadingIndicator.IsRunning = false;
|
||||
PostsRefreshView.IsRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshing(object sender, EventArgs e)
|
||||
{
|
||||
await LoadPostsAsync(true);
|
||||
}
|
||||
|
||||
private async void OnPostSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.CurrentSelection.FirstOrDefault() is PostViewModel selectedPost)
|
||||
{
|
||||
// Навигация на страницу деталей
|
||||
await Navigation.PushAsync(new PostDetailsPage(selectedPost));
|
||||
|
||||
// Снимаем выделение
|
||||
PostsCollectionView.SelectedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,11 +25,11 @@ public partial class ProfilePage : ContentPage
|
||||
if (user != null)
|
||||
{
|
||||
_currentUser = user;
|
||||
FullNameLabel.Text = _currentUser.FullName ?? "Не указано";
|
||||
FullNameLabel.Text = _currentUser.FullName;
|
||||
UsernameLabel.Text = $"@{_currentUser.Username}";
|
||||
EmailLabel.Text = _currentUser.Email ?? "Не указано";
|
||||
PositionLabel.Text = _currentUser.Position ?? "Не указано";
|
||||
DepartmentLabel.Text = _currentUser.Department ?? "Не указано";
|
||||
PositionLabel.Text = _currentUser.PositionName;
|
||||
DepartmentLabel.Text = _currentUser.DepartmentName;
|
||||
StatusLabel.Text = _currentUser.IsOnline ? "В сети" : "Не в сети";
|
||||
StatusLabel.TextColor = _currentUser.IsOnline ? Colors.LimeGreen : Colors.Gray;
|
||||
|
||||
|
||||
@ -99,12 +99,12 @@
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
|
||||
<Label
|
||||
Text="{Binding Position}"
|
||||
Text="{Binding PositionName}"
|
||||
FontSize="13"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
|
||||
<Label
|
||||
Text="{Binding Department}"
|
||||
Text="{Binding DepartmentName}"
|
||||
FontSize="12"
|
||||
TextColor="{StaticResource Gray500}"/>
|
||||
</VerticalStackLayout>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CORP_MAUI.Models;
|
||||
using CORP_MAUI.Services;
|
||||
|
||||
namespace CORP_MAUI.Pages;
|
||||
|
||||
@ -11,37 +11,80 @@ public partial class UsersPage : ContentPage
|
||||
|
||||
public UsersPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_users = new ObservableCollection<User>();
|
||||
|
||||
UsersCollectionView.ItemsSource = _users;
|
||||
try
|
||||
{
|
||||
Trace.WriteLine("=== USERSPAGE CONSTRUCTOR START ===");
|
||||
Trace.WriteLine("[UsersPage] >>> Вызов InitializeComponent");
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Trace.WriteLine("[UsersPage] >>> InitializeComponent завершен");
|
||||
|
||||
_users = new ObservableCollection<User>();
|
||||
|
||||
UsersCollectionView.ItemsSource = _users;
|
||||
|
||||
// Подписываемся на изменения статуса пользователей
|
||||
ServiceProvider.WebSocketService.UserStatusChanged += OnUserStatusChanged;
|
||||
Trace.WriteLine("[UsersPage] >>> Подписка на UserStatusChanged");
|
||||
// Подписываемся на изменения статуса пользователей
|
||||
ServiceProvider.WebSocketService.UserStatusChanged += OnUserStatusChanged;
|
||||
|
||||
// Загружаем пользователей
|
||||
_ = LoadUsersAsync();
|
||||
Trace.WriteLine("[UsersPage] >>> Запуск LoadUsersAsync");
|
||||
// Загружаем пользователей
|
||||
_ = LoadUsersAsync();
|
||||
|
||||
Trace.WriteLine("=== USERSPAGE CONSTRUCTOR END ===");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("=== USERSPAGE CONSTRUCTOR ERROR ===");
|
||||
Trace.WriteLine($"[UsersPage] ❌ Тип ошибки: {ex.GetType().FullName}");
|
||||
Trace.WriteLine($"[UsersPage] ❌ Сообщение: {ex.Message}");
|
||||
Trace.WriteLine($"[UsersPage] ❌ StackTrace: {ex.StackTrace}");
|
||||
|
||||
// Показываем ошибку напрямую
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
var mainPage = Application.Current?.Windows[0]?.Page;
|
||||
if (mainPage != null)
|
||||
{
|
||||
await mainPage.DisplayAlert("Ошибка UsersPage",
|
||||
$"Ошибка инициализации:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
|
||||
}
|
||||
});
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadUsersAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Trace.WriteLine("[UsersPage] >>> LoadUsersAsync START");
|
||||
|
||||
LoadingIndicator.IsVisible = true;
|
||||
LoadingIndicator.IsRunning = true;
|
||||
|
||||
_allUsers = await ServiceProvider.ApiService.GetEmployeesAsync();
|
||||
Trace.WriteLine("[UsersPage] >>> Вызов GetAllEmployeesAsync");
|
||||
_allUsers = await ServiceProvider.ApiService.GetAllEmployeesAsync();
|
||||
|
||||
Trace.WriteLine($"[UsersPage] >>> Получено пользователей: {_allUsers.Count}");
|
||||
|
||||
_users.Clear();
|
||||
foreach (var user in _allUsers.OrderBy(u => u.FullName ?? u.Username))
|
||||
foreach (var user in _allUsers.OrderBy(u => u.FullName))
|
||||
{
|
||||
_users.Add(user);
|
||||
}
|
||||
|
||||
Trace.WriteLine($"[UsersPage] ✅ Пользователи загружены успешно");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DisplayAlert("Ошибка", $"Не удалось загрузить список пользователей: {ex.Message}", "OK");
|
||||
Trace.WriteLine($"[UsersPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
|
||||
Trace.WriteLine($"[UsersPage] Message: {ex.Message}");
|
||||
Trace.WriteLine($"[UsersPage] StackTrace: {ex.StackTrace}");
|
||||
|
||||
await DisplayAlert("Ошибка", $"Не удалось загрузить список пользователей:\n{ex.Message}", "OK");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -76,7 +119,7 @@ public partial class UsersPage : ContentPage
|
||||
{
|
||||
// Показываем всех пользователей
|
||||
_users.Clear();
|
||||
foreach (var user in _allUsers.OrderBy(u => u.FullName ?? u.Username))
|
||||
foreach (var user in _allUsers.OrderBy(u => u.FullName))
|
||||
{
|
||||
_users.Add(user);
|
||||
}
|
||||
@ -85,14 +128,14 @@ public partial class UsersPage : ContentPage
|
||||
{
|
||||
// Фильтруем пользователей
|
||||
var filtered = _allUsers.Where(u =>
|
||||
(u.FullName?.ToLower().Contains(searchText) ?? false) ||
|
||||
(u.Username?.ToLower().Contains(searchText) ?? false) ||
|
||||
(u.Department?.ToLower().Contains(searchText) ?? false) ||
|
||||
(u.Position?.ToLower().Contains(searchText) ?? false)
|
||||
u.FullName.ToLower().Contains(searchText) ||
|
||||
u.Username.ToLower().Contains(searchText) ||
|
||||
u.DepartmentName.ToLower().Contains(searchText) ||
|
||||
u.PositionName.ToLower().Contains(searchText)
|
||||
).ToList();
|
||||
|
||||
_users.Clear();
|
||||
foreach (var user in filtered.OrderBy(u => u.FullName ?? u.Username))
|
||||
foreach (var user in filtered.OrderBy(u => u.FullName))
|
||||
{
|
||||
_users.Add(user);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
|
||||
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" android:usesCleartextTraffic="true"></application>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@ -2,7 +2,24 @@
|
||||
"profiles": {
|
||||
"Windows Machine": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false
|
||||
"nativeDebugging": false,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Android Emulator": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"iOS Simulator": {
|
||||
"commandName": "Project",
|
||||
"nativeDebugging": false,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,8 @@ public class ApiService : IDisposable
|
||||
|
||||
private string? _accessToken;
|
||||
private string? _refreshToken;
|
||||
private bool _tokensLoaded = false;
|
||||
private readonly SemaphoreSlim _tokenLoadSemaphore = new(1, 1);
|
||||
|
||||
// Ключи для SecureStorage
|
||||
private const string ACCESS_TOKEN_KEY = "api.jwt_access";
|
||||
@ -51,15 +53,15 @@ public class ApiService : IDisposable
|
||||
|
||||
public ApiService(ApiConfig? config = null)
|
||||
{
|
||||
Debug.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Debug.WriteLine("║ [ApiService] INITIALIZATION STARTED");
|
||||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("║ [ApiService] INITIALIZATION STARTED");
|
||||
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
_config = config ?? new ApiConfig();
|
||||
|
||||
Debug.WriteLine($"[ApiService] BaseURL: {_config.BaseUrl}");
|
||||
Debug.WriteLine($"[ApiService] TokenObtainPath: {_config.TokenObtainPath}");
|
||||
Debug.WriteLine($"[ApiService] TokenRefreshPath: {_config.TokenRefreshPath}");
|
||||
Trace.WriteLine($"[ApiService] BaseURL: {_config.BaseUrl}");
|
||||
Trace.WriteLine($"[ApiService] TokenObtainPath: {_config.TokenObtainPath}");
|
||||
Trace.WriteLine($"[ApiService] TokenRefreshPath: {_config.TokenRefreshPath}");
|
||||
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
@ -78,13 +80,32 @@ public class ApiService : IDisposable
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
Debug.WriteLine($"[ApiService] HttpClient BaseAddress set to: {_httpClient.BaseAddress}");
|
||||
Debug.WriteLine("[ApiService] Loading saved tokens...");
|
||||
Trace.WriteLine($"[ApiService] HttpClient BaseAddress set to: {_httpClient.BaseAddress}");
|
||||
Trace.WriteLine("[ApiService] Initialization complete\n");
|
||||
|
||||
// Загружаем сохраненные токены
|
||||
_ = LoadTokensAsync();
|
||||
|
||||
Debug.WriteLine("[ApiService] Initialization complete\n");
|
||||
// НЕ загружаем токены здесь - они будут загружены при первом запросе
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Гарантирует, что токены загружены из SecureStorage перед выполнением запроса
|
||||
/// </summary>
|
||||
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
|
||||
@ -94,43 +115,43 @@ public class ApiService : IDisposable
|
||||
/// </summary>
|
||||
private void LogHttpDetails(string operation, HttpRequestMessage? request, HttpResponseMessage? response, string? additionalInfo = null)
|
||||
{
|
||||
Debug.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Debug.WriteLine($"║ [ApiService] {operation}");
|
||||
Debug.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine($"║ [ApiService] {operation}");
|
||||
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
if (request != null)
|
||||
{
|
||||
Debug.WriteLine($"║ REQUEST:");
|
||||
Debug.WriteLine($"║ Method: {request.Method}");
|
||||
Debug.WriteLine($"║ URI: {request.RequestUri}");
|
||||
Debug.WriteLine($"║ BaseAddress: {_httpClient.BaseAddress}");
|
||||
Debug.WriteLine($"║ Headers:");
|
||||
Trace.WriteLine($"║ REQUEST:");
|
||||
Trace.WriteLine($"║ Method: {request.Method}");
|
||||
Trace.WriteLine($"║ URI: {request.RequestUri}");
|
||||
Trace.WriteLine($"║ BaseAddress: {_httpClient.BaseAddress}");
|
||||
Trace.WriteLine($"║ Headers:");
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
var value = header.Key == "Authorization" ? "[HIDDEN]" : string.Join(", ", header.Value);
|
||||
Debug.WriteLine($"║ {header.Key}: {value}");
|
||||
Trace.WriteLine($"║ {header.Key}: {value}");
|
||||
}
|
||||
if (request.Content != null)
|
||||
{
|
||||
Debug.WriteLine($"║ Content-Type: {request.Content.Headers.ContentType}");
|
||||
Trace.WriteLine($"║ Content-Type: {request.Content.Headers.ContentType}");
|
||||
}
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
Debug.WriteLine($"║ RESPONSE:");
|
||||
Debug.WriteLine($"║ Status: {(int)response.StatusCode} {response.StatusCode}");
|
||||
Debug.WriteLine($"║ Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}");
|
||||
Debug.WriteLine($"║ Content-Length: {response.Content.Headers.ContentLength ?? 0} bytes");
|
||||
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))
|
||||
{
|
||||
Debug.WriteLine($"║ INFO:");
|
||||
Debug.WriteLine($"║ {additionalInfo}");
|
||||
Trace.WriteLine($"║ INFO:");
|
||||
Trace.WriteLine($"║ {additionalInfo}");
|
||||
}
|
||||
|
||||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -149,12 +170,28 @@ public class ApiService : IDisposable
|
||||
|
||||
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)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка загрузки токенов: {ex.Message}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Error loading tokens: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,11 +208,15 @@ public class ApiService : IDisposable
|
||||
_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)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка сохранения токенов: {ex.Message}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Error saving tokens: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +227,7 @@ public class ApiService : IDisposable
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", token);
|
||||
Trace.WriteLine($"[ApiService] 🔑 Default Authorization header set: Bearer [token]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -193,7 +235,7 @@ public class ApiService : IDisposable
|
||||
/// </summary>
|
||||
private void ClearTokens()
|
||||
{
|
||||
Debug.WriteLine("\n[ApiService] 🧹 Clearing all tokens...");
|
||||
Trace.WriteLine("\n[ApiService] 🧹 Clearing all tokens...");
|
||||
|
||||
_accessToken = null;
|
||||
_refreshToken = null;
|
||||
@ -202,7 +244,7 @@ public class ApiService : IDisposable
|
||||
SecureStorage.Remove(ACCESS_TOKEN_KEY);
|
||||
SecureStorage.Remove(REFRESH_TOKEN_KEY);
|
||||
|
||||
Debug.WriteLine("[ApiService] ✅ Tokens cleared, user is logged out\n");
|
||||
Trace.WriteLine("[ApiService] ✅ Tokens cleared, user is logged out\n");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -220,10 +262,10 @@ public class ApiService : IDisposable
|
||||
// Используем относительный путь, так как BaseAddress уже установлен
|
||||
var path = _config.TokenObtainPath.TrimStart('/');
|
||||
|
||||
Debug.WriteLine($"\n[ApiService] >>> Начинаем логин для {email}");
|
||||
Debug.WriteLine($"[ApiService] >>> Путь токена: {_config.TokenObtainPath} → {path}");
|
||||
Debug.WriteLine($"[ApiService] >>> BaseAddress: {_httpClient.BaseAddress}");
|
||||
Debug.WriteLine($"[ApiService] >>> Ожидаемый URL: {_httpClient.BaseAddress}{path}\n");
|
||||
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 (это запрос для получения токена!)
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, path)
|
||||
@ -243,10 +285,10 @@ public class ApiService : IDisposable
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorText = await response.Content.ReadAsStringAsync();
|
||||
Debug.WriteLine($"\n[ApiService] ❌ Ошибка логина:");
|
||||
Debug.WriteLine($" Status Code: {(int)response.StatusCode} {response.StatusCode}");
|
||||
Debug.WriteLine($" Request URL: {response.RequestMessage?.RequestUri}");
|
||||
Debug.WriteLine($" Error Body: {errorText}\n");
|
||||
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
|
||||
{
|
||||
@ -259,7 +301,7 @@ public class ApiService : IDisposable
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
|
||||
if (!contentType.Contains("application/json"))
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Неверный Content-Type: {contentType}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Неверный Content-Type: {contentType}");
|
||||
return new AuthResponse
|
||||
{
|
||||
Success = false,
|
||||
@ -270,7 +312,7 @@ public class ApiService : IDisposable
|
||||
try
|
||||
{
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
Debug.WriteLine($"\n[ApiService] Response Body: {responseBody}\n");
|
||||
Trace.WriteLine($"\n[ApiService] Response Body: {responseBody}\n");
|
||||
|
||||
var tokenData = JsonSerializer.Deserialize<TokenResponse>(responseBody, new JsonSerializerOptions
|
||||
{
|
||||
@ -279,7 +321,7 @@ public class ApiService : IDisposable
|
||||
|
||||
if (tokenData == null || tokenData.Access == null || tokenData.Refresh == null)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Токены не получены из ответа");
|
||||
Trace.WriteLine($"[ApiService] ❌ Токены не получены из ответа");
|
||||
return new AuthResponse
|
||||
{
|
||||
Success = false,
|
||||
@ -287,20 +329,20 @@ public class ApiService : IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
Debug.WriteLine($"[ApiService] ✅ Токены получены успешно");
|
||||
Debug.WriteLine($" Access Token: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||||
Debug.WriteLine($" Refresh Token: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...\n");
|
||||
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);
|
||||
|
||||
// Получаем информацию о пользователе
|
||||
Debug.WriteLine($"[ApiService] >>> Получаем информацию о пользователе...\n");
|
||||
Trace.WriteLine($"[ApiService] >>> Получаем информацию о пользователе...\n");
|
||||
var user = await GetCurrentUserAsync();
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ✅ Пользователь: {user.Username} ({user.Email})\n");
|
||||
Trace.WriteLine($"[ApiService] ✅ Пользователь: {user.Username} ({user.Email})\n");
|
||||
}
|
||||
|
||||
return new AuthResponse
|
||||
@ -313,7 +355,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Ошибка разбора JSON: {ex.Message}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Ошибка разбора JSON: {ex.Message}");
|
||||
return new AuthResponse
|
||||
{
|
||||
Success = false,
|
||||
@ -323,7 +365,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ HTTP ошибка: {ex.Message}");
|
||||
Trace.WriteLine($"[ApiService] ❌ HTTP ошибка: {ex.Message}");
|
||||
return new AuthResponse
|
||||
{
|
||||
Success = false,
|
||||
@ -332,7 +374,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Неожиданная ошибка: {ex.Message}\n{ex.StackTrace}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Неожиданная ошибка: {ex.Message}\n{ex.StackTrace}");
|
||||
return new AuthResponse
|
||||
{
|
||||
Success = false,
|
||||
@ -356,24 +398,24 @@ public class ApiService : IDisposable
|
||||
{
|
||||
if (string.IsNullOrEmpty(_refreshToken))
|
||||
{
|
||||
Debug.WriteLine("[ApiService] ❌ Cannot refresh: no refresh token available");
|
||||
Trace.WriteLine("[ApiService] ❌ Cannot refresh: no refresh token available");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Debug.WriteLine("\n╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Debug.WriteLine("║ [ApiService] TOKEN REFRESH PROCESS");
|
||||
Debug.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("\n╔════════════════════════════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("║ [ApiService] TOKEN REFRESH PROCESS");
|
||||
Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
// Используем относительный путь, так как BaseAddress уже установлен
|
||||
var path = _config.TokenRefreshPath.TrimStart('/');
|
||||
var refreshData = new { refresh = _refreshToken };
|
||||
|
||||
Debug.WriteLine($"║ Request Path: {path}");
|
||||
Debug.WriteLine($"║ Full URL: {_httpClient.BaseAddress}{path}");
|
||||
Debug.WriteLine($"║ Refresh Token: {_refreshToken.Substring(0, Math.Min(20, _refreshToken.Length))}...");
|
||||
Debug.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════");
|
||||
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 токен в теле)
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, path)
|
||||
@ -383,12 +425,12 @@ public class ApiService : IDisposable
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
Debug.WriteLine($"\n[ApiService] Refresh response: {(int)response.StatusCode} {response.StatusCode}");
|
||||
Trace.WriteLine($"\n[ApiService] Refresh response: {(int)response.StatusCode} {response.StatusCode}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
Debug.WriteLine($"[ApiService] Response body: {responseText}");
|
||||
Trace.WriteLine($"[ApiService] Response body: {responseText}");
|
||||
|
||||
var tokenData = JsonSerializer.Deserialize<TokenResponse>(responseText, new JsonSerializerOptions
|
||||
{
|
||||
@ -397,44 +439,61 @@ public class ApiService : IDisposable
|
||||
|
||||
if (!string.IsNullOrEmpty(tokenData?.Access))
|
||||
{
|
||||
Debug.WriteLine($"\n[ApiService] ✅ New access token received: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||||
Trace.WriteLine($"\n[ApiService] ✅ New access token received: {tokenData.Access.Substring(0, Math.Min(20, tokenData.Access.Length))}...");
|
||||
|
||||
_accessToken = tokenData.Access;
|
||||
await SecureStorage.SetAsync(ACCESS_TOKEN_KEY, 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))
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ✅ New refresh token received: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...");
|
||||
Trace.WriteLine($"[ApiService] ✅ New refresh token received: {tokenData.Refresh.Substring(0, Math.Min(20, tokenData.Refresh.Length))}...");
|
||||
_refreshToken = tokenData.Refresh;
|
||||
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, tokenData.Refresh);
|
||||
|
||||
try
|
||||
{
|
||||
await SecureStorage.SetAsync(REFRESH_TOKEN_KEY, tokenData.Refresh);
|
||||
}
|
||||
catch (Exception storageEx)
|
||||
{
|
||||
Trace.WriteLine($"[ApiService] ⚠️ Ошибка сохранения refresh token в SecureStorage: {storageEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.WriteLine("[ApiService] ✅ Tokens saved successfully\n");
|
||||
Trace.WriteLine("[ApiService] ✅ Tokens saved successfully\n");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine("[ApiService] ❌ No access token in response");
|
||||
Trace.WriteLine("[ApiService] ❌ No access token in response");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorText = await response.Content.ReadAsStringAsync();
|
||||
Debug.WriteLine($"[ApiService] ❌ Refresh failed: {response.StatusCode}");
|
||||
Debug.WriteLine($" Error: {errorText}");
|
||||
Trace.WriteLine($"[ApiService] ❌ Refresh failed: {response.StatusCode}");
|
||||
Trace.WriteLine($" Error: {errorText}");
|
||||
}
|
||||
|
||||
// Не удалось обновить токены - очищаем их
|
||||
Debug.WriteLine("[ApiService] ⚠️ Clearing all tokens due to refresh failure\n");
|
||||
Trace.WriteLine("[ApiService] ⚠️ Clearing all tokens due to refresh failure\n");
|
||||
ClearTokens();
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Exception during token refresh: {ex.Message}");
|
||||
Debug.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||||
Trace.WriteLine($"[ApiService] ❌ Exception during token refresh: {ex.Message}");
|
||||
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||||
ClearTokens();
|
||||
return false;
|
||||
}
|
||||
@ -460,6 +519,12 @@ public class ApiService : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
// ⚡ Гарантируем, что токены загружены перед запросом
|
||||
if (requiresAuth)
|
||||
{
|
||||
await EnsureTokensLoadedAsync();
|
||||
}
|
||||
|
||||
// Используем относительный путь, так как BaseAddress установлен
|
||||
var relativePath = path.TrimStart('/');
|
||||
|
||||
@ -477,7 +542,7 @@ public class ApiService : IDisposable
|
||||
if (requiresAuth && !string.IsNullOrEmpty(_accessToken))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||||
Debug.WriteLine($"[ApiService] 🔑 Authorization header added with token");
|
||||
Trace.WriteLine($"[ApiService] 🔑 Authorization header added with token");
|
||||
}
|
||||
|
||||
// Добавляем дополнительные заголовки
|
||||
@ -495,43 +560,43 @@ public class ApiService : IDisposable
|
||||
request.Content = JsonContent.Create(jsonData);
|
||||
}
|
||||
|
||||
Debug.WriteLine($"\n[ApiService] >>> {method} Request");
|
||||
Debug.WriteLine($" Path: {path} → {relativePath}");
|
||||
Debug.WriteLine($" Full URL: {_httpClient.BaseAddress}{relativePath}");
|
||||
Debug.WriteLine($" Requires Auth: {requiresAuth}");
|
||||
Debug.WriteLine($" Has Token: {!string.IsNullOrEmpty(_accessToken)}");
|
||||
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)
|
||||
{
|
||||
Debug.WriteLine($" Has Body: Yes");
|
||||
Trace.WriteLine($" Has Body: Yes");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
Debug.WriteLine($"[ApiService] <<< Response: {(int)response.StatusCode} {response.StatusCode}");
|
||||
Debug.WriteLine($" Content-Type: {response.Content.Headers.ContentType?.MediaType ?? "N/A"}\n");
|
||||
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))
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] 🔄 Token expired or invalid, attempting refresh...");
|
||||
Debug.WriteLine($" Current access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||||
Debug.WriteLine($" Refresh token available: Yes\n");
|
||||
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())
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ✅ Token successfully refreshed!");
|
||||
Debug.WriteLine($" New access token: {_accessToken?.Substring(0, Math.Min(20, _accessToken.Length))}...");
|
||||
Debug.WriteLine($" Retrying original request...\n");
|
||||
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<T>(method, path, jsonData, queryParams, headers, retry: false, requiresAuth);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Token refresh failed - tokens cleared");
|
||||
Debug.WriteLine($" User needs to login again\n");
|
||||
Trace.WriteLine($"[ApiService] ❌ Token refresh failed - tokens cleared");
|
||||
Trace.WriteLine($" User needs to login again\n");
|
||||
}
|
||||
}
|
||||
|
||||
@ -539,10 +604,10 @@ public class ApiService : IDisposable
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Request failed:");
|
||||
Debug.WriteLine($" Status: {(int)response.StatusCode} {response.StatusCode}");
|
||||
Debug.WriteLine($" URL: {response.RequestMessage?.RequestUri}");
|
||||
Debug.WriteLine($" Body: {responseText.Substring(0, Math.Min(200, responseText.Length))}\n");
|
||||
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<T>
|
||||
{
|
||||
@ -563,11 +628,11 @@ public class ApiService : IDisposable
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
Debug.WriteLine($"[ApiService] ✅ Data parsed successfully\n");
|
||||
Trace.WriteLine($"[ApiService] ✅ Data parsed successfully\n");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ⚠️ JSON parse warning: {ex.Message}\n");
|
||||
Trace.WriteLine($"[ApiService] ⚠️ JSON parse warning: {ex.Message}\n");
|
||||
// Не JSON ответ, игнорируем
|
||||
}
|
||||
}
|
||||
@ -582,8 +647,8 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[ApiService] ❌ Request exception: {ex.Message}");
|
||||
Debug.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||||
Trace.WriteLine($"[ApiService] ❌ Request exception: {ex.Message}");
|
||||
Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
|
||||
return new ApiResponse<T>
|
||||
{
|
||||
Ok = false,
|
||||
@ -637,20 +702,58 @@ public class ApiService : IDisposable
|
||||
|
||||
#region API Methods
|
||||
|
||||
// ============================================
|
||||
// EMPLOYEES (Сотрудники)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список сотрудников
|
||||
/// Получить список сотрудников с пагинацией
|
||||
/// </summary>
|
||||
public async Task<List<User>> GetEmployeesAsync()
|
||||
public async Task<PaginatedResponse<User>> GetEmployeesPaginatedAsync(int? page = null, int? pageSize = null, string? search = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<List<User>>("api/v1/employees/");
|
||||
return response.Ok && response.Data != null ? response.Data : new List<User>();
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
if (!string.IsNullOrEmpty(search)) queryParams["search"] = search;
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<User>>("api/v1/employees/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<User>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка получения пользователей: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка получения сотрудников: {ex.Message}");
|
||||
return new PaginatedResponse<User>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить список всех сотрудников (все страницы)
|
||||
/// </summary>
|
||||
public async Task<List<User>> GetAllEmployeesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allEmployees = new List<User>();
|
||||
int currentPage = 1;
|
||||
PaginatedResponse<User>? response;
|
||||
|
||||
do
|
||||
{
|
||||
response = await GetEmployeesPaginatedAsync(page: currentPage);
|
||||
if (response.Results.Any())
|
||||
{
|
||||
allEmployees.AddRange(response.Results);
|
||||
currentPage++;
|
||||
}
|
||||
} while (!string.IsNullOrEmpty(response.Next));
|
||||
|
||||
return allEmployees;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения всех сотрудников: {ex.Message}");
|
||||
return new List<User>();
|
||||
}
|
||||
}
|
||||
@ -667,13 +770,305 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка получения профиля: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка получения профиля: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить историю сообщений
|
||||
/// Получить сотрудника по ID
|
||||
/// </summary>
|
||||
public async Task<User?> GetEmployeeByIdAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<User>($"api/v1/employees/{id}/");
|
||||
return response.Ok ? response.Data : null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения сотрудника {id}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DEPARTMENTS (Отделы)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список отделов с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<Department>> GetDepartmentsPaginatedAsync(int? page = null, int? pageSize = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<Department>>("api/v1/departments/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Department>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения отделов: {ex.Message}");
|
||||
return new PaginatedResponse<Department>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить список всех отделов
|
||||
/// </summary>
|
||||
public async Task<List<Department>> GetAllDepartmentsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allDepartments = new List<Department>();
|
||||
int currentPage = 1;
|
||||
PaginatedResponse<Department>? 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<Department>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// POSITIONS (Должности)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список всех должностей
|
||||
/// </summary>
|
||||
public async Task<List<Position>> GetPositionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<List<Position>>("api/v1/positions/");
|
||||
return response.Ok && response.Data != null ? response.Data : new List<Position>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения должностей: {ex.Message}");
|
||||
return new List<Position>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SKILLS (Навыки)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список всех навыков
|
||||
/// </summary>
|
||||
public async Task<List<Skill>> GetSkillsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<List<Skill>>("api/v1/skills/");
|
||||
return response.Ok && response.Data != null ? response.Data : new List<Skill>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения навыков: {ex.Message}");
|
||||
return new List<Skill>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CALENDAR EVENTS (События календаря)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить события календаря
|
||||
/// </summary>
|
||||
public async Task<List<CalendarEvent>> GetCalendarEventsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<List<CalendarEvent>>("api/v1/calendar/events/");
|
||||
return response.Ok && response.Data != null ? response.Data : new List<CalendarEvent>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения событий календаря: {ex.Message}");
|
||||
return new List<CalendarEvent>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DOCUMENTS (Документы)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список документов с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<Document>> GetDocumentsPaginatedAsync(int? page = null, int? pageSize = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<Document>>("api/v1/documents/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Document>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения документов: {ex.Message}");
|
||||
return new PaginatedResponse<Document>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// REQUESTS (Запросы)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список запросов с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<Request>> GetRequestsPaginatedAsync(int? page = null, int? pageSize = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<Request>>("api/v1/requests/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Request>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения запросов: {ex.Message}");
|
||||
return new PaginatedResponse<Request>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EMPLOYEE ACTIONS (Действия сотрудников)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить историю действий сотрудников
|
||||
/// </summary>
|
||||
public async Task<List<EmployeeAction>> GetEmployeeActionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await GetAsync<List<EmployeeAction>>("api/v1/employee-actions/");
|
||||
return response.Ok && response.Data != null ? response.Data : new List<EmployeeAction>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения действий сотрудников: {ex.Message}");
|
||||
return new List<EmployeeAction>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// POSTS (Посты/Новости)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список постов с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<Post>> GetPostsPaginatedAsync(int? page = null, int? pageSize = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<Post>>("api/v1/posts/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Post>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения постов: {ex.Message}");
|
||||
return new PaginatedResponse<Post>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// COMMENTS (Комментарии)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список комментариев с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<Comment>> GetCommentsPaginatedAsync(int? page = null, int? pageSize = null, int? postId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
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();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<Comment>>("api/v1/comments/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<Comment>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения комментариев: {ex.Message}");
|
||||
return new PaginatedResponse<Comment>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DEPARTMENT ROLES (Роли отделов)
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список ролей в отделах с пагинацией
|
||||
/// </summary>
|
||||
public async Task<PaginatedResponse<DepartmentRole>> GetDepartmentRolesPaginatedAsync(int? page = null, int? pageSize = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new Dictionary<string, string>();
|
||||
if (page.HasValue) queryParams["page"] = page.Value.ToString();
|
||||
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
|
||||
|
||||
var response = await GetAsync<PaginatedResponse<DepartmentRole>>("api/v1/department-roles/", queryParams);
|
||||
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse<DepartmentRole>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Ошибка получения ролей отделов: {ex.Message}");
|
||||
return new PaginatedResponse<DepartmentRole>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LEGACY/COMPATIBILITY METHODS
|
||||
// ============================================
|
||||
|
||||
/// <summary>
|
||||
/// Получить список сотрудников (устаревший метод для обратной совместимости)
|
||||
/// </summary>
|
||||
[Obsolete("Используйте GetAllEmployeesAsync() или GetEmployeesPaginatedAsync()")]
|
||||
public Task<List<User>> GetEmployeesAsync() => GetAllEmployeesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Получить историю сообщений (если есть эндпоинт)
|
||||
/// </summary>
|
||||
public async Task<List<Message>> GetMessagesAsync(int? userId = null)
|
||||
{
|
||||
@ -688,7 +1083,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка получения сообщений: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка получения сообщений: {ex.Message}");
|
||||
return new List<Message>();
|
||||
}
|
||||
}
|
||||
@ -705,7 +1100,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка отправки сообщения: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка отправки сообщения: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -723,7 +1118,7 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка поиска: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка поиска: {ex.Message}");
|
||||
return new List<User>();
|
||||
}
|
||||
}
|
||||
@ -740,11 +1135,60 @@ public class ApiService : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка обновления статуса: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка обновления статуса: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загрузить изображение (в виде байтов) с авторизацией
|
||||
/// </summary>
|
||||
public async Task<byte[]?> GetImageBytesAsync(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(url)) return null;
|
||||
|
||||
// Если URL относительный - добавляем базу
|
||||
if (!url.StartsWith("http"))
|
||||
{
|
||||
url = _config.BaseUrl.TrimEnd('/') + "/" + url.TrimStart('/');
|
||||
}
|
||||
|
||||
await EnsureTokensLoadedAsync();
|
||||
|
||||
// Добавляем токен, если есть
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrEmpty(_accessToken))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var mediaType = response.Content.Headers.ContentType?.MediaType;
|
||||
if (mediaType != null && !mediaType.StartsWith("image/"))
|
||||
{
|
||||
Trace.WriteLine($"[ApiService] ⚠️ URL {url} вернул не изображение, а {mediaType}");
|
||||
return null;
|
||||
}
|
||||
|
||||
var 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;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
@ -26,6 +26,7 @@ public class WebSocketService
|
||||
private bool _isConnected;
|
||||
|
||||
public event EventHandler<Message>? MessageReceived;
|
||||
public event EventHandler<List<Message>>? MessageHistoryReceived;
|
||||
public event EventHandler<User>? UserStatusChanged;
|
||||
public event EventHandler<bool>? ConnectionStatusChanged;
|
||||
|
||||
@ -33,20 +34,20 @@ public class WebSocketService
|
||||
|
||||
public async Task ConnectAsync(string authToken)
|
||||
{
|
||||
Debug.WriteLine("\n╔════════════════════════════════════════════════════════════");
|
||||
Debug.WriteLine("║ [WebSocket] CONNECTION ATTEMPT");
|
||||
Debug.WriteLine("╚════════════════════════════════════════════════════════════");
|
||||
Debug.WriteLine($"[WebSocket] Has Token: {!string.IsNullOrEmpty(authToken)}");
|
||||
Trace.WriteLine("\n╔════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine("║ [WebSocket] CONNECTION ATTEMPT");
|
||||
Trace.WriteLine("╚════════════════════════════════════════════════════════════");
|
||||
Trace.WriteLine($"[WebSocket] Has Token: {!string.IsNullOrEmpty(authToken)}");
|
||||
|
||||
if (string.IsNullOrEmpty(authToken))
|
||||
{
|
||||
Debug.WriteLine("[WebSocket] ❌ ERROR: No authentication token provided!");
|
||||
Trace.WriteLine("[WebSocket] ❌ ERROR: No authentication token provided!");
|
||||
throw new ArgumentException("Authentication token is required", nameof(authToken));
|
||||
}
|
||||
|
||||
if (_webSocket is { State: WebSocketState.Open })
|
||||
{
|
||||
Debug.WriteLine("[WebSocket] Closing existing connection...");
|
||||
Trace.WriteLine("[WebSocket] Closing existing connection...");
|
||||
await DisconnectAsync();
|
||||
}
|
||||
|
||||
@ -57,21 +58,21 @@ public class WebSocketService
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"\n[WebSocket] >>> Trying: {url}");
|
||||
Trace.WriteLine($"\n[WebSocket] >>> Trying: {url}");
|
||||
|
||||
_webSocket = new ClientWebSocket();
|
||||
|
||||
// 🔑 Устанавливаем Authorization заголовок
|
||||
Debug.WriteLine($"[WebSocket] 🔑 Setting Authorization header...");
|
||||
Debug.WriteLine($"[WebSocket] Token (first 30 chars): {authToken.Substring(0, Math.Min(30, authToken.Length))}...");
|
||||
Debug.WriteLine($"[WebSocket] Token length: {authToken.Length}");
|
||||
Trace.WriteLine($"[WebSocket] 🔑 Setting Authorization header...");
|
||||
Trace.WriteLine($"[WebSocket] Token (first 30 chars): {authToken.Substring(0, Math.Min(30, authToken.Length))}...");
|
||||
Trace.WriteLine($"[WebSocket] Token length: {authToken.Length}");
|
||||
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {authToken}");
|
||||
Debug.WriteLine($"[WebSocket] ✅ Authorization header set: Bearer [token]");
|
||||
Trace.WriteLine($"[WebSocket] ✅ Authorization header set: Bearer [token]");
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
var uri = new Uri(url);
|
||||
Debug.WriteLine($"[WebSocket] Connecting to: {uri}");
|
||||
Trace.WriteLine($"[WebSocket] Connecting to: {uri}");
|
||||
await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
|
||||
|
||||
// Если дошли сюда - подключение успешно!
|
||||
@ -79,9 +80,9 @@ public class WebSocketService
|
||||
_connectedUrl = url;
|
||||
ConnectionStatusChanged?.Invoke(this, true);
|
||||
|
||||
Debug.WriteLine($"[WebSocket] ✅ Connected successfully!");
|
||||
Debug.WriteLine($"[WebSocket] URL: {url}");
|
||||
Debug.WriteLine($"[WebSocket] State: {_webSocket.State}\n");
|
||||
Trace.WriteLine($"[WebSocket] ✅ Connected successfully!");
|
||||
Trace.WriteLine($"[WebSocket] URL: {url}");
|
||||
Trace.WriteLine($"[WebSocket] State: {_webSocket.State}\n");
|
||||
|
||||
// Запускаем прослушивание сообщений
|
||||
_ = Task.Run(async () => await ReceiveMessagesAsync());
|
||||
@ -90,10 +91,10 @@ public class WebSocketService
|
||||
}
|
||||
catch (WebSocketException wsEx)
|
||||
{
|
||||
Debug.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}");
|
||||
Debug.WriteLine($" WebSocketError: {wsEx.WebSocketErrorCode}");
|
||||
Debug.WriteLine($" Message: {wsEx.Message}");
|
||||
Debug.WriteLine($" InnerException: {wsEx.InnerException?.Message}");
|
||||
Trace.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}");
|
||||
Trace.WriteLine($" WebSocketError: {wsEx.WebSocketErrorCode}");
|
||||
Trace.WriteLine($" Message: {wsEx.Message}");
|
||||
Trace.WriteLine($" InnerException: {wsEx.InnerException?.Message}");
|
||||
lastException = wsEx;
|
||||
|
||||
// Закрываем и очищаем
|
||||
@ -104,9 +105,9 @@ public class WebSocketService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}");
|
||||
Debug.WriteLine($" Exception: {ex.GetType().Name}");
|
||||
Debug.WriteLine($" Message: {ex.Message}");
|
||||
Trace.WriteLine($"[WebSocket] ❌ Failed to connect to: {url}");
|
||||
Trace.WriteLine($" Exception: {ex.GetType().Name}");
|
||||
Trace.WriteLine($" Message: {ex.Message}");
|
||||
lastException = ex;
|
||||
|
||||
// Закрываем и очищаем
|
||||
@ -121,9 +122,9 @@ public class WebSocketService
|
||||
_isConnected = false;
|
||||
ConnectionStatusChanged?.Invoke(this, false);
|
||||
|
||||
Debug.WriteLine($"\n[WebSocket] ❌ ALL CONNECTION ATTEMPTS FAILED!");
|
||||
Debug.WriteLine($"[WebSocket] Tried {WebSocketUrls.Length} different URLs");
|
||||
Debug.WriteLine($"[WebSocket] Last error: {lastException?.Message}\n");
|
||||
Trace.WriteLine($"\n[WebSocket] ❌ ALL CONNECTION ATTEMPTS FAILED!");
|
||||
Trace.WriteLine($"[WebSocket] Tried {WebSocketUrls.Length} different URLs");
|
||||
Trace.WriteLine($"[WebSocket] Last error: {lastException?.Message}\n");
|
||||
|
||||
throw new Exception($"Не удалось подключиться к WebSocket. Попробовано {WebSocketUrls.Length} вариантов URL.", lastException);
|
||||
}
|
||||
@ -134,7 +135,10 @@ public class WebSocketService
|
||||
{
|
||||
if (_webSocket is { State: WebSocketState.Open })
|
||||
{
|
||||
await _cancellationTokenSource?.CancelAsync()!;
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
}
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
|
||||
}
|
||||
|
||||
@ -143,18 +147,20 @@ public class WebSocketService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка отключения WebSocket: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка отключения WebSocket: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_webSocket?.Dispose();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_webSocket = null;
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(Message message)
|
||||
{
|
||||
if (!IsConnected)
|
||||
if (!IsConnected || _webSocket == null || _cancellationTokenSource == null)
|
||||
{
|
||||
throw new InvalidOperationException("WebSocket не подключен");
|
||||
}
|
||||
@ -170,21 +176,21 @@ public class WebSocketService
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var buffer = new ArraySegment<byte>(bytes);
|
||||
|
||||
if (_webSocket != null && _cancellationTokenSource != null)
|
||||
{
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
|
||||
}
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка отправки сообщения через WebSocket: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка отправки сообщения через WebSocket: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendTypingIndicatorAsync(int recipientId, bool isTyping)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
if (!IsConnected || _webSocket == null || _cancellationTokenSource == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@ -197,14 +203,39 @@ public class WebSocketService
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var buffer = new ArraySegment<byte>(bytes);
|
||||
|
||||
if (_webSocket != null && _cancellationTokenSource != null)
|
||||
{
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
|
||||
}
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка отправки индикатора набора: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка отправки индикатора набора: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RequestMessageHistoryAsync(int userId)
|
||||
{
|
||||
if (!IsConnected || _webSocket == null || _cancellationTokenSource == null)
|
||||
{
|
||||
Trace.WriteLine("[WebSocket] Не подключен - невозможно запросить историю");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
type = "get_history",
|
||||
data = new { userId }
|
||||
});
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var buffer = new ArraySegment<byte>(bytes);
|
||||
|
||||
await _webSocket.SendAsync(buffer, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
|
||||
Trace.WriteLine($"[WebSocket] Запрошена история сообщений с пользователем {userId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"[WebSocket] Ошибка запроса истории: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,7 +273,7 @@ public class WebSocketService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка получения сообщений WebSocket: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка получения сообщений WebSocket: {ex.Message}");
|
||||
_isConnected = false;
|
||||
ConnectionStatusChanged?.Invoke(this, false);
|
||||
}
|
||||
@ -263,6 +294,11 @@ public class WebSocketService
|
||||
if (message != null) MessageReceived?.Invoke(this, message);
|
||||
break;
|
||||
|
||||
case "message_history":
|
||||
var history = JsonConvert.DeserializeObject<List<Message>>(wrapper.Data.ToString() ?? string.Empty);
|
||||
if (history != null) MessageHistoryReceived?.Invoke(this, history);
|
||||
break;
|
||||
|
||||
case "user_status":
|
||||
var user = JsonConvert.DeserializeObject<User>(wrapper.Data.ToString() ?? string.Empty);
|
||||
if (user != null) UserStatusChanged?.Invoke(this, user);
|
||||
@ -273,14 +309,14 @@ public class WebSocketService
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.WriteLine($"Неизвестный тип сообщения: {wrapper?.Type ?? "null"}");
|
||||
Trace.WriteLine($"Неизвестный тип сообщения: {wrapper?.Type ?? "null"}");
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Ошибка обработки сообщения: {ex.Message}");
|
||||
Trace.WriteLine($"Ошибка обработки сообщения: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
38
CORP-MAUI/ViewModels/PostDetailsViewModel.cs
Normal file
38
CORP-MAUI/ViewModels/PostDetailsViewModel.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using CORP_MAUI.Models;
|
||||
|
||||
namespace CORP_MAUI.ViewModels;
|
||||
|
||||
public class PostDetailsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public PostViewModel PostViewModel { get; }
|
||||
public ObservableCollection<Comment> Comments { get; } = new();
|
||||
|
||||
private bool _isLoadingComments;
|
||||
public bool IsLoadingComments
|
||||
{
|
||||
get => _isLoadingComments;
|
||||
set
|
||||
{
|
||||
_isLoadingComments = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsNotLoadingComments));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsNotLoadingComments => !_isLoadingComments;
|
||||
|
||||
public PostDetailsViewModel(PostViewModel postViewModel)
|
||||
{
|
||||
PostViewModel = postViewModel;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
92
CORP-MAUI/ViewModels/PostViewModel.cs
Normal file
92
CORP-MAUI/ViewModels/PostViewModel.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using CORP_MAUI.Models;
|
||||
|
||||
namespace CORP_MAUI.ViewModels;
|
||||
|
||||
public class PostViewModel : INotifyPropertyChanged
|
||||
{
|
||||
public Post Post { get; }
|
||||
|
||||
public PostViewModel(Post post)
|
||||
{
|
||||
Post = post;
|
||||
// Truncate content for preview
|
||||
if (!string.IsNullOrEmpty(Post.Content) && Post.Content.Length > 0)
|
||||
{
|
||||
if (Post.Content.Length > 100)
|
||||
{
|
||||
_previewContent = Post.Content.Substring(0, 100) + "...";
|
||||
}
|
||||
else
|
||||
{
|
||||
_previewContent = Post.Content;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_previewContent = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string? _localImagePath;
|
||||
public string? LocalImagePath
|
||||
{
|
||||
get => _localImagePath;
|
||||
set
|
||||
{
|
||||
_localImagePath = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsImageVisible));
|
||||
OnPropertyChanged(nameof(DisplayImageSource));
|
||||
}
|
||||
}
|
||||
|
||||
private ImageSource? _imageSource;
|
||||
public ImageSource? AuthorizedImageSource
|
||||
{
|
||||
get => _imageSource;
|
||||
set
|
||||
{
|
||||
_imageSource = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsImageVisible));
|
||||
OnPropertyChanged(nameof(DisplayImageSource));
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSource? DisplayImageSource
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_imageSource != null) return _imageSource;
|
||||
if (!string.IsNullOrEmpty(_localImagePath)) return ImageSource.FromFile(_localImagePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsImageVisible => DisplayImageSource != null;
|
||||
|
||||
private string _previewContent;
|
||||
public string PreviewContent
|
||||
{
|
||||
get => _previewContent;
|
||||
set
|
||||
{
|
||||
_previewContent = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy properties for cleaner XAML binding if needed, or bind to Post.Title directly
|
||||
public int Id => Post.Id;
|
||||
public string Title => Post.Title;
|
||||
public string? AuthorizedImageUrl => Post.DisplayImageUrl;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
785
TECHDOC/API.md
785
TECHDOC/API.md
@ -1,497 +1,300 @@
|
||||
# API и интеграция
|
||||
# 📚 Corp API - Справочник по всем эндпоинтам
|
||||
|
||||
## 🌐 Базовая информация
|
||||
## 🎯 Обзор
|
||||
|
||||
**Базовый URL:** `https://corp.robotail.pro`
|
||||
|
||||
**API версия:** v1
|
||||
|
||||
**Протоколы:**
|
||||
- REST API - `https://corp.robotail.pro/api/v1/`
|
||||
- WebSocket - `wss://corp.robotail.pro/ws`
|
||||
|
||||
## 🔐 Аутентификация
|
||||
|
||||
### Получение токена
|
||||
|
||||
```http
|
||||
POST /api/auth/token/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### Обновление токена
|
||||
|
||||
```http
|
||||
POST /api/auth/token/refresh/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
### Использование токена
|
||||
|
||||
Все защищенные endpoints требуют Bearer токен в заголовке:
|
||||
|
||||
```http
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## 👥 REST API - Пользователи
|
||||
|
||||
### Получить список всех сотрудников
|
||||
|
||||
```http
|
||||
GET /api/v1/employees/
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"username": "ivan_ivanov",
|
||||
"email": "ivan@company.com",
|
||||
"firstName": "Иван",
|
||||
"lastName": "Иванов",
|
||||
"department": "IT",
|
||||
"position": "Разработчик",
|
||||
"isOnline": true,
|
||||
"avatarUrl": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"username": "maria_petrova",
|
||||
"email": "maria@company.com",
|
||||
"firstName": "Мария",
|
||||
"lastName": "Петрова",
|
||||
"department": "HR",
|
||||
"position": "Менеджер",
|
||||
"isOnline": false,
|
||||
"avatarUrl": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Получить текущего пользователя
|
||||
|
||||
```http
|
||||
GET /api/v1/employees/me/
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "ivan_ivanov",
|
||||
"email": "ivan@company.com",
|
||||
"firstName": "Иван",
|
||||
"lastName": "Иванов",
|
||||
"department": "IT",
|
||||
"position": "Разработчик",
|
||||
"isOnline": true,
|
||||
"avatarUrl": null
|
||||
}
|
||||
```
|
||||
|
||||
### Поиск пользователей
|
||||
|
||||
```http
|
||||
GET /api/v1/users/search/?q={query}
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `q` (string, required) - поисковый запрос (имя, отдел, должность)
|
||||
|
||||
**Пример:**
|
||||
```http
|
||||
GET /api/v1/users/search/?q=Иван
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"username": "ivan_ivanov",
|
||||
"firstName": "Иван",
|
||||
"lastName": "Иванов",
|
||||
"department": "IT",
|
||||
"position": "Разработчик",
|
||||
"isOnline": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Обновить статус пользователя
|
||||
|
||||
```http
|
||||
PUT /api/v1/users/status/
|
||||
Authorization: Bearer {access_token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"isOnline": true
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Статус обновлен"
|
||||
}
|
||||
```
|
||||
|
||||
## 💬 REST API - Сообщения
|
||||
|
||||
### Получить все сообщения
|
||||
|
||||
```http
|
||||
GET /api/v1/messages/
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"senderId": 2,
|
||||
"senderName": "Мария Петрова",
|
||||
"receiverId": 1,
|
||||
"content": "Привет! Как дела?",
|
||||
"timestamp": "2026-01-12T10:30:00Z",
|
||||
"isRead": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Получить сообщения с конкретным пользователем
|
||||
|
||||
```http
|
||||
GET /api/v1/messages/?user_id={userId}
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (int, required) - ID пользователя для фильтрации
|
||||
|
||||
**Пример:**
|
||||
```http
|
||||
GET /api/v1/messages/?user_id=2
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"senderId": 2,
|
||||
"senderName": "Мария Петрова",
|
||||
"receiverId": 1,
|
||||
"content": "Привет!",
|
||||
"timestamp": "2026-01-12T10:30:00Z",
|
||||
"isRead": true
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"senderId": 1,
|
||||
"senderName": "Иван Иванов",
|
||||
"receiverId": 2,
|
||||
"content": "Привет! Как дела?",
|
||||
"timestamp": "2026-01-12T10:31:00Z",
|
||||
"isRead": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Отправить сообщение (REST fallback)
|
||||
|
||||
```http
|
||||
POST /api/v1/messages/
|
||||
Authorization: Bearer {access_token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"senderId": 1,
|
||||
"receiverId": 2,
|
||||
"content": "Текст сообщения"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"id": 3,
|
||||
"senderId": 1,
|
||||
"senderName": "Иван Иванов",
|
||||
"receiverId": 2,
|
||||
"content": "Текст сообщения",
|
||||
"timestamp": "2026-01-12T10:35:00Z",
|
||||
"isRead": false
|
||||
}
|
||||
```
|
||||
|
||||
## 🔌 WebSocket API
|
||||
|
||||
### Подключение
|
||||
|
||||
**URL:** `wss://corp.robotail.pro/ws`
|
||||
|
||||
**Поддерживаемые варианты URL:**
|
||||
- `wss://corp.robotail.pro/ws` (основной)
|
||||
- `ws://corp.robotail.pro/ws` (без SSL)
|
||||
- `wss://corp.robotail.pro/ws/` (со слешем)
|
||||
- `wss://corp.robotail.pro/api/v1/ws`
|
||||
- `ws://corp.robotail.pro/api/v1/ws`
|
||||
|
||||
> WebSocketService автоматически пробует разные варианты и подключается к первому работающему
|
||||
|
||||
**Обязательный заголовок:**
|
||||
```http
|
||||
Authorization: Bearer {access_token}
|
||||
```
|
||||
|
||||
**Пример подключения (C#):**
|
||||
```csharp
|
||||
var ws = new ClientWebSocket();
|
||||
ws.Options.SetRequestHeader("Authorization", $"Bearer {accessToken}");
|
||||
await ws.ConnectAsync(new Uri("wss://corp.robotail.pro/ws"), CancellationToken.None);
|
||||
```
|
||||
|
||||
### Формат сообщений
|
||||
|
||||
Все WebSocket сообщения имеют единый формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message_type",
|
||||
"data": {
|
||||
// данные специфичные для типа
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типы сообщений
|
||||
|
||||
#### 1. Отправка сообщения (Client → Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"data": {
|
||||
"receiverId": 2,
|
||||
"content": "Текст сообщения"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Получение сообщения (Server → Client)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "message",
|
||||
"data": {
|
||||
"id": 456,
|
||||
"senderId": 2,
|
||||
"senderName": "Мария Петрова",
|
||||
"receiverId": 1,
|
||||
"content": "Текст сообщения",
|
||||
"timestamp": "2026-01-12T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Индикатор набора текста (Client → Server)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "typing",
|
||||
"data": {
|
||||
"recipientId": 2,
|
||||
"isTyping": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Индикатор набора текста (Server → Client)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "typing",
|
||||
"data": {
|
||||
"senderId": 2,
|
||||
"senderName": "Мария Петрова",
|
||||
"isTyping": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Обновление статуса пользователя (Server → Client)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "user_status",
|
||||
"data": {
|
||||
"userId": 2,
|
||||
"isOnline": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Обработка событий (C#)
|
||||
|
||||
```csharp
|
||||
// Подписка на события
|
||||
ServiceProvider.WebSocketService.MessageReceived += OnMessageReceived;
|
||||
ServiceProvider.WebSocketService.UserStatusChanged += OnUserStatusChanged;
|
||||
ServiceProvider.WebSocketService.TypingIndicatorReceived += OnTypingIndicator;
|
||||
|
||||
// Обработчики
|
||||
private void OnMessageReceived(object sender, Message message)
|
||||
{
|
||||
// Обработка нового сообщения
|
||||
}
|
||||
|
||||
private void OnUserStatusChanged(object sender, (int userId, bool isOnline) status)
|
||||
{
|
||||
// Обновление статуса пользователя в UI
|
||||
}
|
||||
|
||||
private void OnTypingIndicator(object sender, (int userId, bool isTyping) typing)
|
||||
{
|
||||
// Показать/скрыть индикатор "печатает..."
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Система управления токенами
|
||||
|
||||
### Автоматическое обновление
|
||||
|
||||
ApiService автоматически управляет токенами:
|
||||
|
||||
1. **При получении 401/403 ошибки:**
|
||||
- Пытается обновить access токен используя refresh токен
|
||||
- Повторяет оригинальный запрос с новым токеном
|
||||
- При неудаче перенаправляет на страницу входа
|
||||
|
||||
2. **При успешном обновлении:**
|
||||
- Сохраняет новый access токен в SecureStorage
|
||||
- Обновляет HTTP заголовки
|
||||
- Возвращает результат оригинального запроса
|
||||
|
||||
3. **При неудаче обновления:**
|
||||
- Очищает все токены
|
||||
- Очищает SecureStorage
|
||||
- Требует повторную аутентификацию
|
||||
|
||||
### Хранение токенов
|
||||
|
||||
Токены хранятся в `SecureStorage`:
|
||||
|
||||
```csharp
|
||||
// Сохранение
|
||||
await SecureStorage.SetAsync("access_token", accessToken);
|
||||
await SecureStorage.SetAsync("refresh_token", refreshToken);
|
||||
|
||||
// Получение
|
||||
var accessToken = await SecureStorage.GetAsync("access_token");
|
||||
var refreshToken = await SecureStorage.GetAsync("refresh_token");
|
||||
|
||||
// Удаление
|
||||
SecureStorage.Remove("access_token");
|
||||
SecureStorage.Remove("refresh_token");
|
||||
```
|
||||
|
||||
## ⚠️ Обработка ошибок
|
||||
|
||||
### HTTP статус коды
|
||||
|
||||
- **200 OK** - успешный запрос
|
||||
- **201 Created** - ресурс создан
|
||||
- **400 Bad Request** - неверные параметры запроса
|
||||
- **401 Unauthorized** - токен отсутствует или недействителен
|
||||
- **403 Forbidden** - доступ запрещен
|
||||
- **404 Not Found** - ресурс не найден
|
||||
- **500 Internal Server Error** - ошибка сервера
|
||||
|
||||
### Формат ошибок
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Описание ошибки",
|
||||
"detail": "Детальная информация",
|
||||
"code": "ERROR_CODE"
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket коды закрытия
|
||||
|
||||
- **1000** - нормальное закрытие
|
||||
- **1001** - сервер недоступен
|
||||
- **1006** - аномальное закрытие (например, потеря сети)
|
||||
- **1008** - нарушение политики (например, недействительный токен)
|
||||
|
||||
## 🔧 Конфигурация ApiService
|
||||
|
||||
```csharp
|
||||
var config = new ApiConfig
|
||||
{
|
||||
BaseUrl = "https://corp.robotail.pro/api",
|
||||
TokenObtainPath = "/auth/token/",
|
||||
TokenRefreshPath = "/auth/token/refresh/",
|
||||
Timeout = 30,
|
||||
DefaultHeaders = new Dictionary<string, string>
|
||||
{
|
||||
{ "Accept", "application/json" },
|
||||
{ "Content-Type", "application/json" }
|
||||
}
|
||||
};
|
||||
|
||||
var apiService = new ApiService(config);
|
||||
```
|
||||
|
||||
## 📝 Примечания
|
||||
|
||||
### Лимиты и ограничения
|
||||
|
||||
- Максимальная длина сообщения: 4000 символов
|
||||
- Rate limiting: 100 запросов в минуту на пользователя
|
||||
- WebSocket idle timeout: 5 минут без активности
|
||||
|
||||
### Рекомендации
|
||||
|
||||
1. **Приоритет WebSocket** - используйте WebSocket для отправки сообщений, REST API только как fallback
|
||||
2. **Переподключение** - реализуйте автоматическое переподключение WebSocket при разрыве
|
||||
3. **Кэширование** - кэшируйте список пользователей для offline режима
|
||||
4. **Optimistic UI** - обновляйте UI сразу, не дожидаясь ответа сервера
|
||||
**Базовый URL:** `https://corp.robotail.pro/api/v1/`
|
||||
**Обнаружено эндпоинтов:** 13 (+ корневой)
|
||||
**Тесты:** ✅ 22/22 прошли успешно
|
||||
|
||||
---
|
||||
|
||||
**Последнее обновление:** 2026-01-12
|
||||
## 🔐 Аутентификация
|
||||
|
||||
Все эндпоинты требуют Bearer token:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**Получение токена:**
|
||||
- URL: `https://corp.robotail.pro/api/auth/token/`
|
||||
- Метод: POST
|
||||
- Тело: `{"email": "...", "password": "..."}`
|
||||
|
||||
---
|
||||
|
||||
## 📋 Список всех эндпоинтов
|
||||
|
||||
### 1. 🏠 Корневой эндпоинт
|
||||
```
|
||||
GET /api/v1/
|
||||
```
|
||||
**Возвращает:** Словарь со всеми доступными эндпоинтами
|
||||
**Формат ответа:** `{"endpoint_name": "full_url", ...}`
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 2. 📅 События календаря
|
||||
```
|
||||
GET /api/v1/calendar/events/
|
||||
POST /api/v1/calendar/events/
|
||||
```
|
||||
**Возвращает:** Массив событий
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
**Пример использования:**
|
||||
```python
|
||||
response = requests.get(
|
||||
"https://corp.robotail.pro/api/v1/calendar/events/",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
events = response.json() # list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 👥 Сотрудники
|
||||
```
|
||||
GET /api/v1/employees/
|
||||
POST /api/v1/employees/
|
||||
```
|
||||
**Возвращает:** Пагинированный список сотрудников
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тесты:** ✅ 2 теста пройдены
|
||||
|
||||
**Структура ответа:**
|
||||
```json
|
||||
{
|
||||
"count": 100,
|
||||
"next": "https://...?page=2",
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 🏢 Отделы
|
||||
```
|
||||
GET /api/v1/departments/
|
||||
POST /api/v1/departments/
|
||||
```
|
||||
**Возвращает:** Пагинированный список отделов
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 5. 💼 Должности
|
||||
```
|
||||
GET /api/v1/positions/
|
||||
POST /api/v1/positions/
|
||||
```
|
||||
**Возвращает:** Массив должностей
|
||||
**Поля объекта:** `id`, `name`, `description`, `groups`, `groups_verbose`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 6. 🎓 Навыки
|
||||
```
|
||||
GET /api/v1/skills/
|
||||
POST /api/v1/skills/
|
||||
```
|
||||
**Возвращает:** Массив навыков
|
||||
**Поля объекта:** `id`, `name`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
**Пример:**
|
||||
```json
|
||||
[
|
||||
{"id": 1, "name": "Python"},
|
||||
{"id": 2, "name": "JavaScript"}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 📝 Действия сотрудников
|
||||
```
|
||||
GET /api/v1/employee-actions/
|
||||
POST /api/v1/employee-actions/
|
||||
```
|
||||
**Возвращает:** Массив действий
|
||||
**Поля:** `id`, `employee`, `comment`, `extra`, `history`, `action_display`, `date`, `action`, `date_display`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 8. 📄 Документы
|
||||
```
|
||||
GET /api/v1/documents/
|
||||
POST /api/v1/documents/
|
||||
```
|
||||
**Возвращает:** Пагинированный список документов
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 9. 📨 Запросы
|
||||
```
|
||||
GET /api/v1/requests/
|
||||
POST /api/v1/requests/
|
||||
```
|
||||
**Возвращает:** Пагинированный список запросов
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 10. 📰 Посты
|
||||
```
|
||||
GET /api/v1/posts/
|
||||
POST /api/v1/posts/
|
||||
```
|
||||
**Возвращает:** Пагинированный список постов
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 11. 💬 Комментарии
|
||||
```
|
||||
GET /api/v1/comments/
|
||||
POST /api/v1/comments/
|
||||
```
|
||||
**Возвращает:** Пагинированный список комментариев
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 12. 🎭 Роли отделов
|
||||
```
|
||||
GET /api/v1/department-roles/
|
||||
POST /api/v1/department-roles/
|
||||
```
|
||||
**Возвращает:** Пагинированный список ролей
|
||||
**Поля ответа:** `count`, `next`, `previous`, `results`
|
||||
**Методы:** GET, POST, HEAD, OPTIONS
|
||||
**Тест:** ✅ Пройден
|
||||
|
||||
---
|
||||
|
||||
### 13. 🚫 Группы (ограничен доступ)
|
||||
```
|
||||
GET /api/v1/groups/
|
||||
```
|
||||
**Статус:** 403 Forbidden
|
||||
**Описание:** Доступ ограничен для текущего пользователя
|
||||
**Тест:** ✅ Пройден (проверка 403)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Результаты тестирования
|
||||
|
||||
### Все тесты (22 из 22): ✅
|
||||
|
||||
#### Функциональные тесты:
|
||||
- ✅ Корневой эндпоинт возвращает список
|
||||
- ✅ Корневой эндпоинт содержит основные эндпоинты
|
||||
- ✅ Calendar events
|
||||
- ✅ Employees (список)
|
||||
- ✅ Employees (пагинация)
|
||||
- ✅ Departments
|
||||
- ✅ Positions
|
||||
- ✅ Skills
|
||||
- ✅ Employee actions
|
||||
- ✅ Documents
|
||||
- ✅ Requests
|
||||
- ✅ Posts
|
||||
- ✅ Comments
|
||||
- ✅ Department roles
|
||||
- ✅ Groups (403)
|
||||
|
||||
#### Тесты безопасности (7):
|
||||
- ✅ Employees требует авторизации
|
||||
- ✅ Departments требует авторизации
|
||||
- ✅ Positions требует авторизации
|
||||
- ✅ Documents требует авторизации
|
||||
- ✅ Requests требует авторизации
|
||||
- ✅ Posts требует авторизации
|
||||
- ✅ Comments требует авторизации
|
||||
|
||||
---
|
||||
|
||||
## 📦 Созданные файлы
|
||||
|
||||
| Файл | Описание | Статус |
|
||||
|------|----------|--------|
|
||||
| `requests_corp_api_test.py` | Базовый скрипт для получения токена | ✅ |
|
||||
| `explore_api_endpoints.py` | Рекурсивное обнаружение эндпоинтов | ✅ |
|
||||
| `endpoints_list.py` | Структурированный список эндпоинтов | ✅ |
|
||||
| `test_corp_api.py` | Тесты аутентификации (9 тестов) | ✅ |
|
||||
| `test_all_endpoints.py` | Полный набор тестов (22 теста) | ✅ |
|
||||
| `API_DOCUMENTATION.md` | Документация по аутентификации | ✅ |
|
||||
| `ENDPOINTS_REFERENCE.md` | Этот справочник | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 1. Получение токена
|
||||
```python
|
||||
from requests_corp_api_test import get_token
|
||||
|
||||
token = get_token("your@email.com", "password")
|
||||
```
|
||||
|
||||
### 2. Использование эндпоинтов
|
||||
```python
|
||||
import requests
|
||||
from endpoints_list import API_BASE_URL
|
||||
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Получить список сотрудников
|
||||
response = requests.get(API_BASE_URL + "employees/", headers=headers)
|
||||
employees = response.json()
|
||||
|
||||
# Получить навыки
|
||||
response = requests.get(API_BASE_URL + "skills/", headers=headers)
|
||||
skills = response.json()
|
||||
```
|
||||
|
||||
### 3. Запуск тестов
|
||||
```bash
|
||||
# Все тесты
|
||||
pytest test_all_endpoints.py -v
|
||||
|
||||
# Конкретный класс
|
||||
pytest test_all_endpoints.py::TestEmployeesEndpoints -v
|
||||
|
||||
# С подробным выводом
|
||||
pytest test_all_endpoints.py -v --tb=short
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Статистика
|
||||
|
||||
- **Всего эндпоинтов:** 13
|
||||
- **Тестов написано:** 22
|
||||
- **Успешных тестов:** 22 (100%)
|
||||
- **Время выполнения:** ~26 секунд
|
||||
- **Пагинированных эндпоинтов:** 8
|
||||
- **Array эндпоинтов:** 4
|
||||
- **Ограниченных эндпоинтов:** 1 (groups)
|
||||
|
||||
|
||||
@ -13,10 +13,12 @@
|
||||
|
||||
### [API и интеграция](API.md)
|
||||
Полная документация по REST API и WebSocket протоколу:
|
||||
- REST API endpoints
|
||||
- WebSocket сообщения
|
||||
- Система аутентификации и управления токенами
|
||||
- Форматы запросов и ответов
|
||||
- **REST API endpoints** - все доступные эндпоинты (employees, departments, documents, requests, posts, comments, positions, skills, calendar events, employee-actions, department-roles)
|
||||
- **Пагинация** - работа с большими списками данных
|
||||
- **WebSocket протокол** - real-time коммуникация
|
||||
- **Аутентификация JWT** - получение и обновление токенов
|
||||
- **Форматы данных** - структура запросов и ответов
|
||||
- **Обработка ошибок** - статус коды и их обработка
|
||||
|
||||
### [Установка и настройка](SETUP.md)
|
||||
Инструкции по установке, настройке и сборке проекта:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user