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}">
-
+
+
+
+
-
+
+
-
+
diff --git a/CORP-MAUI/Pages/ChatPage.xaml.cs b/CORP-MAUI/Pages/ChatPage.xaml.cs
index 5aa8ef2..a7b8c04 100644
--- a/CORP-MAUI/Pages/ChatPage.xaml.cs
+++ b/CORP-MAUI/Pages/ChatPage.xaml.cs
@@ -19,6 +19,9 @@ public partial class ChatPage : ContentPage
_messages = [];
MessagesCollectionView.ItemsSource = _messages;
+
+ // Устанавливаем аватар получателя
+ RecipientAvatar.Source = _recipient.DisplayAvatarSource;
// Подписываемся на события WebSocket
ServiceProvider.WebSocketService.MessageReceived += OnMessageReceived;
@@ -191,6 +194,12 @@ public partial class ChatPage : ContentPage
});
}
+ private async void OnBackButtonClicked(object sender, EventArgs e)
+ {
+ // Возвращаемся к списку сотрудников (предыдущая страница)
+ await Navigation.PopAsync();
+ }
+
protected override void OnDisappearing()
{
base.OnDisappearing();
@@ -213,4 +222,3 @@ public class MessageViewModel
public DateTime Timestamp { get; set; }
public bool IsMyMessage { get; set; }
}
-
diff --git a/CORP-MAUI/Pages/DepartmentDetailsPage.xaml b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml
new file mode 100644
index 0000000..87a755e
--- /dev/null
+++ b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
new file mode 100644
index 0000000..0905c4c
--- /dev/null
+++ b/CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
@@ -0,0 +1,160 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using CORP_MAUI.Models;
+namespace CORP_MAUI.Pages;
+public partial class DepartmentDetailsPage : ContentPage
+{
+ private readonly ObservableCollection _teamMembers;
+ private readonly int _departmentId;
+ private Department? _department;
+ public DepartmentDetailsPage(int departmentId)
+ {
+ try
+ {
+ Trace.WriteLine($"=== DEPARTMENTDETAILSPAGE CONSTRUCTOR START (ID: {departmentId}) ===");
+ InitializeComponent();
+ _departmentId = departmentId;
+ _teamMembers = new ObservableCollection();
+ TeamCollectionView.ItemsSource = _teamMembers;
+ _ = LoadDepartmentDataAsync();
+ Trace.WriteLine("=== DEPARTMENTDETAILSPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== DEPARTMENTDETAILSPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ StackTrace: {ex.StackTrace}");
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ if (Application.Current?.Windows.Count > 0)
+ {
+ Page? mainPage = Application.Current.Windows[0]?.Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка DepartmentDetailsPage",
+ $"Ошибка инициализации:\n{ex.Message}", "OK");
+ }
+ }
+ });
+ throw;
+ }
+ }
+ private async Task LoadDepartmentDataAsync()
+ {
+ try
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> LoadDepartmentDataAsync START (ID: {_departmentId})");
+ LoadingIndicator.IsVisible = true;
+ LoadingIndicator.IsRunning = true;
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Вызов GetDepartmentByIdAsync({_departmentId})");
+ _department = await ServiceProvider.ApiService.GetDepartmentByIdAsync(_departmentId);
+ if (_department == null)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Отдел с ID {_departmentId} не найден");
+ await DisplayAlert("Ошибка", "Не удалось загрузить информацию об отделе", "OK");
+ await Navigation.PopAsync();
+ return;
+ }
+ Trace.WriteLine($"[DepartmentDetailsPage] ✅ Отдел загружен: {_department.Name}");
+ DepartmentNameLabel.Text = _department.Name;
+ DepartmentDescriptionLabel.Text = _department.DisplayDescription;
+ EmployeesCountLabel.Text = $"Сотрудников: {_department.EmployeesCount}";
+ if (_department.Head != null)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Руководитель: {_department.Head.FullName}");
+ HeadSection.IsVisible = true;
+ HeadNameLabel.Text = _department.Head.FullName;
+ HeadPositionLabel.Text = _department.Head.PositionName ?? "Должность не указана";
+ HeadAvatarImage.Source = _department.Head.DisplayAvatarSource;
+ }
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Вызов GetDepartmentEmployeesAsync({_departmentId})");
+ var employees = await ServiceProvider.ApiService.GetDepartmentEmployeesAsync(_departmentId);
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Получено сотрудников: {employees.Count}");
+ _teamMembers.Clear();
+ if (_department.Head != null && !employees.Any(e => e.Id == _department.Head.Id))
+ {
+ _teamMembers.Add(_department.Head);
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Руководитель добавлен в команду");
+ }
+ foreach (var employee in employees.OrderBy(e => e.FullName))
+ {
+ if (_department.Head != null && employee.Id == _department.Head.Id)
+ {
+ _teamMembers.Insert(0, employee);
+ }
+ else
+ {
+ _teamMembers.Add(employee);
+ }
+ }
+ Trace.WriteLine($"[DepartmentDetailsPage] ✅ Данные загружены успешно. Команда: {_teamMembers.Count} человек");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
+ Trace.WriteLine($"[DepartmentDetailsPage] Message: {ex.Message}");
+ Trace.WriteLine($"[DepartmentDetailsPage] StackTrace: {ex.StackTrace}");
+ await DisplayAlert("Ошибка", $"Не удалось загрузить информацию об отделе:\n{ex.Message}", "OK");
+ }
+ finally
+ {
+ LoadingIndicator.IsVisible = false;
+ LoadingIndicator.IsRunning = false;
+ }
+ }
+
+ private async void OnTeamMemberSelected(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is User selectedUser)
+ {
+ TeamCollectionView.SelectedItem = null;
+
+ // Открываем профиль сотрудника
+ await Navigation.PushAsync(new EmployeeProfilePage(selectedUser));
+ }
+ }
+
+ ///
+ /// Обработчик клика по карточке сотрудника (открытие профиля)
+ ///
+ private async void OnTeamMemberCardTapped(object sender, TappedEventArgs e)
+ {
+ if (e.Parameter is User selectedUser)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Открытие профиля: {selectedUser.FullName}");
+
+ try
+ {
+ await Navigation.PushAsync(new EmployeeProfilePage(selectedUser));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Ошибка перехода к профилю: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть профиль сотрудника", "OK");
+ }
+ }
+ }
+
+ ///
+ /// Обработчик клика по иконке чата (открытие чата)
+ ///
+ private async void OnChatIconTapped(object sender, TappedEventArgs e)
+ {
+ if (e.Parameter is User selectedUser)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] >>> Открытие чата с: {selectedUser.FullName}");
+
+ try
+ {
+ await Navigation.PushAsync(new ChatPage(selectedUser));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentDetailsPage] ❌ Ошибка перехода к чату: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
+ }
+ }
+ }
+}
diff --git a/CORP-MAUI/Pages/DepartmentsPage.xaml b/CORP-MAUI/Pages/DepartmentsPage.xaml
new file mode 100644
index 0000000..80a2065
--- /dev/null
+++ b/CORP-MAUI/Pages/DepartmentsPage.xaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Pages/DepartmentsPage.xaml.cs b/CORP-MAUI/Pages/DepartmentsPage.xaml.cs
new file mode 100644
index 0000000..603aa05
--- /dev/null
+++ b/CORP-MAUI/Pages/DepartmentsPage.xaml.cs
@@ -0,0 +1,174 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using CORP_MAUI.Models;
+
+namespace CORP_MAUI.Pages;
+
+public partial class DepartmentsPage : ContentPage
+{
+ private readonly ObservableCollection _departments;
+ private List _allDepartments = new();
+
+ public DepartmentsPage()
+ {
+ try
+ {
+ Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR START ===");
+ Trace.WriteLine("[DepartmentsPage] >>> Вызов InitializeComponent");
+
+ InitializeComponent();
+
+ Trace.WriteLine("[DepartmentsPage] >>> InitializeComponent завершен");
+
+ _departments = new ObservableCollection();
+
+ DepartmentsCollectionView.ItemsSource = _departments;
+
+ Trace.WriteLine("[DepartmentsPage] >>> Запуск LoadDepartmentsAsync");
+ // Загружаем отделы
+ _ = LoadDepartmentsAsync();
+
+ Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[DepartmentsPage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[DepartmentsPage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[DepartmentsPage] ❌ StackTrace: {ex.StackTrace}");
+
+ // Показываем ошибку напрямую
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ if (Application.Current?.Windows.Count > 0)
+ {
+ Page? mainPage = Application.Current.Windows[0]?.Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка DepartmentsPage",
+ $"Ошибка инициализации:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
+ }
+ }
+ });
+ throw;
+ }
+ }
+
+ private async Task LoadDepartmentsAsync()
+ {
+ try
+ {
+ Trace.WriteLine("[DepartmentsPage] >>> LoadDepartmentsAsync START");
+
+ LoadingIndicator.IsVisible = true;
+ LoadingIndicator.IsRunning = true;
+
+ Trace.WriteLine("[DepartmentsPage] >>> Вызов GetAllDepartmentsAsync");
+ _allDepartments = await ServiceProvider.ApiService.GetAllDepartmentsAsync();
+
+ Trace.WriteLine($"[DepartmentsPage] >>> Получено отделов: {_allDepartments.Count}");
+
+ // Детальное логирование первого отдела для проверки
+ if (_allDepartments.Count > 0)
+ {
+ var firstDept = _allDepartments[0];
+ Trace.WriteLine($"[DepartmentsPage] 📊 Первый отдел:");
+ Trace.WriteLine($" - ID: {firstDept.Id}");
+ Trace.WriteLine($" - Name: '{firstDept.Name}'");
+ Trace.WriteLine($" - Description: '{firstDept.Description}'");
+ Trace.WriteLine($" - Description Length: {firstDept.Description?.Length ?? 0}");
+ Trace.WriteLine($" - Description IsNullOrEmpty: {string.IsNullOrEmpty(firstDept.Description)}");
+ Trace.WriteLine($" - EmployeesCount: {firstDept.EmployeesCount}");
+ Trace.WriteLine($" - Head: {firstDept.Head?.FullName ?? "null"}");
+
+ // Выводим все отделы для полной картины
+ Trace.WriteLine($"[DepartmentsPage] 📋 Все отделы:");
+ foreach (var dept in _allDepartments)
+ {
+ Trace.WriteLine($" [{dept.Id}] {dept.Name} - Description: '{dept.Description}' (len: {dept.Description?.Length ?? 0})");
+ }
+ }
+
+ _departments.Clear();
+ foreach (Department department in _allDepartments.OrderBy(d => d.Name))
+ {
+ _departments.Add(department);
+ }
+
+ Trace.WriteLine($"[DepartmentsPage] ✅ Отделы загружены успешно");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentsPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
+ Trace.WriteLine($"[DepartmentsPage] Message: {ex.Message}");
+ Trace.WriteLine($"[DepartmentsPage] StackTrace: {ex.StackTrace}");
+
+ await DisplayAlert("Ошибка", $"Не удалось загрузить список отделов:\n{ex.Message}", "OK");
+ }
+ finally
+ {
+ LoadingIndicator.IsVisible = false;
+ LoadingIndicator.IsRunning = false;
+ RefreshView.IsRefreshing = false;
+ }
+ }
+
+ private async void OnRefreshing(object sender, EventArgs e)
+ {
+ await LoadDepartmentsAsync();
+ }
+
+ private async void OnDepartmentSelected(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is Department selectedDepartment)
+ {
+ // Сбрасываем выбор
+ DepartmentsCollectionView.SelectedItem = null;
+
+ Trace.WriteLine($"[DepartmentsPage] >>> Выбран отдел: {selectedDepartment.Name} (ID: {selectedDepartment.Id})");
+
+ // Переход на страницу деталей отдела
+ try
+ {
+ await Navigation.PushAsync(new DepartmentDetailsPage(selectedDepartment.Id));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentsPage] ❌ Ошибка перехода к деталям отдела: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть страницу отдела", "OK");
+ }
+ }
+ }
+
+ private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
+ {
+ string searchText = e.NewTextValue?.ToLower() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(searchText))
+ {
+ // Показываем все отделы
+ _departments.Clear();
+ foreach (Department department in _allDepartments.OrderBy(d => d.Name))
+ {
+ _departments.Add(department);
+ }
+ }
+ else
+ {
+ // Фильтруем отделы
+ List filtered = _allDepartments.Where(d =>
+ d.Name.ToLower().Contains(searchText) ||
+ d.Description.ToLower().Contains(searchText) ||
+ (d.Head?.FullName.ToLower().Contains(searchText) ?? false)
+ ).ToList();
+
+ _departments.Clear();
+ foreach (Department department in filtered.OrderBy(d => d.Name))
+ {
+ _departments.Add(department);
+ }
+ }
+ }
+}
+
diff --git a/CORP-MAUI/Pages/EmployeeProfilePage.xaml b/CORP-MAUI/Pages/EmployeeProfilePage.xaml
new file mode 100644
index 0000000..a283191
--- /dev/null
+++ b/CORP-MAUI/Pages/EmployeeProfilePage.xaml
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
new file mode 100644
index 0000000..aae359f
--- /dev/null
+++ b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
@@ -0,0 +1,180 @@
+using System.Diagnostics;
+using CORP_MAUI.Models;
+
+namespace CORP_MAUI.Pages;
+
+public partial class EmployeeProfilePage : ContentPage
+{
+ private User? _user;
+ private readonly int _userId;
+
+ public EmployeeProfilePage(int userId)
+ {
+ try
+ {
+ Trace.WriteLine($"=== EMPLOYEEPROFILEPAGE CONSTRUCTOR START (UserID: {userId}) ===");
+
+ InitializeComponent();
+
+ _userId = userId;
+
+ // Загружаем данные
+ _ = LoadUserProfileAsync();
+
+ Trace.WriteLine("=== EMPLOYEEPROFILEPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== EMPLOYEEPROFILEPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ StackTrace: {ex.StackTrace}");
+
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ if (Application.Current?.Windows.Count > 0)
+ {
+ Page? mainPage = Application.Current.Windows[0]?.Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка EmployeeProfilePage",
+ $"Ошибка инициализации:\n{ex.Message}", "OK");
+ }
+ }
+ });
+ throw;
+ }
+ }
+
+ ///
+ /// Конструктор для передачи готового объекта пользователя
+ ///
+ public EmployeeProfilePage(User user) : this(user.Id)
+ {
+ _user = user;
+ }
+
+ private async Task LoadUserProfileAsync()
+ {
+ try
+ {
+ Trace.WriteLine($"[EmployeeProfilePage] >>> LoadUserProfileAsync START (UserID: {_userId})");
+
+ LoadingIndicator.IsVisible = true;
+ LoadingIndicator.IsRunning = true;
+
+ // Если пользователь уже передан, используем его, иначе загружаем
+ if (_user == null)
+ {
+ Trace.WriteLine($"[EmployeeProfilePage] >>> Вызов GetUserByIdAsync({_userId})");
+ _user = await ServiceProvider.ApiService.GetUserByIdAsync(_userId);
+
+ if (_user == null)
+ {
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Пользователь с ID {_userId} не найден");
+ await DisplayAlert("Ошибка", "Не удалось загрузить профиль сотрудника", "OK");
+ await Navigation.PopAsync();
+ return;
+ }
+ }
+
+ Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль загружен: {_user.FullName}");
+
+ // Заполняем основную информацию
+ FullNameLabel.Text = _user.FullName;
+ PositionLabel.Text = _user.PositionName ?? "Должность не указана";
+ AvatarImage.Source = _user.DisplayAvatarSource;
+ OnlineIndicator.IsVisible = _user.IsOnline;
+
+ // Email
+ if (!string.IsNullOrEmpty(_user.Email))
+ {
+ EmailLabel.Text = _user.Email;
+ EmailSection.IsVisible = true;
+ }
+
+ // Телефон
+ if (!string.IsNullOrEmpty(_user.PhoneNumber))
+ {
+ PhoneLabel.Text = _user.PhoneNumber;
+ PhoneSection.IsVisible = true;
+ }
+
+ // Отделы
+ if (_user.Departments != null && _user.Departments.Any())
+ {
+ DepartmentsSection.IsVisible = true;
+ foreach (var department in _user.Departments)
+ {
+ var label = new Label
+ {
+ Text = $"• {department.Name}",
+ FontSize = 14,
+ TextColor = Color.FromArgb("#404040")
+ };
+ DepartmentsContainer.Children.Add(label);
+ }
+ Trace.WriteLine($"[EmployeeProfilePage] >>> Отделов: {_user.Departments.Count}");
+ }
+
+ // Навыки
+ if (_user.Skills != null && _user.Skills.Any())
+ {
+ SkillsSection.IsVisible = true;
+ foreach (var skill in _user.Skills)
+ {
+ var skillBadge = new Border
+ {
+ Padding = new Thickness(12, 6),
+ Margin = new Thickness(5),
+ StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 15 },
+ BackgroundColor = Color.FromArgb("#E1E1E1"),
+ Content = new Label
+ {
+ Text = skill.Name,
+ FontSize = 13,
+ TextColor = Color.FromArgb("#404040")
+ }
+ };
+ SkillsContainer.Children.Add(skillBadge);
+ }
+ Trace.WriteLine($"[EmployeeProfilePage] >>> Навыков: {_user.Skills.Count}");
+ }
+
+ Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль отображен успешно");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Ошибка загрузки: {ex.GetType().Name}");
+ Trace.WriteLine($"[EmployeeProfilePage] Message: {ex.Message}");
+ Trace.WriteLine($"[EmployeeProfilePage] StackTrace: {ex.StackTrace}");
+
+ await DisplayAlert("Ошибка", $"Не удалось загрузить профиль сотрудника:\n{ex.Message}", "OK");
+ }
+ finally
+ {
+ LoadingIndicator.IsVisible = false;
+ LoadingIndicator.IsRunning = false;
+ }
+ }
+
+ private async void OnChatButtonClicked(object sender, EventArgs e)
+ {
+ if (_user == null)
+ return;
+
+ Trace.WriteLine($"[EmployeeProfilePage] >>> Открытие чата с: {_user.FullName}");
+
+ try
+ {
+ await Navigation.PushAsync(new ChatPage(_user));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Ошибка перехода к чату: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
+ }
+ }
+}
+
diff --git a/CORP-MAUI/Pages/UsersPage.xaml b/CORP-MAUI/Pages/EmployeesPage.xaml
similarity index 53%
rename from CORP-MAUI/Pages/UsersPage.xaml
rename to CORP-MAUI/Pages/EmployeesPage.xaml
index b744bca..ef9cb91 100644
--- a/CORP-MAUI/Pages/UsersPage.xaml
+++ b/CORP-MAUI/Pages/EmployeesPage.xaml
@@ -2,7 +2,7 @@
@@ -10,14 +10,11 @@
+ Style="{StaticResource SearchContainerBorder}">
@@ -29,58 +26,39 @@
Refreshing="OnRefreshing">
+ SelectionChanged="OnEmployeeSelected">
-
+
+ Style="{StaticResource EmptyStateTitle}"/>
-
+
-
+
+
+
+
+
+ Style="{StaticResource AvatarMedium}">
@@ -94,31 +72,36 @@
+ Style="{StaticResource CardSubtitle}"/>
+ Style="{StaticResource CardText}"
+ FontSize="13"/>
+ Style="{StaticResource CardMeta}"/>
-
-
+
diff --git a/CORP-MAUI/Pages/EmployeesPage.xaml.cs b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
new file mode 100644
index 0000000..1709bd9
--- /dev/null
+++ b/CORP-MAUI/Pages/EmployeesPage.xaml.cs
@@ -0,0 +1,213 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using CORP_MAUI.Models;
+
+namespace CORP_MAUI.Pages;
+
+public partial class EmployeesPage : ContentPage
+{
+ private readonly ObservableCollection _employees;
+ private List _allEmployees = new();
+
+ public EmployeesPage()
+ {
+ try
+ {
+ Trace.WriteLine("=== EMPLOYEESPAGE CONSTRUCTOR START ===");
+ Trace.WriteLine("[EmployeesPage] >>> Вызов InitializeComponent");
+
+ InitializeComponent();
+
+ Trace.WriteLine("[EmployeesPage] >>> InitializeComponent завершен");
+
+ _employees = new ObservableCollection();
+
+ EmployeesCollectionView.ItemsSource = _employees;
+
+ Trace.WriteLine("[EmployeesPage] >>> Подписка на UserStatusChanged");
+ // Подписываемся на изменения статуса пользователей
+ ServiceProvider.WebSocketService.UserStatusChanged += OnUserStatusChanged;
+
+ Trace.WriteLine("[EmployeesPage] >>> Запуск LoadEmployeesAsync");
+ // Загружаем сотрудников
+ _ = LoadEmployeesAsync();
+
+ Trace.WriteLine("=== EMPLOYEESPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== EMPLOYEESPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[EmployeesPage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[EmployeesPage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[EmployeesPage] ❌ StackTrace: {ex.StackTrace}");
+
+ // Показываем ошибку напрямую
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ Page? mainPage = Application.Current?.Windows[0]?.Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка EmployeesPage",
+ $"Ошибка инициализации:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
+ }
+ });
+ throw;
+ }
+ }
+
+ private async Task LoadEmployeesAsync()
+ {
+ try
+ {
+ Trace.WriteLine("[EmployeesPage] >>> LoadEmployeesAsync START");
+
+ LoadingIndicator.IsVisible = true;
+ LoadingIndicator.IsRunning = true;
+
+ Trace.WriteLine("[EmployeesPage] >>> Вызов GetAllEmployeesAsync");
+ _allEmployees = await ServiceProvider.ApiService.GetAllEmployeesAsync();
+
+ Trace.WriteLine($"[EmployeesPage] >>> Получено сотрудников: {_allEmployees.Count}");
+
+ _employees.Clear();
+ foreach (User user in _allEmployees.OrderBy(u => u.FullName))
+ {
+ _employees.Add(user);
+ }
+
+ Trace.WriteLine($"[EmployeesPage] ✅ Сотрудники загружены успешно");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeesPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
+ Trace.WriteLine($"[EmployeesPage] Message: {ex.Message}");
+ Trace.WriteLine($"[EmployeesPage] StackTrace: {ex.StackTrace}");
+
+ await DisplayAlert("Ошибка", $"Не удалось загрузить список сотрудников:\n{ex.Message}", "OK");
+ }
+ finally
+ {
+ LoadingIndicator.IsVisible = false;
+ LoadingIndicator.IsRunning = false;
+ RefreshView.IsRefreshing = false;
+ }
+ }
+
+ private async void OnRefreshing(object sender, EventArgs e)
+ {
+ await LoadEmployeesAsync();
+ }
+
+ private async void OnEmployeeSelected(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is User selectedEmployee)
+ {
+ // Сбрасываем выбор
+ EmployeesCollectionView.SelectedItem = null;
+
+ // Открываем профиль сотрудника
+ await Navigation.PushAsync(new EmployeeProfilePage(selectedEmployee));
+ }
+ }
+
+ ///
+ /// Обработчик клика по карточке сотрудника (открытие профиля)
+ ///
+ private async void OnEmployeeCardTapped(object sender, TappedEventArgs e)
+ {
+ if (e.Parameter is User selectedEmployee)
+ {
+ Trace.WriteLine($"[EmployeesPage] >>> Открытие профиля: {selectedEmployee.FullName}");
+
+ try
+ {
+ await Navigation.PushAsync(new EmployeeProfilePage(selectedEmployee));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeesPage] ❌ Ошибка перехода к профилю: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть профиль сотрудника", "OK");
+ }
+ }
+ }
+
+ ///
+ /// Обработчик клика по иконке чата (открытие чата)
+ ///
+ private async void OnChatIconTapped(object sender, TappedEventArgs e)
+ {
+ if (e.Parameter is User selectedEmployee)
+ {
+ Trace.WriteLine($"[EmployeesPage] >>> Открытие чата с: {selectedEmployee.FullName}");
+
+ try
+ {
+ await Navigation.PushAsync(new ChatPage(selectedEmployee));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeesPage] ❌ Ошибка перехода к чату: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
+ }
+ }
+ }
+
+ private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
+ {
+ string searchText = e.NewTextValue?.ToLower() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(searchText))
+ {
+ // Показываем всех сотрудников
+ _employees.Clear();
+ foreach (User user in _allEmployees.OrderBy(u => u.FullName))
+ {
+ _employees.Add(user);
+ }
+ }
+ else
+ {
+ // Фильтруем сотрудников
+ List filtered = _allEmployees.Where(u =>
+ u.FullName.ToLower().Contains(searchText) ||
+ u.Username.ToLower().Contains(searchText) ||
+ u.DepartmentName.ToLower().Contains(searchText) ||
+ u.PositionName.ToLower().Contains(searchText)
+ ).ToList();
+
+ _employees.Clear();
+ foreach (User user in filtered.OrderBy(u => u.FullName))
+ {
+ _employees.Add(user);
+ }
+ }
+ }
+
+ private void OnUserStatusChanged(object? sender, User updatedUser)
+ {
+ // Обновляем статус пользователя в списке
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ User? user = _employees.FirstOrDefault(u => u.Id == updatedUser.Id);
+ if (user != null)
+ {
+ user.IsOnline = updatedUser.IsOnline;
+ }
+
+ // Обновляем в основном списке
+ User? allUser = _allEmployees?.FirstOrDefault(u => u.Id == updatedUser.Id);
+ if (allUser != null)
+ {
+ allUser.IsOnline = updatedUser.IsOnline;
+ }
+ });
+ }
+
+ protected override void OnDisappearing()
+ {
+ base.OnDisappearing();
+ ServiceProvider.WebSocketService.UserStatusChanged -= OnUserStatusChanged;
+ }
+}
+
diff --git a/CORP-MAUI/Pages/PostDetailsPage.xaml b/CORP-MAUI/Pages/PostDetailsPage.xaml
index 8c17747..892452d 100644
--- a/CORP-MAUI/Pages/PostDetailsPage.xaml
+++ b/CORP-MAUI/Pages/PostDetailsPage.xaml
@@ -39,11 +39,7 @@
HeightRequest="250"
BackgroundColor="#F5F5F5"
Source="{Binding PostViewModel.DisplayImageSource}"
- IsVisible="{Binding PostViewModel.IsImageVisible}">
-
-
-
-
+ IsVisible="{Binding PostViewModel.IsImageVisible}"/>
diff --git a/CORP-MAUI/Pages/PostsPage.xaml b/CORP-MAUI/Pages/PostsPage.xaml
index 79c2335..e630662 100644
--- a/CORP-MAUI/Pages/PostsPage.xaml
+++ b/CORP-MAUI/Pages/PostsPage.xaml
@@ -77,11 +77,7 @@
Margin="0,5"
BackgroundColor="#F5F5F5"
Source="{Binding DisplayImageSource}"
- IsVisible="{Binding IsImageVisible}">
-
-
-
-
+ IsVisible="{Binding IsImageVisible}"/>
50 ? post.Image.Substring(0, 50) + "..." : post.Image;
+ Trace.WriteLine($"[PostsPage] Пост {post.Id} - Post.Image: {imagePreview}");
+ }
+ if (!string.IsNullOrEmpty(post.ImageUrl))
+ {
+ Trace.WriteLine($"[PostsPage] Пост {post.Id} - Post.ImageUrl: {post.ImageUrl}");
+ }
+
+ // Проверяем, есть ли Base64 изображение в Post.Image
+ if (!string.IsNullOrEmpty(post.Image) && post.Image.StartsWith("data:image"))
+ {
+ Trace.WriteLine($"[PostsPage] ✅ Пост {post.Id} содержит Base64 изображение, пропускаем загрузку");
+ // DisplayImageSource автоматически обработает Base64
+ return vm;
+ }
+
+ // Если есть URL для изображения (через DisplayImageUrl, который проверяет ImageUrl и Image)
if (!string.IsNullOrEmpty(post.DisplayImageUrl))
{
try
{
+ Trace.WriteLine($"[PostsPage] 🔄 Обработка URL изображения для поста {post.Id}: {post.DisplayImageUrl}");
+
Uri uri = new Uri(post.DisplayImageUrl);
// Sanitize filename & use extension from URL if available, else .jpg
string extension = Path.GetExtension(uri.LocalPath);
if (string.IsNullOrEmpty(extension)) extension = ".jpg";
string fileName = Path.GetFileNameWithoutExtension(uri.LocalPath);
+ if (string.IsNullOrEmpty(fileName)) fileName = $"image_{post.Id}";
fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, invalidChar) => current.Replace(invalidChar, '_'));
string cacheFileName = $"post_img_{post.Id}_{fileName}{extension}";
@@ -80,10 +103,15 @@ public partial class PostsPage
FileInfo fileInfo = new FileInfo(cacheFilePath);
if (fileInfo.Length > 0)
{
- Trace.WriteLine($"[PostsPage] Изображение из кэша: {cacheFilePath}");
+ Trace.WriteLine($"[PostsPage] ✅ Изображение из кэша: {cacheFilePath}");
vm.LocalImagePath = cacheFilePath;
isCached = true;
}
+ else
+ {
+ Trace.WriteLine($"[PostsPage] ⚠️ Файл кэша пуст, удаляем: {cacheFilePath}");
+ try { File.Delete(cacheFilePath); } catch { }
+ }
}
// 2. Если нет в кэше - загружаем
diff --git a/CORP-MAUI/Pages/ProfilePage.xaml b/CORP-MAUI/Pages/ProfilePage.xaml
index a743553..86af64d 100644
--- a/CORP-MAUI/Pages/ProfilePage.xaml
+++ b/CORP-MAUI/Pages/ProfilePage.xaml
@@ -26,7 +26,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Pages/RequestsPage.xaml.cs b/CORP-MAUI/Pages/RequestsPage.xaml.cs
new file mode 100644
index 0000000..2361701
--- /dev/null
+++ b/CORP-MAUI/Pages/RequestsPage.xaml.cs
@@ -0,0 +1,285 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using CORP_MAUI.Models;
+
+namespace CORP_MAUI.Pages;
+
+public partial class RequestsPage : ContentPage
+{
+ private readonly ObservableCollection _requests;
+ private List _allRequests = new();
+ private string _currentFilter = "all";
+ private string _searchText = string.Empty;
+
+ public RequestsPage()
+ {
+ try
+ {
+ Trace.WriteLine("=== REQUESTSPAGE CONSTRUCTOR START ===");
+ Trace.WriteLine("[RequestsPage] >>> Вызов InitializeComponent");
+
+ InitializeComponent();
+
+ Trace.WriteLine("[RequestsPage] >>> InitializeComponent завершен");
+
+ _requests = new ObservableCollection();
+
+ RequestsCollectionView.ItemsSource = _requests;
+
+ Trace.WriteLine("[RequestsPage] >>> Запуск LoadRequestsAsync");
+ // Загружаем заявления
+ _ = LoadRequestsAsync();
+
+ Trace.WriteLine("=== REQUESTSPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== REQUESTSPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[RequestsPage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[RequestsPage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[RequestsPage] ❌ StackTrace: {ex.StackTrace}");
+
+ // Показываем ошибку напрямую
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ if (Application.Current?.Windows.Count > 0)
+ {
+ Page? mainPage = Application.Current.Windows[0].Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка RequestsPage",
+ $"Ошибка инициализации:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
+ }
+ }
+ });
+ throw;
+ }
+ }
+
+ private async Task LoadRequestsAsync()
+ {
+ try
+ {
+ Trace.WriteLine("[RequestsPage] >>> LoadRequestsAsync START");
+
+ LoadingIndicator.IsVisible = true;
+ LoadingIndicator.IsRunning = true;
+
+ Trace.WriteLine("[RequestsPage] >>> Вызов GetAllRequestsAsync");
+ _allRequests = await ServiceProvider.ApiService.GetAllRequestsAsync();
+
+ Trace.WriteLine($"[RequestsPage] >>> Получено заявлений: {_allRequests.Count}");
+
+ ApplyFilters();
+
+ Trace.WriteLine($"[RequestsPage] ✅ Заявления загружены успешно");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[RequestsPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
+ Trace.WriteLine($"[RequestsPage] Message: {ex.Message}");
+ Trace.WriteLine($"[RequestsPage] StackTrace: {ex.StackTrace}");
+
+ await DisplayAlert("Ошибка", $"Не удалось загрузить список заявлений:\n{ex.Message}", "OK");
+ }
+ finally
+ {
+ LoadingIndicator.IsVisible = false;
+ LoadingIndicator.IsRunning = false;
+ RefreshView.IsRefreshing = false;
+ }
+ }
+
+ private void ApplyFilters()
+ {
+ IEnumerable filtered = _allRequests;
+
+ // Фильтр по статусу
+ filtered = _currentFilter switch
+ {
+ "pending" => filtered.Where(r =>
+ r.Status.Equals("pending", StringComparison.OrdinalIgnoreCase) ||
+ r.Status.Equals("waiting", StringComparison.OrdinalIgnoreCase)),
+ "approved" => filtered.Where(r =>
+ r.Status.Equals("approved", StringComparison.OrdinalIgnoreCase) ||
+ r.Status.Equals("accepted", StringComparison.OrdinalIgnoreCase)),
+ "rejected" => filtered.Where(r =>
+ r.Status.Equals("rejected", StringComparison.OrdinalIgnoreCase) ||
+ r.Status.Equals("declined", StringComparison.OrdinalIgnoreCase) ||
+ r.Status.Equals("cancelled", StringComparison.OrdinalIgnoreCase)),
+ _ => filtered
+ };
+
+ // Фильтр по поиску
+ if (!string.IsNullOrWhiteSpace(_searchText))
+ {
+ string search = _searchText.ToLower();
+ filtered = filtered.Where(r =>
+ r.DisplayType.ToLower().Contains(search) ||
+ r.DisplayStatus.ToLower().Contains(search) ||
+ r.DisplayDescription.ToLower().Contains(search) ||
+ r.EmployeeName.ToLower().Contains(search));
+ }
+
+ // Сортировка по дате (новые сверху)
+ filtered = filtered.OrderByDescending(r => r.CreatedAt);
+
+ _requests.Clear();
+ foreach (var request in filtered)
+ {
+ _requests.Add(request);
+ }
+
+ Trace.WriteLine($"[RequestsPage] >>> Отображено заявлений: {_requests.Count}");
+ }
+
+ private async void OnRefreshing(object sender, EventArgs e)
+ {
+ await LoadRequestsAsync();
+ }
+
+ private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
+ {
+ _searchText = e.NewTextValue ?? string.Empty;
+ ApplyFilters();
+ }
+
+ private async void OnRequestSelected(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is Request selectedRequest)
+ {
+ // Сбрасываем выбор
+ RequestsCollectionView.SelectedItem = null;
+
+ // Показываем детали заявления
+ await ShowRequestDetails(selectedRequest);
+ }
+ }
+
+ private async Task ShowRequestDetails(Request request)
+ {
+ string details = $"Тип: {request.DisplayType}\n" +
+ $"Статус: {request.DisplayStatus}\n" +
+ $"Дата создания: {request.DisplayCreatedAt}\n";
+
+ if (!string.IsNullOrEmpty(request.DisplayPeriod))
+ details += $"Период: {request.DisplayPeriod}\n";
+
+ details += $"\nОт кого: {request.EmployeeName}\n";
+
+ if (!string.IsNullOrEmpty(request.Comment))
+ details += $"\nОписание:\n{request.Comment}\n";
+
+ // Получатели
+ if (request.Recipients != null && request.Recipients.Count > 0)
+ {
+ details += $"\n📬 Получатели ({request.Recipients.Count}):\n";
+ foreach (var recipient in request.Recipients)
+ {
+ details += $" • {recipient.FullName}\n";
+ }
+ }
+ else if (request.RecipientCount > 0)
+ {
+ details += $"\n📬 Получателей: {request.RecipientCount}\n";
+ }
+
+ // В копии
+ if (request.CcUsers != null && request.CcUsers.Count > 0)
+ {
+ details += $"\n📋 В копии ({request.CcUsers.Count}):\n";
+ foreach (var ccUser in request.CcUsers)
+ {
+ details += $" • {ccUser.FullName}\n";
+ }
+ }
+ else if (request.CcCount > 0)
+ {
+ details += $"\n📋 В копии: {request.CcCount}\n";
+ }
+
+ // Решение
+ if (request.Approver != null)
+ details += $"\n✍️ Решение принял: {request.ApproverName}";
+
+ if (request.DecidedAt.HasValue)
+ details += $"\n📅 Дата решения: {request.DecidedAt.Value:dd.MM.yyyy HH:mm}";
+
+ await DisplayAlert($"{request.TypeIcon} {request.DisplayType}", details, "Закрыть");
+ }
+
+ // ================================
+ // Обработчики фильтров
+ // ================================
+
+ private void OnFilterAllTapped(object sender, TappedEventArgs e)
+ {
+ SetActiveFilter("all");
+ }
+
+ private void OnFilterPendingTapped(object sender, TappedEventArgs e)
+ {
+ SetActiveFilter("pending");
+ }
+
+ private void OnFilterApprovedTapped(object sender, TappedEventArgs e)
+ {
+ SetActiveFilter("approved");
+ }
+
+ private void OnFilterRejectedTapped(object sender, TappedEventArgs e)
+ {
+ SetActiveFilter("rejected");
+ }
+
+ private void SetActiveFilter(string filter)
+ {
+ _currentFilter = filter;
+
+ // Обновляем визуальное состояние кнопок
+ UpdateFilterButtonStyles();
+
+ // Применяем фильтры
+ ApplyFilters();
+ }
+
+ private void UpdateFilterButtonStyles()
+ {
+ // Сбрасываем все кнопки
+ FilterAll.BackgroundColor = Colors.White;
+ FilterAll.Stroke = Color.FromArgb("#D1D5DB");
+ if (FilterAll.Content is Label labelAll)
+ labelAll.TextColor = Color.FromArgb("#374151");
+
+ FilterPending.BackgroundColor = Colors.White;
+ FilterPending.Stroke = Color.FromArgb("#D1D5DB");
+ if (FilterPending.Content is Label labelPending)
+ labelPending.TextColor = Color.FromArgb("#374151");
+
+ FilterApproved.BackgroundColor = Colors.White;
+ FilterApproved.Stroke = Color.FromArgb("#D1D5DB");
+ if (FilterApproved.Content is Label labelApproved)
+ labelApproved.TextColor = Color.FromArgb("#374151");
+
+ FilterRejected.BackgroundColor = Colors.White;
+ FilterRejected.Stroke = Color.FromArgb("#D1D5DB");
+ if (FilterRejected.Content is Label labelRejected)
+ labelRejected.TextColor = Color.FromArgb("#374151");
+
+ // Активируем выбранную кнопку
+ Border activeButton = _currentFilter switch
+ {
+ "pending" => FilterPending,
+ "approved" => FilterApproved,
+ "rejected" => FilterRejected,
+ _ => FilterAll
+ };
+
+ activeButton.BackgroundColor = (Color)Application.Current!.Resources["Primary"];
+ activeButton.Stroke = Colors.Transparent;
+ if (activeButton.Content is Label activeLabel)
+ activeLabel.TextColor = Colors.White;
+ }
+}
+
diff --git a/CORP-MAUI/Pages/UsersPage.xaml.cs b/CORP-MAUI/Pages/UsersPage.xaml.cs
deleted file mode 100644
index 7e9662c..0000000
--- a/CORP-MAUI/Pages/UsersPage.xaml.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using CORP_MAUI.Models;
-
-namespace CORP_MAUI.Pages;
-
-public partial class UsersPage : ContentPage
-{
- private readonly ObservableCollection _users;
- private List _allUsers = new();
-
- public UsersPage()
- {
- try
- {
- Trace.WriteLine("=== USERSPAGE CONSTRUCTOR START ===");
- Trace.WriteLine("[UsersPage] >>> Вызов InitializeComponent");
-
- InitializeComponent();
-
- Trace.WriteLine("[UsersPage] >>> InitializeComponent завершен");
-
- _users = new ObservableCollection();
-
- UsersCollectionView.ItemsSource = _users;
-
- Trace.WriteLine("[UsersPage] >>> Подписка на UserStatusChanged");
- // Подписываемся на изменения статуса пользователей
- ServiceProvider.WebSocketService.UserStatusChanged += OnUserStatusChanged;
-
- 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);
- Page? 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;
-
- Trace.WriteLine("[UsersPage] >>> Вызов GetAllEmployeesAsync");
- _allUsers = await ServiceProvider.ApiService.GetAllEmployeesAsync();
-
- Trace.WriteLine($"[UsersPage] >>> Получено пользователей: {_allUsers.Count}");
-
- _users.Clear();
- foreach (User user in _allUsers.OrderBy(u => u.FullName))
- {
- _users.Add(user);
- }
-
- Trace.WriteLine($"[UsersPage] ✅ Пользователи загружены успешно");
- }
- catch (Exception ex)
- {
- 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
- {
- LoadingIndicator.IsVisible = false;
- LoadingIndicator.IsRunning = false;
- RefreshView.IsRefreshing = false;
- }
- }
-
- private async void OnRefreshing(object sender, EventArgs e)
- {
- await LoadUsersAsync();
- }
-
- private async void OnUserSelected(object sender, SelectionChangedEventArgs e)
- {
- if (e.CurrentSelection.FirstOrDefault() is User selectedUser)
- {
- // Сбрасываем выбор
- UsersCollectionView.SelectedItem = null;
-
- // Переходим на страницу чата
- await Navigation.PushAsync(new ChatPage(selectedUser));
- }
- }
-
- private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
- {
- string searchText = e.NewTextValue?.ToLower() ?? string.Empty;
-
- if (string.IsNullOrWhiteSpace(searchText))
- {
- // Показываем всех пользователей
- _users.Clear();
- foreach (User user in _allUsers.OrderBy(u => u.FullName))
- {
- _users.Add(user);
- }
- }
- else
- {
- // Фильтруем пользователей
- List filtered = _allUsers.Where(u =>
- u.FullName.ToLower().Contains(searchText) ||
- u.Username.ToLower().Contains(searchText) ||
- u.DepartmentName.ToLower().Contains(searchText) ||
- u.PositionName.ToLower().Contains(searchText)
- ).ToList();
-
- _users.Clear();
- foreach (User user in filtered.OrderBy(u => u.FullName))
- {
- _users.Add(user);
- }
- }
- }
-
- private void OnUserStatusChanged(object? sender, User updatedUser)
- {
- // Обновляем статус пользователя в списке
- MainThread.BeginInvokeOnMainThread(() =>
- {
- User? user = _users.FirstOrDefault(u => u.Id == updatedUser.Id);
- if (user != null)
- {
- user.IsOnline = updatedUser.IsOnline;
- }
-
- // Обновляем в основном списке
- User? allUser = _allUsers?.FirstOrDefault(u => u.Id == updatedUser.Id);
- if (allUser != null)
- {
- allUser.IsOnline = updatedUser.IsOnline;
- }
- });
- }
-
- protected override void OnDisappearing()
- {
- base.OnDisappearing();
- ServiceProvider.WebSocketService.UserStatusChanged -= OnUserStatusChanged;
- }
-}
-
diff --git a/CORP-MAUI/Resources/Styles/AppStyles.xaml b/CORP-MAUI/Resources/Styles/AppStyles.xaml
new file mode 100644
index 0000000..fabd763
--- /dev/null
+++ b/CORP-MAUI/Resources/Styles/AppStyles.xaml
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Resources/Styles/Colors.xaml b/CORP-MAUI/Resources/Styles/Colors.xaml
index bab1d20..71b29ac 100644
--- a/CORP-MAUI/Resources/Styles/Colors.xaml
+++ b/CORP-MAUI/Resources/Styles/Colors.xaml
@@ -26,6 +26,7 @@
#6E6E6E
#404040
#303030
+ #282828
#212121
#141414
@@ -44,6 +45,7 @@
+
diff --git a/CORP-MAUI/Services/ApiService.cs b/CORP-MAUI/Services/ApiService.cs
index 1eeb0c9..a0c0cf0 100644
--- a/CORP-MAUI/Services/ApiService.cs
+++ b/CORP-MAUI/Services/ApiService.cs
@@ -38,7 +38,7 @@ public class ApiService : IDisposable
private string? _accessToken;
private string? _refreshToken;
- private bool _tokensLoaded = false;
+ private bool _tokensLoaded;
private readonly SemaphoreSlim _tokenLoadSemaphore = new(1, 1);
// Ключи для SecureStorage
@@ -730,7 +730,7 @@ public class ApiService : IDisposable
}
///
- /// Получить список всех сотрудников (все страницы)
+ /// Получить список всех активных сотрудников (все страницы)
///
public async Task> GetAllEmployeesAsync()
{
@@ -742,7 +742,15 @@ public class ApiService : IDisposable
do
{
- response = await GetEmployeesPaginatedAsync(page: currentPage);
+ Dictionary queryParams = new Dictionary
+ {
+ ["active"] = "true",
+ ["page"] = currentPage.ToString()
+ };
+
+ ApiResponse> apiResponse = await GetAsync>("api/v1/employees/", queryParams);
+ response = apiResponse.Ok && apiResponse.Data != null ? apiResponse.Data : new PaginatedResponse();
+
if (response.Results.Any())
{
allEmployees.AddRange(response.Results);
@@ -750,28 +758,86 @@ public class ApiService : IDisposable
}
} while (!string.IsNullOrEmpty(response.Next));
+ Trace.WriteLine($"[ApiService] ✅ Загружено активных сотрудников: {allEmployees.Count}");
return allEmployees;
}
catch (Exception ex)
{
- Trace.WriteLine($"Ошибка получения всех сотрудников: {ex.Message}");
+ Trace.WriteLine($"[ApiService] ❌ Ошибка получения всех активных сотрудников: {ex.Message}");
return new List();
}
}
+ ///
+ /// Получить информацию о конкретном пользователе по ID
+ ///
+ public async Task GetUserByIdAsync(int userId)
+ {
+ try
+ {
+ ApiResponse response = await GetAsync($"api/v1/employees/{userId}/");
+
+ if (response.Ok && response.Data != null)
+ {
+ Trace.WriteLine($"[ApiService] ✅ Пользователь загружен: {response.Data.FullName}");
+ return response.Data;
+ }
+
+ Trace.WriteLine($"[ApiService] ❌ Пользователь с ID {userId} не найден");
+ return null;
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[ApiService] ❌ Ошибка получения пользователя {userId}: {ex.Message}");
+ return null;
+ }
+ }
+
///
/// Получить информацию о текущем пользователе
+ /// Использует эндпоинт /api/v1/employees/me/
///
public async Task GetCurrentUserAsync()
{
try
{
+ Trace.WriteLine("\n╔═══════════════════════════════════════════════════════════════════════════════════");
+ Trace.WriteLine("║ [ApiService] FETCHING CURRENT USER PROFILE");
+ Trace.WriteLine("╠════════════════════════════════════════════════════════════════════════════════════");
+ Trace.WriteLine($"║ Endpoint: api/v1/employees/me/");
+ Trace.WriteLine($"║ Has Access Token: {!string.IsNullOrEmpty(_accessToken)}");
+ if (!string.IsNullOrEmpty(_accessToken))
+ {
+ Trace.WriteLine($"║ Token (first 30 chars): {_accessToken.Substring(0, Math.Min(30, _accessToken.Length))}...");
+ }
+ Trace.WriteLine("╚════════════════════════════════════════════════════════════════════════════════════\n");
+
ApiResponse response = await GetAsync("api/v1/employees/me/");
- return response.Ok ? response.Data : null;
+
+ if (response.Ok && response.Data != null)
+ {
+ Trace.WriteLine($"[ApiService] ✅ Current user profile loaded successfully");
+ Trace.WriteLine($" User ID: {response.Data.Id}");
+ Trace.WriteLine($" Email: {response.Data.Email}");
+ Trace.WriteLine($" Name: {response.Data.FirstName} {response.Data.LastName}");
+ Trace.WriteLine($" Position: {response.Data.PositionName}");
+ Trace.WriteLine($" Departments: {response.Data.Departments?.Count ?? 0}");
+ Trace.WriteLine($" Skills: {response.Data.Skills?.Count ?? 0}\n");
+ return response.Data;
+ }
+ else
+ {
+ Trace.WriteLine($"[ApiService] ❌ Failed to load current user profile");
+ Trace.WriteLine($" Status: {response.Status}");
+ Trace.WriteLine($" Error: {response.ErrorMessage ?? "Unknown error"}");
+ Trace.WriteLine($" Response Text: {response.Text?.Substring(0, Math.Min(200, response.Text?.Length ?? 0))}\n");
+ return null;
+ }
}
catch (Exception ex)
{
- Trace.WriteLine($"Ошибка получения профиля: {ex.Message}");
+ Trace.WriteLine($"[ApiService] ❌ Exception while getting current user profile: {ex.Message}");
+ Trace.WriteLine($" StackTrace: {ex.StackTrace}\n");
return null;
}
}
@@ -809,6 +875,26 @@ public class ApiService : IDisposable
if (pageSize.HasValue) queryParams["page_size"] = pageSize.Value.ToString();
ApiResponse> response = await GetAsync>("api/v1/departments/", queryParams);
+
+ // Логируем сырой JSON ответ
+ if (response.Ok && !string.IsNullOrEmpty(response.Text))
+ {
+ Trace.WriteLine($"[ApiService] 📄 Raw JSON response (first 500 chars):");
+ Trace.WriteLine(response.Text.Substring(0, Math.Min(500, response.Text.Length)));
+ }
+
+ // Логируем первый отдел для проверки
+ if (response.Ok && response.Data != null && response.Data.Results.Any())
+ {
+ var firstDept = response.Data.Results.First();
+ Trace.WriteLine($"[ApiService] 📊 Первый загруженный отдел:");
+ Trace.WriteLine($" - ID: {firstDept.Id}");
+ Trace.WriteLine($" - Name: '{firstDept.Name}'");
+ Trace.WriteLine($" - Description: '{firstDept.Description}'");
+ Trace.WriteLine($" - EmployeesCount: {firstDept.EmployeesCount}");
+ Trace.WriteLine($" - Head: {firstDept.Head?.FullName ?? "null"}");
+ }
+
return response.Ok && response.Data != null ? response.Data : new PaginatedResponse();
}
catch (Exception ex)
@@ -848,6 +934,68 @@ public class ApiService : IDisposable
}
}
+ ///
+ /// Получить информацию о конкретном отделе
+ ///
+ public async Task GetDepartmentByIdAsync(int departmentId)
+ {
+ try
+ {
+ ApiResponse response = await GetAsync($"api/v1/departments/{departmentId}/");
+ return response.Ok && response.Data != null ? response.Data : null;
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Ошибка получения отдела {departmentId}: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Получить список сотрудников отдела
+ ///
+ public async Task> GetDepartmentEmployeesAsync(int departmentId)
+ {
+ try
+ {
+ Dictionary queryParams = new Dictionary
+ {
+ { "department", departmentId.ToString() }
+ };
+
+ List allEmployees = new List();
+ int currentPage = 1;
+
+ do
+ {
+ queryParams["page"] = currentPage.ToString();
+ var apiResponse = await GetAsync>("api/v1/employees/", queryParams);
+
+ if (!apiResponse.Ok || apiResponse.Data == null)
+ break;
+
+ var response = apiResponse.Data;
+
+ if (response.Results.Any())
+ {
+ allEmployees.AddRange(response.Results);
+ currentPage++;
+ }
+
+ if (string.IsNullOrEmpty(response.Next))
+ break;
+
+ } while (true);
+
+ return allEmployees;
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"Ошибка получения сотрудников отдела {departmentId}: {ex.Message}");
+ return new List();
+ }
+ }
+
// ============================================
// POSITIONS (Должности)
// ============================================
@@ -961,6 +1109,46 @@ public class ApiService : IDisposable
}
}
+ ///
+ /// Получить все заявления (с автоматической пагинацией)
+ ///
+ public async Task> GetAllRequestsAsync()
+ {
+ List allRequests = new List();
+ int page = 1;
+ bool hasMore = true;
+
+ try
+ {
+ Trace.WriteLine("[ApiService] >>> Загрузка всех заявлений...");
+
+ while (hasMore)
+ {
+ PaginatedResponse response = await GetRequestsPaginatedAsync(page);
+
+ if (response.Results != null && response.Results.Any())
+ {
+ allRequests.AddRange(response.Results);
+ Trace.WriteLine($"[ApiService] >>> Загружено заявлений: {allRequests.Count} / {response.Count}");
+ hasMore = !string.IsNullOrEmpty(response.Next);
+ page++;
+ }
+ else
+ {
+ hasMore = false;
+ }
+ }
+
+ Trace.WriteLine($"[ApiService] ✅ Всего загружено заявлений: {allRequests.Count}");
+ return allRequests;
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки заявлений: {ex.Message}");
+ return allRequests;
+ }
+ }
+
// ============================================
// EMPLOYEE ACTIONS (Действия сотрудников)
// ============================================
diff --git a/CORP-MAUI/ViewModels/PostViewModel.cs b/CORP-MAUI/ViewModels/PostViewModel.cs
index a1d8274..ab052a8 100644
--- a/CORP-MAUI/ViewModels/PostViewModel.cs
+++ b/CORP-MAUI/ViewModels/PostViewModel.cs
@@ -60,16 +60,55 @@ public class PostViewModel : INotifyPropertyChanged
{
get
{
+ // 1. Проверяем установленный _imageSource (для предварительно загруженных)
if (_imageSource != null) return _imageSource;
+ // 2. Проверяем, есть ли Base64 изображение в Post.Image
+ if (Post != null && !string.IsNullOrEmpty(Post.Image))
+ {
+ // Только если это Base64 - декодируем напрямую
+ if (Post.Image.StartsWith("data:image"))
+ {
+ try
+ {
+ // Извлекаем Base64 данные из строки "data:image/jpeg;base64,..."
+ string base64Data = Post.Image;
+ int commaIndex = base64Data.IndexOf(',');
+ if (commaIndex > 0)
+ {
+ base64Data = base64Data.Substring(commaIndex + 1);
+ byte[] imageBytes = Convert.FromBase64String(base64Data);
+ Trace.WriteLine($"[PostViewModel] ✅ Base64 изображение декодировано ({imageBytes.Length} bytes)");
+ return ImageSource.FromStream(() => new MemoryStream(imageBytes));
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[PostViewModel] ⚠️ Ошибка декодирования Base64 изображения: {ex.Message}");
+ }
+ }
+ // Если Post.Image содержит URL (не Base64), то он будет обработан через DisplayImageUrl
+ // и загружен в PostsPage, после чего попадёт в _localImagePath
+ }
+
+ // 3. Проверяем локальный кеш (для URL изображений, загруженных в PostsPage)
if (!string.IsNullOrEmpty(_localImagePath) && File.Exists(_localImagePath))
{
// Basic check for empty files
try
{
- if (new FileInfo(_localImagePath).Length == 0) return null;
+ FileInfo fileInfo = new FileInfo(_localImagePath);
+ if (fileInfo.Length == 0)
+ {
+ Trace.WriteLine($"[PostViewModel] ⚠️ Файл изображения пуст: {_localImagePath}");
+ return null;
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[PostViewModel] ⚠️ Ошибка проверки файла: {ex.Message}");
+ return null;
}
- catch { return null; }
// Используем FromStream для надежности, чтобы не блокировать файл
// и гарантировать чтение даже из системных папок
@@ -81,11 +120,12 @@ public class PostViewModel : INotifyPropertyChanged
}
catch (Exception ex)
{
- Trace.WriteLine($"[PostViewModel] Error reading image {_localImagePath}: {ex.Message}");
+ Trace.WriteLine($"[PostViewModel] ⚠️ Ошибка чтения изображения {_localImagePath}: {ex.Message}");
return null;
}
});
}
+
return null;
}
}
diff --git a/CORP-MAUI/ViewModels/ProfileViewModel.cs b/CORP-MAUI/ViewModels/ProfileViewModel.cs
index f862c6a..44f3a6c 100644
--- a/CORP-MAUI/ViewModels/ProfileViewModel.cs
+++ b/CORP-MAUI/ViewModels/ProfileViewModel.cs
@@ -36,6 +36,7 @@ public class ProfileViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(StatusColor));
OnPropertyChanged(nameof(Skills));
OnPropertyChanged(nameof(HasSkills));
+ OnPropertyChanged(nameof(DisplayImageSource)); // Обновляем изображение при изменении пользователя
}
}
@@ -66,26 +67,56 @@ public class ProfileViewModel : INotifyPropertyChanged
{
get
{
- if (!string.IsNullOrEmpty(_localImagePath) && File.Exists(_localImagePath))
+ // Сначала проверяем, есть ли Base64 изображение в User.Avatar
+ if (_user != null && !string.IsNullOrEmpty(_user.Avatar))
+ {
+ try
+ {
+ // Проверяем, это Base64 строка?
+ if (_user.Avatar.StartsWith("data:image"))
+ {
+ // Извлекаем Base64 данные из строки "data:image/jpeg;base64,..."
+ string base64Data = _user.Avatar;
+ int commaIndex = base64Data.IndexOf(',');
+ if (commaIndex > 0)
+ {
+ base64Data = base64Data.Substring(commaIndex + 1);
+ byte[] imageBytes = Convert.FromBase64String(base64Data);
+ return ImageSource.FromStream(() => new MemoryStream(imageBytes));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Ошибка декодирования Base64 аватара: {ex.Message}");
+ }
+ }
+
+ // Если есть локальный файл кеша
+ if (!string.IsNullOrEmpty(_localImagePath) && File.Exists(_localImagePath))
{
try
{
- if (new FileInfo(_localImagePath).Length == 0) return null;
- return ImageSource.FromStream(() =>
+ FileInfo fileInfo = new FileInfo(_localImagePath);
+ if (fileInfo.Length > 0)
{
- try
+ return ImageSource.FromStream(() =>
{
- return new FileStream(_localImagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
- }
- catch
- {
- return null;
- }
- });
+ try
+ {
+ return new FileStream(_localImagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ }
+ catch
+ {
+ return null;
+ }
+ });
+ }
}
- catch { return null; }
+ catch { }
}
- // Default placeholder if needed
+
+ // Default placeholder
return ImageSource.FromFile("dotnet_bot.png");
}
}
@@ -100,67 +131,130 @@ public class ProfileViewModel : INotifyPropertyChanged
try
{
+ Trace.WriteLine("[ProfileViewModel] >>> Начало загрузки профиля");
User? loadedUser = await ServiceProvider.ApiService.GetCurrentUserAsync();
+
if (loadedUser != null)
{
+ Trace.WriteLine($"[ProfileViewModel] ✅ Пользователь загружен: {loadedUser.FullName}");
User = loadedUser;
- await LoadAvatarAsync();
+
+ try
+ {
+ Trace.WriteLine("[ProfileViewModel] >>> Загрузка аватара");
+ await LoadAvatarAsync();
+ Trace.WriteLine("[ProfileViewModel] ✅ Аватар загружен успешно");
+ }
+ catch (Exception avatarEx)
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Ошибка загрузки аватара (не критично): {avatarEx.Message}");
+ // Ошибка загрузки аватара не критична - продолжаем работу
+ }
+ }
+ else
+ {
+ Trace.WriteLine("[ProfileViewModel] ❌ loadedUser == null");
+ Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
+ if (mainPage != null)
+ await mainPage.DisplayAlert("Ошибка", "Не удалось загрузить профиль", "OK");
}
}
catch (Exception ex)
{
- Trace.WriteLine($"Error loading profile: {ex.Message}");
+ Trace.WriteLine($"[ProfileViewModel] ❌ Исключение при загрузке профиля:");
+ Trace.WriteLine($" Type: {ex.GetType().Name}");
+ Trace.WriteLine($" Message: {ex.Message}");
+ Trace.WriteLine($" StackTrace: {ex.StackTrace}");
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
if (mainPage != null)
- await mainPage.DisplayAlert("Ошибка", "Не удалось загрузить профиль", "OK");
+ await mainPage.DisplayAlert("Ошибка", $"Не удалось загрузить профиль: {ex.Message}", "OK");
}
finally
{
IsLoading = false;
+ Trace.WriteLine("[ProfileViewModel] <<< Завершение загрузки профиля");
}
}
private async Task LoadAvatarAsync()
{
- if (_user == null || string.IsNullOrEmpty(_user.DisplayAvatarUrl)) return;
+ // Если аватар уже в виде Base64 в User.Avatar, ничего не делаем
+ if (_user != null && !string.IsNullOrEmpty(_user.Avatar) && _user.Avatar.StartsWith("data:image"))
+ {
+ Trace.WriteLine("[ProfileViewModel] ✅ Аватар уже загружен как Base64, пропускаем загрузку");
+ return;
+ }
+
+ if (_user == null || string.IsNullOrEmpty(_user.DisplayAvatarUrl))
+ {
+ Trace.WriteLine("[ProfileViewModel] Нет URL аватара для загрузки");
+ return;
+ }
try
{
string url = _user.DisplayAvatarUrl;
- // Generate cache filename
- Uri uri = new Uri(url);
+ Trace.WriteLine($"[ProfileViewModel] Загрузка аватара с URL: {url}");
+
+ // Generate cache filename - безопасное создание Uri
+ Uri? uri = null;
+ try
+ {
+ uri = new Uri(url);
+ }
+ catch (UriFormatException uriEx)
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Некорректный URL аватара: {uriEx.Message}");
+ return;
+ }
+
string extension = Path.GetExtension(uri.LocalPath);
if (string.IsNullOrEmpty(extension)) extension = ".jpg";
string fileName = Path.GetFileNameWithoutExtension(uri.LocalPath);
+ if (string.IsNullOrEmpty(fileName)) fileName = $"avatar_{_user.Id}";
+
+ // Заменяем все недопустимые символы
fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c, '_'));
string cacheFileName = $"user_{_user.Id}_{fileName}{extension}";
string cacheFilePath = Path.Combine(FileSystem.CacheDirectory, cacheFileName);
+ Trace.WriteLine($"[ProfileViewModel] Путь кеша: {cacheFilePath}");
+
// Check cache
if (File.Exists(cacheFilePath))
{
FileInfo fileInfo = new FileInfo(cacheFilePath);
if (fileInfo.Length > 0)
{
+ Trace.WriteLine($"[ProfileViewModel] ✅ Аватар найден в кеше ({fileInfo.Length} bytes)");
_localImagePath = cacheFilePath;
OnPropertyChanged(nameof(DisplayImageSource));
return;
}
+ else
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Файл кеша пуст, удаляем");
+ try { File.Delete(cacheFilePath); } catch { }
+ }
}
// Download
+ Trace.WriteLine($"[ProfileViewModel] Загрузка аватара с сервера...");
byte[]? bytes = await ServiceProvider.ApiService.GetImageBytesAsync(url);
+
if (bytes is { Length: > 0 })
{
+ Trace.WriteLine($"[ProfileViewModel] Получено {bytes.Length} bytes");
+
// Simple signature check
bool isImage = false;
if (bytes.Length > 2)
{
- if (bytes[0] == 0xFF && bytes[1] == 0xD8) isImage = true;
- else if (bytes[0] == 0x89 && bytes[1] == 0x50) isImage = true;
+ if (bytes[0] == 0xFF && bytes[1] == 0xD8) isImage = true; // JPEG
+ else if (bytes[0] == 0x89 && bytes[1] == 0x50) isImage = true; // PNG
else isImage = true; // Relaxed check
if (bytes[0] == '<') isImage = false; // HTML check
@@ -168,15 +262,29 @@ public class ProfileViewModel : INotifyPropertyChanged
if (isImage)
{
+ Trace.WriteLine($"[ProfileViewModel] ✅ Данные похожи на изображение, сохраняем в кеш");
await File.WriteAllBytesAsync(cacheFilePath, bytes);
_localImagePath = cacheFilePath;
OnPropertyChanged(nameof(DisplayImageSource));
+ Trace.WriteLine($"[ProfileViewModel] ✅ Аватар сохранён и отображён");
}
+ else
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Данные не похожи на изображение");
+ }
+ }
+ else
+ {
+ Trace.WriteLine($"[ProfileViewModel] ⚠️ Не удалось загрузить данные аватара (пустой ответ)");
}
}
catch (Exception ex)
{
- Trace.WriteLine($"Avatar load error: {ex.Message}");
+ Trace.WriteLine($"[ProfileViewModel] ❌ Ошибка загрузки аватара:");
+ Trace.WriteLine($" Type: {ex.GetType().Name}");
+ Trace.WriteLine($" Message: {ex.Message}");
+ Trace.WriteLine($" StackTrace: {ex.StackTrace}");
+ // Не пробрасываем исключение дальше - ошибка загрузки аватара не критична
}
}
diff --git a/TECHDOC/ARCHITECTURE.md b/TECHDOC/ARCHITECTURE.md
index 7c2bb1e..f5cbbbb 100644
--- a/TECHDOC/ARCHITECTURE.md
+++ b/TECHDOC/ARCHITECTURE.md
@@ -14,7 +14,7 @@ CORP-MAUI/
├── Pages/ # Страницы приложения
│ ├── LoginPage.xaml # Страница входа
│ ├── ProfilePage.xaml # Профиль пользователя
-│ ├── UsersPage.xaml # Список сотрудников
+│ ├── EmployeesPage.xaml # Список сотрудников
│ └── ChatPage.xaml # Чат с сотрудником
├── Platforms/ # Платформенно-специфичный код
│ ├── Android/
@@ -162,7 +162,7 @@ WebSocketService использует события (Events) для уведо
### Страницы
1. **LoginPage** - аутентификация
-2. **UsersPage** - список сотрудников с поиском
+2. **EmployeesPage** - список сотрудников с поиском
3. **ChatPage** - личные сообщения
4. **ProfilePage** - профиль пользователя
diff --git a/TECHDOC/CODE_EXAMPLES.md b/TECHDOC/CODE_EXAMPLES.md
index 39612bc..4142445 100644
--- a/TECHDOC/CODE_EXAMPLES.md
+++ b/TECHDOC/CODE_EXAMPLES.md
@@ -126,7 +126,7 @@ private async void OnLogoutClicked(object sender, EventArgs e)
### Получение списка сотрудников
```csharp
-// В UsersPage.xaml.cs
+// В EmployeesPage.xaml.cs
private List _allUsers;
private ObservableCollection _displayedUsers;
@@ -162,7 +162,7 @@ private async Task LoadUsersAsync()
### Поиск пользователей
```csharp
-// В UsersPage.xaml.cs
+// В EmployeesPage.xaml.cs
private async void OnSearchTextChanged(object sender, TextChangedEventArgs e)
{
var searchText = e.NewTextValue?.Trim();
@@ -211,7 +211,7 @@ private async void OnSearchTextChanged(object sender, TextChangedEventArgs e)
### Pull-to-Refresh
```csharp
-// В UsersPage.xaml.cs
+// В EmployeesPage.xaml.cs
private async void OnRefreshing(object sender, EventArgs e)
{
try
@@ -226,9 +226,9 @@ private async void OnRefreshing(object sender, EventArgs e)
```
```xml
-
+
-
+
@@ -454,7 +454,7 @@ private async Task EnsureWebSocketConnection()
### Обработка изменения статуса пользователя
```csharp
-// В UsersPage.xaml.cs
+// В EmployeesPage.xaml.cs
protected override void OnAppearing()
{
base.OnAppearing();
diff --git a/clean-and-build.bat b/clean-and-build.bat
new file mode 100644
index 0000000..fb1e673
--- /dev/null
+++ b/clean-and-build.bat
@@ -0,0 +1,26 @@
+@echo off
+echo Cleaning CORP-MAUI project...
+
+REM Kill any running CORP-MAUI processes
+taskkill /F /IM CORP-MAUI.exe 2>nul
+
+REM Wait a bit
+timeout /t 2 /nobreak >nul
+
+REM Clean the project
+cd /d "%~dp0"
+dotnet clean
+
+echo.
+echo Deleting bin and obj folders...
+if exist "CORP-MAUI\bin" rmdir /s /q "CORP-MAUI\bin"
+if exist "CORP-MAUI\obj" rmdir /s /q "CORP-MAUI\obj"
+
+echo.
+echo Building project...
+dotnet build --no-restore
+
+echo.
+echo Done!
+pause
+
diff --git a/clean-and-build.ps1 b/clean-and-build.ps1
new file mode 100644
index 0000000..f59e1b5
--- /dev/null
+++ b/clean-and-build.ps1
@@ -0,0 +1,84 @@
+# Скрипт для очистки и сборки CORP-MAUI проекта
+
+Write-Host "=====================================" -ForegroundColor Cyan
+Write-Host " CORP-MAUI Clean & Build Script" -ForegroundColor Cyan
+Write-Host "=====================================" -ForegroundColor Cyan
+Write-Host ""
+
+# Переходим в директорию скрипта
+$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
+Set-Location $scriptPath
+
+# Завершаем все процессы CORP-MAUI
+Write-Host "Stopping CORP-MAUI processes..." -ForegroundColor Yellow
+$processes = Get-Process | Where-Object {
+ $_.ProcessName -like "*CORP-MAUI*" -or
+ $_.ProcessName -like "*CORP_MAUI*"
+}
+
+if ($processes) {
+ $processes | ForEach-Object {
+ Write-Host " Stopping process: $($_.ProcessName) (PID: $($_.Id))" -ForegroundColor Gray
+ Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
+ }
+ Start-Sleep -Seconds 2
+ Write-Host " Processes stopped." -ForegroundColor Green
+} else {
+ Write-Host " No running processes found." -ForegroundColor Green
+}
+
+Write-Host ""
+
+# Очищаем проект через dotnet
+Write-Host "Cleaning project with 'dotnet clean'..." -ForegroundColor Yellow
+dotnet clean --verbosity quiet
+Write-Host " Done." -ForegroundColor Green
+
+Write-Host ""
+
+# Удаляем папки bin и obj
+Write-Host "Deleting bin and obj folders..." -ForegroundColor Yellow
+
+$binPath = Join-Path $scriptPath "CORP-MAUI\bin"
+$objPath = Join-Path $scriptPath "CORP-MAUI\obj"
+
+if (Test-Path $binPath) {
+ Remove-Item -Path $binPath -Recurse -Force -ErrorAction SilentlyContinue
+ Write-Host " Deleted: $binPath" -ForegroundColor Gray
+}
+
+if (Test-Path $objPath) {
+ Remove-Item -Path $objPath -Recurse -Force -ErrorAction SilentlyContinue
+ Write-Host " Deleted: $objPath" -ForegroundColor Gray
+}
+
+Write-Host " Done." -ForegroundColor Green
+
+Write-Host ""
+
+# Собираем проект
+Write-Host "Building project..." -ForegroundColor Yellow
+Write-Host ""
+
+$buildOutput = dotnet build --no-restore 2>&1
+
+# Проверяем результат
+if ($LASTEXITCODE -eq 0) {
+ Write-Host ""
+ Write-Host "=====================================" -ForegroundColor Green
+ Write-Host " BUILD SUCCESSFUL!" -ForegroundColor Green
+ Write-Host "=====================================" -ForegroundColor Green
+} else {
+ Write-Host ""
+ Write-Host "=====================================" -ForegroundColor Red
+ Write-Host " BUILD FAILED!" -ForegroundColor Red
+ Write-Host "=====================================" -ForegroundColor Red
+ Write-Host ""
+ Write-Host "Build output:" -ForegroundColor Yellow
+ Write-Host $buildOutput
+}
+
+Write-Host ""
+Write-Host "Press any key to exit..."
+$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+
diff --git a/test-departments-api.ps1 b/test-departments-api.ps1
new file mode 100644
index 0000000..2420b01
--- /dev/null
+++ b/test-departments-api.ps1
@@ -0,0 +1,44 @@
+# Тестовый скрипт для проверки API отделов
+
+$apiUrl = "https://corp.robotail.pro/api/v1/departments/"
+
+Write-Host "Получение данных из API..." -ForegroundColor Yellow
+Write-Host "URL: $apiUrl" -ForegroundColor Gray
+Write-Host ""
+
+try {
+ $response = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers @{
+ "Accept" = "application/json"
+ }
+
+ Write-Host "✅ Успешно получено отделов: $($response.results.Count)" -ForegroundColor Green
+ Write-Host ""
+
+ # Выводим первые 3 отдела
+ $response.results | Select-Object -First 3 | ForEach-Object {
+ Write-Host "─────────────────────────────────" -ForegroundColor Cyan
+ Write-Host "ID: $($_.id)" -ForegroundColor White
+ Write-Host "Name: $($_.name)" -ForegroundColor White
+ Write-Host "Description: '$($_.description)'" -ForegroundColor Yellow
+ Write-Host "Description Length: $($_.description.Length)" -ForegroundColor Gray
+ Write-Host "Employees Count: $($_.employees_count)" -ForegroundColor White
+ if ($_.head) {
+ Write-Host "Head: $($_.head.full_name)" -ForegroundColor White
+ } else {
+ Write-Host "Head: null" -ForegroundColor Gray
+ }
+ }
+ Write-Host "─────────────────────────────────" -ForegroundColor Cyan
+
+ Write-Host ""
+ Write-Host "Полный JSON первого отдела:" -ForegroundColor Yellow
+ $response.results[0] | ConvertTo-Json -Depth 3
+
+} catch {
+ Write-Host "❌ Ошибка: $($_.Exception.Message)" -ForegroundColor Red
+}
+
+Write-Host ""
+Write-Host "Press any key to exit..."
+$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
+
diff --git a/test-deserialization.csx b/test-deserialization.csx
new file mode 100644
index 0000000..78bb40a
--- /dev/null
+++ b/test-deserialization.csx
@@ -0,0 +1,74 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+// Тестовая модель
+public class TestDepartment
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+
+ [JsonPropertyName("employees_count")]
+ public int EmployeesCount { get; set; }
+
+ [JsonIgnore]
+ public string DisplayDescription =>
+ string.IsNullOrWhiteSpace(Description)
+ ? "Описание отсутствует"
+ : Description;
+}
+
+// Тестовые данные - то, что приходит с API
+string json = @"{
+ ""id"": 1,
+ ""name"": ""IT"",
+ ""description"": ""Отдел информационных технологий"",
+ ""employees_count"": 7
+}";
+
+Console.WriteLine("JSON от API:");
+Console.WriteLine(json);
+Console.WriteLine();
+
+// Десериализация
+var options = new JsonSerializerOptions
+{
+ PropertyNameCaseInsensitive = true
+};
+
+var dept = JsonSerializer.Deserialize(json, options);
+
+Console.WriteLine("Результат десериализации:");
+Console.WriteLine($"ID: {dept.Id}");
+Console.WriteLine($"Name: {dept.Name}");
+Console.WriteLine($"Description: '{dept.Description}'");
+Console.WriteLine($"Description Length: {dept.Description.Length}");
+Console.WriteLine($"EmployeesCount: {dept.EmployeesCount}");
+Console.WriteLine($"DisplayDescription: '{dept.DisplayDescription}'");
+Console.WriteLine();
+
+// Тест с пустым описанием
+string json2 = @"{
+ ""id"": 2,
+ ""name"": ""Маркетинг"",
+ ""description"": """",
+ ""employees_count"": 6
+}";
+
+Console.WriteLine("JSON с пустым описанием:");
+Console.WriteLine(json2);
+Console.WriteLine();
+
+var dept2 = JsonSerializer.Deserialize(json2, options);
+
+Console.WriteLine("Результат десериализации:");
+Console.WriteLine($"ID: {dept2.Id}");
+Console.WriteLine($"Name: {dept2.Name}");
+Console.WriteLine($"Description: '{dept2.Description}'");
+Console.WriteLine($"Description Length: {dept2.Description.Length}");
+Console.WriteLine($"EmployeesCount: {dept2.EmployeesCount}");
+Console.WriteLine($"DisplayDescription: '{dept2.DisplayDescription}'");
+
+Console.WriteLine();
+Console.WriteLine("Тест завершен успешно!");
+