From 674e47027228d44e02a8d046a7f53ef7604d9178 Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Mon, 12 Jan 2026 17:02:53 +0300 Subject: [PATCH] Update API documentation and fix warnings --- CORP-MAUI.sln.DotSettings.user | 7 +- CORP-MAUI/App.xaml.cs | 160 +++- CORP-MAUI/AppShell.xaml | 16 +- CORP-MAUI/AppShell.xaml.cs | 83 +- CORP-MAUI/CORP-MAUI.csproj | 9 +- CORP-MAUI/DebugCheck.cs | 20 + CORP-MAUI/DebugChecker.cs | 0 CORP-MAUI/DebugTest.cs | 25 - CORP-MAUI/MauiProgram.cs | 23 +- CORP-MAUI/Models/CalendarEvent.cs | 17 + CORP-MAUI/Models/Comment.cs | 63 ++ CORP-MAUI/Models/Department.cs | 14 + CORP-MAUI/Models/DepartmentRole.cs | 14 + CORP-MAUI/Models/Document.cs | 17 + CORP-MAUI/Models/EmployeeAction.cs | 18 + CORP-MAUI/Models/PaginatedResponse.cs | 12 + CORP-MAUI/Models/Position.cs | 14 + CORP-MAUI/Models/Post.cs | 71 ++ CORP-MAUI/Models/Request.cs | 17 + CORP-MAUI/Models/Skill.cs | 11 + CORP-MAUI/Models/User.cs | 55 +- CORP-MAUI/Pages/ChatPage.xaml.cs | 87 +- CORP-MAUI/Pages/LoginPage.xaml.cs | 85 +- CORP-MAUI/Pages/PostDetailsPage.xaml | 135 +++ CORP-MAUI/Pages/PostDetailsPage.xaml.cs | 56 ++ CORP-MAUI/Pages/PostsPage.xaml | 116 +++ CORP-MAUI/Pages/PostsPage.xaml.cs | 167 ++++ CORP-MAUI/Pages/ProfilePage.xaml.cs | 6 +- CORP-MAUI/Pages/UsersPage.xaml | 4 +- CORP-MAUI/Pages/UsersPage.xaml.cs | 81 +- .../Platforms/Android/AndroidManifest.xml | 2 +- CORP-MAUI/Properties/launchSettings.json | 19 +- CORP-MAUI/Services/ApiService.cs | 662 ++++++++++++--- CORP-MAUI/Services/WebSocketService.cs | 120 ++- CORP-MAUI/ViewModels/PostDetailsViewModel.cs | 38 + CORP-MAUI/ViewModels/PostViewModel.cs | 92 ++ TECHDOC/API.md | 785 +++++++----------- TECHDOC/README.md | 10 +- 38 files changed, 2328 insertions(+), 803 deletions(-) delete mode 100644 CORP-MAUI/DebugChecker.cs delete mode 100644 CORP-MAUI/DebugTest.cs create mode 100644 CORP-MAUI/Pages/PostDetailsPage.xaml create mode 100644 CORP-MAUI/Pages/PostDetailsPage.xaml.cs create mode 100644 CORP-MAUI/Pages/PostsPage.xaml create mode 100644 CORP-MAUI/Pages/PostsPage.xaml.cs create mode 100644 CORP-MAUI/ViewModels/PostDetailsViewModel.cs create mode 100644 CORP-MAUI/ViewModels/PostViewModel.cs diff --git a/CORP-MAUI.sln.DotSettings.user b/CORP-MAUI.sln.DotSettings.user index 6b0b69d..d5a1ca2 100644 --- a/CORP-MAUI.sln.DotSettings.user +++ b/CORP-MAUI.sln.DotSettings.user @@ -1,6 +1,11 @@  ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded - ForceIncluded \ No newline at end of file + ForceIncluded + ForceIncluded + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/CORP-MAUI/App.xaml.cs b/CORP-MAUI/App.xaml.cs index 3863a29..5810e98 100644 --- a/CORP-MAUI/App.xaml.cs +++ b/CORP-MAUI/App.xaml.cs @@ -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()); + }); } } -} \ No newline at end of file + + 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()); + }); + } + } +} diff --git a/CORP-MAUI/AppShell.xaml b/CORP-MAUI/AppShell.xaml index dbf0ef8..f17cc63 100644 --- a/CORP-MAUI/AppShell.xaml +++ b/CORP-MAUI/AppShell.xaml @@ -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"> + - + ContentTemplate="{DataTemplate pages:UsersPage}" /> + ContentTemplate="{DataTemplate pages:ProfilePage}" /> diff --git a/CORP-MAUI/AppShell.xaml.cs b/CORP-MAUI/AppShell.xaml.cs index 95b5776..2f5c5c3 100644 --- a/CORP-MAUI/AppShell.xaml.cs +++ b/CORP-MAUI/AppShell.xaml.cs @@ -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"); } } } + diff --git a/CORP-MAUI/CORP-MAUI.csproj b/CORP-MAUI/CORP-MAUI.csproj index 24c5197..bf7e5ea 100644 --- a/CORP-MAUI/CORP-MAUI.csproj +++ b/CORP-MAUI/CORP-MAUI.csproj @@ -39,8 +39,13 @@ 10.0.17763.0 10.0.17763.0 6.5 - Debug;Release;Android - AnyCPU + + + D:\Applications\Java\JDK\Temurin\jdk-21.0.9+10 + + + DEBUG;TRACE + TRACE diff --git a/CORP-MAUI/DebugCheck.cs b/CORP-MAUI/DebugCheck.cs index e69de29..e01ed7a 100644 --- a/CORP-MAUI/DebugCheck.cs +++ b/CORP-MAUI/DebugCheck.cs @@ -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 + } +} + diff --git a/CORP-MAUI/DebugChecker.cs b/CORP-MAUI/DebugChecker.cs deleted file mode 100644 index e69de29..0000000 diff --git a/CORP-MAUI/DebugTest.cs b/CORP-MAUI/DebugTest.cs deleted file mode 100644 index 1bcbf26..0000000 --- a/CORP-MAUI/DebugTest.cs +++ /dev/null @@ -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() - { - // Пустой метод для вызова статического конструктора - } -} - diff --git a/CORP-MAUI/MauiProgram.cs b/CORP-MAUI/MauiProgram.cs index 4a2218d..312f378 100644 --- a/CORP-MAUI/MauiProgram.cs +++ b/CORP-MAUI/MauiProgram.cs @@ -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(); + builder.Services.AddSingleton(); + + // Регистрируем страницы + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder.Build(); } } \ No newline at end of file diff --git a/CORP-MAUI/Models/CalendarEvent.cs b/CORP-MAUI/Models/CalendarEvent.cs index e69de29..48b1bd5 100644 --- a/CORP-MAUI/Models/CalendarEvent.cs +++ b/CORP-MAUI/Models/CalendarEvent.cs @@ -0,0 +1,17 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель события календаря +/// +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 Participants { get; set; } = new(); + public User? Creator { get; set; } +} + diff --git a/CORP-MAUI/Models/Comment.cs b/CORP-MAUI/Models/Comment.cs index e69de29..fcbefbe 100644 --- a/CORP-MAUI/Models/Comment.cs +++ b/CORP-MAUI/Models/Comment.cs @@ -0,0 +1,63 @@ + +using System.Text.Json.Serialization; + +namespace CORP_MAUI.Models; + +/// +/// Модель комментария +/// +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; } +} diff --git a/CORP-MAUI/Models/Department.cs b/CORP-MAUI/Models/Department.cs index e69de29..109ccab 100644 --- a/CORP-MAUI/Models/Department.cs +++ b/CORP-MAUI/Models/Department.cs @@ -0,0 +1,14 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель отдела +/// +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; } +} diff --git a/CORP-MAUI/Models/DepartmentRole.cs b/CORP-MAUI/Models/DepartmentRole.cs index e69de29..b37fd6c 100644 --- a/CORP-MAUI/Models/DepartmentRole.cs +++ b/CORP-MAUI/Models/DepartmentRole.cs @@ -0,0 +1,14 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель роли в отделе +/// +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 Permissions { get; set; } = new(); +} + diff --git a/CORP-MAUI/Models/Document.cs b/CORP-MAUI/Models/Document.cs index e69de29..ccbd0e0 100644 --- a/CORP-MAUI/Models/Document.cs +++ b/CORP-MAUI/Models/Document.cs @@ -0,0 +1,17 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель документа +/// +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; +} + diff --git a/CORP-MAUI/Models/EmployeeAction.cs b/CORP-MAUI/Models/EmployeeAction.cs index e69de29..f93618d 100644 --- a/CORP-MAUI/Models/EmployeeAction.cs +++ b/CORP-MAUI/Models/EmployeeAction.cs @@ -0,0 +1,18 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель действия сотрудника +/// +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 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; +} + diff --git a/CORP-MAUI/Models/PaginatedResponse.cs b/CORP-MAUI/Models/PaginatedResponse.cs index e69de29..5d56792 100644 --- a/CORP-MAUI/Models/PaginatedResponse.cs +++ b/CORP-MAUI/Models/PaginatedResponse.cs @@ -0,0 +1,12 @@ +namespace CORP_MAUI.Models; + +/// +/// Обобщенная модель для пагинированных ответов +/// +public class PaginatedResponse +{ + public int Count { get; set; } + public string? Next { get; set; } + public string? Previous { get; set; } + public List Results { get; set; } = new(); +} diff --git a/CORP-MAUI/Models/Position.cs b/CORP-MAUI/Models/Position.cs index e69de29..a745cc9 100644 --- a/CORP-MAUI/Models/Position.cs +++ b/CORP-MAUI/Models/Position.cs @@ -0,0 +1,14 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель должности +/// +public class Position +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Groups { get; set; } = new(); + public List GroupsVerbose { get; set; } = new(); +} + diff --git a/CORP-MAUI/Models/Post.cs b/CORP-MAUI/Models/Post.cs index e69de29..eaedadc 100644 --- a/CORP-MAUI/Models/Post.cs +++ b/CORP-MAUI/Models/Post.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; + +namespace CORP_MAUI.Models; + +/// +/// Модель поста/новости +/// +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 Tags { get; set; } = new(); +} diff --git a/CORP-MAUI/Models/Request.cs b/CORP-MAUI/Models/Request.cs index e69de29..4c9f7d0 100644 --- a/CORP-MAUI/Models/Request.cs +++ b/CORP-MAUI/Models/Request.cs @@ -0,0 +1,17 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель запроса (отпуск, заявка и т.д.) +/// +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; +} + diff --git a/CORP-MAUI/Models/Skill.cs b/CORP-MAUI/Models/Skill.cs index e69de29..d9f260e 100644 --- a/CORP-MAUI/Models/Skill.cs +++ b/CORP-MAUI/Models/Skill.cs @@ -0,0 +1,11 @@ +namespace CORP_MAUI.Models; + +/// +/// Модель навыка +/// +public class Skill +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + diff --git a/CORP-MAUI/Models/User.cs b/CORP-MAUI/Models/User.cs index 63c90fc..dea0bf5 100644 --- a/CORP-MAUI/Models/User.cs +++ b/CORP-MAUI/Models/User.cs @@ -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? 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; + } + } } diff --git a/CORP-MAUI/Pages/ChatPage.xaml.cs b/CORP-MAUI/Pages/ChatPage.xaml.cs index cf8986e..8d2b0c9 100644 --- a/CORP-MAUI/Pages/ChatPage.xaml.cs +++ b/CORP-MAUI/Pages/ChatPage.xaml.cs @@ -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 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(); diff --git a/CORP-MAUI/Pages/LoginPage.xaml.cs b/CORP-MAUI/Pages/LoginPage.xaml.cs index 78b4e46..b4c6efe 100644 --- a/CORP-MAUI/Pages/LoginPage.xaml.cs +++ b/CORP-MAUI/Pages/LoginPage.xaml.cs @@ -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 } } + diff --git a/CORP-MAUI/Pages/PostDetailsPage.xaml b/CORP-MAUI/Pages/PostDetailsPage.xaml new file mode 100644 index 0000000..8c17747 --- /dev/null +++ b/CORP-MAUI/Pages/PostDetailsPage.xaml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +