From 979d79780b51d05fd62432e5851aac5d72137b79 Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Thu, 15 Jan 2026 13:34:49 +0300 Subject: [PATCH] Refactor Profile and Navigation: Update User and Department pages, add EmployeeProfile and Departments pages, and implement new layout and functionality --- .../.idea/AndroidProjectSystem.xml | 6 - CORP-MAUI/App.xaml | 1 + CORP-MAUI/AppShell.xaml | 8 +- CORP-MAUI/AppShell.xaml.cs | 2 + CORP-MAUI/MauiProgram.cs | 2 +- CORP-MAUI/Models/Department.cs | 14 + CORP-MAUI/Models/Request.cs | 277 +++++++++++++- CORP-MAUI/Models/User.cs | 58 ++- CORP-MAUI/Pages/ChatPage.xaml | 25 +- CORP-MAUI/Pages/ChatPage.xaml.cs | 10 +- CORP-MAUI/Pages/DepartmentDetailsPage.xaml | 207 ++++++++++ CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs | 160 ++++++++ CORP-MAUI/Pages/DepartmentsPage.xaml | 119 ++++++ CORP-MAUI/Pages/DepartmentsPage.xaml.cs | 174 +++++++++ CORP-MAUI/Pages/EmployeeProfilePage.xaml | 140 +++++++ CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs | 180 +++++++++ .../{UsersPage.xaml => EmployeesPage.xaml} | 85 ++--- CORP-MAUI/Pages/EmployeesPage.xaml.cs | 213 +++++++++++ CORP-MAUI/Pages/PostDetailsPage.xaml | 6 +- CORP-MAUI/Pages/PostsPage.xaml | 6 +- CORP-MAUI/Pages/PostsPage.xaml.cs | 30 +- CORP-MAUI/Pages/ProfilePage.xaml | 2 +- CORP-MAUI/Pages/RequestsPage.xaml | 191 ++++++++++ CORP-MAUI/Pages/RequestsPage.xaml.cs | 285 ++++++++++++++ CORP-MAUI/Pages/UsersPage.xaml.cs | 171 --------- CORP-MAUI/Resources/Styles/AppStyles.xaml | 360 ++++++++++++++++++ CORP-MAUI/Resources/Styles/Colors.xaml | 2 + CORP-MAUI/Services/ApiService.cs | 200 +++++++++- CORP-MAUI/ViewModels/PostViewModel.cs | 46 ++- CORP-MAUI/ViewModels/ProfileViewModel.cs | 152 ++++++-- TECHDOC/ARCHITECTURE.md | 4 +- TECHDOC/CODE_EXAMPLES.md | 12 +- clean-and-build.bat | 26 ++ clean-and-build.ps1 | 84 ++++ test-departments-api.ps1 | 44 +++ test-deserialization.csx | 74 ++++ 36 files changed, 3085 insertions(+), 291 deletions(-) delete mode 100644 .idea/.idea.CORP-MAUI/.idea/AndroidProjectSystem.xml create mode 100644 CORP-MAUI/Pages/DepartmentDetailsPage.xaml create mode 100644 CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs create mode 100644 CORP-MAUI/Pages/DepartmentsPage.xaml create mode 100644 CORP-MAUI/Pages/DepartmentsPage.xaml.cs create mode 100644 CORP-MAUI/Pages/EmployeeProfilePage.xaml create mode 100644 CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs rename CORP-MAUI/Pages/{UsersPage.xaml => EmployeesPage.xaml} (53%) create mode 100644 CORP-MAUI/Pages/EmployeesPage.xaml.cs create mode 100644 CORP-MAUI/Pages/RequestsPage.xaml create mode 100644 CORP-MAUI/Pages/RequestsPage.xaml.cs delete mode 100644 CORP-MAUI/Pages/UsersPage.xaml.cs create mode 100644 CORP-MAUI/Resources/Styles/AppStyles.xaml create mode 100644 clean-and-build.bat create mode 100644 clean-and-build.ps1 create mode 100644 test-departments-api.ps1 create mode 100644 test-deserialization.csx diff --git a/.idea/.idea.CORP-MAUI/.idea/AndroidProjectSystem.xml b/.idea/.idea.CORP-MAUI/.idea/AndroidProjectSystem.xml deleted file mode 100644 index e82600c..0000000 --- a/.idea/.idea.CORP-MAUI/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/CORP-MAUI/App.xaml b/CORP-MAUI/App.xaml index c1d5938..53c336a 100644 --- a/CORP-MAUI/App.xaml +++ b/CORP-MAUI/App.xaml @@ -8,6 +8,7 @@ + diff --git a/CORP-MAUI/AppShell.xaml b/CORP-MAUI/AppShell.xaml index f17cc63..710c755 100644 --- a/CORP-MAUI/AppShell.xaml +++ b/CORP-MAUI/AppShell.xaml @@ -11,7 +11,13 @@ ContentTemplate="{DataTemplate pages:PostsPage}" /> + ContentTemplate="{DataTemplate pages:EmployeesPage}" /> + + diff --git a/CORP-MAUI/AppShell.xaml.cs b/CORP-MAUI/AppShell.xaml.cs index 774fccf..fb24165 100644 --- a/CORP-MAUI/AppShell.xaml.cs +++ b/CORP-MAUI/AppShell.xaml.cs @@ -19,6 +19,8 @@ public partial class AppShell : Shell // Регистрация маршрутов для навигации Routing.RegisterRoute(nameof(ChatPage), typeof(ChatPage)); + Routing.RegisterRoute(nameof(DepartmentDetailsPage), typeof(DepartmentDetailsPage)); + Routing.RegisterRoute(nameof(EmployeeProfilePage), typeof(EmployeeProfilePage)); Trace.WriteLine("[AppShell] ✅ AppShell инициализирован"); Trace.WriteLine("=== APPSHELL CONSTRUCTOR END ==="); diff --git a/CORP-MAUI/MauiProgram.cs b/CORP-MAUI/MauiProgram.cs index dcb531c..c6a03ac 100644 --- a/CORP-MAUI/MauiProgram.cs +++ b/CORP-MAUI/MauiProgram.cs @@ -33,7 +33,7 @@ public static class MauiProgram // Регистрируем страницы builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/CORP-MAUI/Models/Department.cs b/CORP-MAUI/Models/Department.cs index 109ccab..e570f01 100644 --- a/CORP-MAUI/Models/Department.cs +++ b/CORP-MAUI/Models/Department.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace CORP_MAUI.Models; /// @@ -9,6 +11,18 @@ public class Department public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public int? Parent { get; set; } + + [JsonPropertyName("employees_count")] public int EmployeesCount { get; set; } + public User? Head { get; set; } + + /// + /// Описание для отображения (с fallback если пусто) + /// + [JsonIgnore] + public string DisplayDescription => + string.IsNullOrWhiteSpace(Description) + ? "Описание отсутствует" + : Description; } diff --git a/CORP-MAUI/Models/Request.cs b/CORP-MAUI/Models/Request.cs index 4c9f7d0..35f809b 100644 --- a/CORP-MAUI/Models/Request.cs +++ b/CORP-MAUI/Models/Request.cs @@ -1,17 +1,284 @@ +using System.Text.Json.Serialization; + namespace CORP_MAUI.Models; /// -/// Модель запроса (отпуск, заявка и т.д.) +/// Модель заявления (отпуск, заявка и т.д.) /// public class Request { public int Id { get; set; } + + /// + /// Тип заявления (vacation, sick_leave, business_trip и т.д.) + /// + [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + + /// + /// Заголовок заявления + /// + [JsonPropertyName("title")] + public string? Title { get; set; } + + /// + /// Отображаемый заголовок + /// + [JsonPropertyName("display_title")] + public string? DisplayTitle { get; set; } + + /// + /// Статус заявления (pending, approved, rejected и т.д.) + /// + [JsonPropertyName("status")] public string Status { get; set; } = string.Empty; + + /// + /// Дата создания заявления + /// + [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public User? Requester { get; set; } + + /// + /// Дата последнего обновления + /// + [JsonPropertyName("updated_at")] + public DateTime? UpdatedAt { get; set; } + + /// + /// Дата принятия решения + /// + [JsonPropertyName("decided_at")] + public DateTime? DecidedAt { get; set; } + + /// + /// Дата начала (для отпуска, командировки и т.д.) + /// + [JsonPropertyName("date_from")] + public DateTime? DateFrom { get; set; } + + /// + /// Дата окончания + /// + [JsonPropertyName("date_to")] + public DateTime? DateTo { get; set; } + + /// + /// Сотрудник, подавший заявление + /// + [JsonPropertyName("employee")] + public User? Employee { get; set; } + + /// + /// Сотрудник, принявший решение + /// + [JsonPropertyName("approver")] public User? Approver { get; set; } - public string Description { get; set; } = string.Empty; + + /// + /// Комментарий/описание к заявлению + /// + [JsonPropertyName("comment")] + public string? Comment { get; set; } + + /// + /// Вложение (файл) + /// + [JsonPropertyName("attachment")] + public string? Attachment { get; set; } + + /// + /// URL вложения + /// + [JsonPropertyName("attachment_url")] + public string? AttachmentUrl { get; set; } + + /// + /// Отдел + /// + [JsonPropertyName("department")] + public Department? Department { get; set; } + + /// + /// ID отделов + /// + [JsonPropertyName("departments")] + public List? Departments { get; set; } + + /// + /// Является ли решение финальным + /// + [JsonPropertyName("is_final")] + public bool IsFinal { get; set; } + + /// + /// Является ли текущий пользователь получателем + /// + [JsonPropertyName("is_recipient")] + public bool IsRecipient { get; set; } + + /// + /// Отправлено всему отделу + /// + [JsonPropertyName("sent_to_all_department")] + public bool SentToAllDepartment { get; set; } + + /// + /// Количество получателей + /// + [JsonPropertyName("recipient_count")] + public int RecipientCount { get; set; } + + /// + /// Количество в копии + /// + [JsonPropertyName("cc_count")] + public int CcCount { get; set; } + + /// + /// Количество комментариев + /// + [JsonPropertyName("comments_count")] + public int CommentsCount { get; set; } + + /// + /// Получатели заявления + /// + [JsonPropertyName("recipients")] + public List? Recipients { get; set; } + + /// + /// Пользователи в копии + /// + [JsonPropertyName("cc_users")] + public List? CcUsers { get; set; } + + // ================================ + // Вспомогательные свойства для UI + // ================================ + + /// + /// Отображаемый тип/заголовок заявления + /// + [JsonIgnore] + public string DisplayType => + !string.IsNullOrEmpty(DisplayTitle) ? DisplayTitle : + !string.IsNullOrEmpty(Title) ? Title : + GetTypeDisplayName(Type); + + /// + /// Отображаемый статус + /// + [JsonIgnore] + public string DisplayStatus => GetStatusDisplayName(Status); + + /// + /// Цвет статуса для UI + /// + [JsonIgnore] + public Color StatusColor => Status?.ToLower() switch + { + "approved" or "accepted" => Color.FromArgb("#22C55E"), // Зелёный + "rejected" or "declined" or "cancelled" => Color.FromArgb("#EF4444"), // Красный + "pending" or "waiting" => Color.FromArgb("#F59E0B"), // Оранжевый + "in_progress" or "processing" => Color.FromArgb("#3B82F6"), // Синий + _ => Color.FromArgb("#6B7280") // Серый + }; + + /// + /// Иконка типа заявления + /// + [JsonIgnore] + public string TypeIcon => Type?.ToLower() switch + { + "vacation" or "отпуск" => "🏖️", + "sick_leave" or "больничный" => "🏥", + "business_trip" or "командировка" => "✈️", + "day_off" or "отгул" => "📅", + "remote_work" or "удаленная работа" => "🏠", + "overtime" or "сверхурочные" => "⏰", + "equipment" or "оборудование" => "💻", + "document" or "документ" => "📄", + "other" => "📝", + _ => "📋" + }; + + /// + /// Отображаемый период (для заявлений с датами) + /// + [JsonIgnore] + public string? DisplayPeriod + { + get + { + if (DateFrom.HasValue && DateTo.HasValue) + { + if (DateFrom.Value.Date == DateTo.Value.Date) + return DateFrom.Value.ToString("dd.MM.yyyy"); + return $"{DateFrom.Value:dd.MM.yyyy} — {DateTo.Value:dd.MM.yyyy}"; + } + if (DateFrom.HasValue) + return $"с {DateFrom.Value:dd.MM.yyyy}"; + return null; + } + } + + /// + /// Отображаемая дата создания + /// + [JsonIgnore] + public string DisplayCreatedAt => CreatedAt.ToString("dd.MM.yyyy HH:mm"); + + /// + /// Отображаемое описание (из поля comment) + /// + [JsonIgnore] + public string DisplayDescription => + !string.IsNullOrEmpty(Comment) ? Comment : + "Без описания"; + + /// + /// Имя сотрудника, подавшего заявление + /// + [JsonIgnore] + public string EmployeeName => Employee?.FullName ?? "Неизвестный сотрудник"; + + /// + /// Имя сотрудника, принявшего решение + /// + [JsonIgnore] + public string ApproverName => Approver?.FullName ?? ""; + + // ================================ + // Вспомогательные методы + // ================================ + + private static string GetTypeDisplayName(string? type) => type?.ToLower() switch + { + "vacation" => "Отпуск", + "sick_leave" => "Больничный", + "business_trip" => "Командировка", + "day_off" => "Отгул", + "remote_work" => "Удалённая работа", + "overtime" => "Сверхурочные", + "equipment" => "Запрос оборудования", + "document" => "Запрос документа", + "other" => "Другое", + _ => type ?? "Заявление" + }; + + private static string GetStatusDisplayName(string? status) => status?.ToLower() switch + { + "pending" => "На рассмотрении", + "waiting" => "Ожидает", + "approved" => "Одобрено", + "accepted" => "Принято", + "rejected" => "Отклонено", + "declined" => "Отклонено", + "cancelled" => "Отменено", + "in_progress" => "В обработке", + "processing" => "Обрабатывается", + "completed" => "Завершено", + _ => status ?? "Неизвестно" + }; } - diff --git a/CORP-MAUI/Models/User.cs b/CORP-MAUI/Models/User.cs index 0ec0bb9..8bbb1f4 100644 --- a/CORP-MAUI/Models/User.cs +++ b/CORP-MAUI/Models/User.cs @@ -1,5 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.Diagnostics; +using Microsoft.Maui.Controls; namespace CORP_MAUI.Models; @@ -9,12 +11,18 @@ public class User public string Username { get; set; } = string.Empty; public string Email { get; set; } = string.Empty; + [JsonPropertyName("phone_number")] + public string? PhoneNumber { get; set; } + [JsonPropertyName("first_name")] public string FirstName { get; set; } = string.Empty; [JsonPropertyName("last_name")] public string LastName { get; set; } = string.Empty; + [JsonPropertyName("departments")] + public List? Departments { get; set; } + // Может быть либо объектом Department, либо строкой (для обратной совместимости) public object? Department { get; set; } @@ -28,6 +36,9 @@ public class User [JsonPropertyName("avatar_url")] public string? AvatarUrl { get; set; } + + [JsonPropertyName("avatar")] + public string? Avatar { get; set; } // Вспомогательные свойства для удобства [JsonIgnore] @@ -38,6 +49,8 @@ public class User { get { + if (!string.IsNullOrEmpty(Avatar)) return Avatar; + if (string.IsNullOrEmpty(AvatarUrl)) return null; // Handle protocol-relative URLs @@ -52,11 +65,55 @@ public class User } } + /// + /// Универсальное свойство для отображения аватара (поддерживает Base64 и URL) + /// + [JsonIgnore] + public ImageSource? DisplayAvatarSource + { + get + { + try + { + // 1. Проверяем, есть ли Base64 изображение в Avatar + if (!string.IsNullOrEmpty(Avatar) && Avatar.StartsWith("data:image")) + { + // Извлекаем Base64 данные из строки "data:image/jpeg;base64,..." + string base64Data = Avatar; + int commaIndex = base64Data.IndexOf(','); + if (commaIndex > 0) + { + base64Data = base64Data.Substring(commaIndex + 1); + byte[] imageBytes = Convert.FromBase64String(base64Data); + return ImageSource.FromStream(() => new MemoryStream(imageBytes)); + } + } + + // 2. Если есть URL аватара + string? url = DisplayAvatarUrl; + if (!string.IsNullOrEmpty(url)) + { + return ImageSource.FromUri(new Uri(url)); + } + } + catch (Exception ex) + { + Trace.WriteLine($"[User] ⚠️ Ошибка загрузки аватара для {FullName}: {ex.Message}"); + } + + // 3. Возвращаем placeholder + return ImageSource.FromFile("dotnet_bot.png"); + } + } + [JsonIgnore] public string DepartmentName { get { + if (Departments != null && Departments.Count > 0) + return Departments[0].Name; + if (Department is Department dept) return dept.Name; if (Department is string str) @@ -92,4 +149,3 @@ public class User } } } - diff --git a/CORP-MAUI/Pages/ChatPage.xaml b/CORP-MAUI/Pages/ChatPage.xaml index 3010854..490b97f 100644 --- a/CORP-MAUI/Pages/ChatPage.xaml +++ b/CORP-MAUI/Pages/ChatPage.xaml @@ -14,13 +14,28 @@ BackgroundColor="{StaticResource Primary}" Shadow="{Shadow Brush=Black, Opacity=0.3, Offset='0,2', Radius=4}"> - + + +