Refactor Profile and Navigation: Update User and Department pages, add EmployeeProfile and Departments pages, and implement new layout and functionality

This commit is contained in:
Данил Ярош 2026-01-15 13:34:49 +03:00
parent b168416fa6
commit 979d79780b
36 changed files with 3085 additions and 291 deletions

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="RiderAndroidProjectSystem" />
</component>
</project>

View File

@ -8,6 +8,7 @@
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/AppStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View File

@ -11,7 +11,13 @@
ContentTemplate="{DataTemplate pages:PostsPage}" />
<ShellContent
Title="Сотрудники"
ContentTemplate="{DataTemplate pages:UsersPage}" />
ContentTemplate="{DataTemplate pages:EmployeesPage}" />
<ShellContent
Title="Отделы"
ContentTemplate="{DataTemplate pages:DepartmentsPage}" />
<ShellContent
Title="Заявления"
ContentTemplate="{DataTemplate pages:RequestsPage}" />
<ShellContent
Title="Профиль"
ContentTemplate="{DataTemplate pages:ProfilePage}" />

View File

@ -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 ===");

View File

@ -33,7 +33,7 @@ public static class MauiProgram
// Регистрируем страницы
builder.Services.AddTransient<LoginPage>();
builder.Services.AddTransient<UsersPage>();
builder.Services.AddTransient<EmployeesPage>();
builder.Services.AddTransient<PostsPage>();
builder.Services.AddTransient<ChatPage>();
builder.Services.AddTransient<ProfilePage>();

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace CORP_MAUI.Models;
/// <summary>
@ -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; }
/// <summary>
/// Описание для отображения (с fallback если пусто)
/// </summary>
[JsonIgnore]
public string DisplayDescription =>
string.IsNullOrWhiteSpace(Description)
? "Описание отсутствует"
: Description;
}

View File

@ -1,17 +1,284 @@
using System.Text.Json.Serialization;
namespace CORP_MAUI.Models;
/// <summary>
/// Модель запроса (отпуск, заявка и т.д.)
/// Модель заявления (отпуск, заявка и т.д.)
/// </summary>
public class Request
{
public int Id { get; set; }
/// <summary>
/// Тип заявления (vacation, sick_leave, business_trip и т.д.)
/// </summary>
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
/// <summary>
/// Заголовок заявления
/// </summary>
[JsonPropertyName("title")]
public string? Title { get; set; }
/// <summary>
/// Отображаемый заголовок
/// </summary>
[JsonPropertyName("display_title")]
public string? DisplayTitle { get; set; }
/// <summary>
/// Статус заявления (pending, approved, rejected и т.д.)
/// </summary>
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
/// <summary>
/// Дата создания заявления
/// </summary>
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public User? Requester { get; set; }
/// <summary>
/// Дата последнего обновления
/// </summary>
[JsonPropertyName("updated_at")]
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// Дата принятия решения
/// </summary>
[JsonPropertyName("decided_at")]
public DateTime? DecidedAt { get; set; }
/// <summary>
/// Дата начала (для отпуска, командировки и т.д.)
/// </summary>
[JsonPropertyName("date_from")]
public DateTime? DateFrom { get; set; }
/// <summary>
/// Дата окончания
/// </summary>
[JsonPropertyName("date_to")]
public DateTime? DateTo { get; set; }
/// <summary>
/// Сотрудник, подавший заявление
/// </summary>
[JsonPropertyName("employee")]
public User? Employee { get; set; }
/// <summary>
/// Сотрудник, принявший решение
/// </summary>
[JsonPropertyName("approver")]
public User? Approver { get; set; }
public string Description { get; set; } = string.Empty;
/// <summary>
/// Комментарий/описание к заявлению
/// </summary>
[JsonPropertyName("comment")]
public string? Comment { get; set; }
/// <summary>
/// Вложение (файл)
/// </summary>
[JsonPropertyName("attachment")]
public string? Attachment { get; set; }
/// <summary>
/// URL вложения
/// </summary>
[JsonPropertyName("attachment_url")]
public string? AttachmentUrl { get; set; }
/// <summary>
/// Отдел
/// </summary>
[JsonPropertyName("department")]
public Department? Department { get; set; }
/// <summary>
/// ID отделов
/// </summary>
[JsonPropertyName("departments")]
public List<int>? Departments { get; set; }
/// <summary>
/// Является ли решение финальным
/// </summary>
[JsonPropertyName("is_final")]
public bool IsFinal { get; set; }
/// <summary>
/// Является ли текущий пользователь получателем
/// </summary>
[JsonPropertyName("is_recipient")]
public bool IsRecipient { get; set; }
/// <summary>
/// Отправлено всему отделу
/// </summary>
[JsonPropertyName("sent_to_all_department")]
public bool SentToAllDepartment { get; set; }
/// <summary>
/// Количество получателей
/// </summary>
[JsonPropertyName("recipient_count")]
public int RecipientCount { get; set; }
/// <summary>
/// Количество в копии
/// </summary>
[JsonPropertyName("cc_count")]
public int CcCount { get; set; }
/// <summary>
/// Количество комментариев
/// </summary>
[JsonPropertyName("comments_count")]
public int CommentsCount { get; set; }
/// <summary>
/// Получатели заявления
/// </summary>
[JsonPropertyName("recipients")]
public List<User>? Recipients { get; set; }
/// <summary>
/// Пользователи в копии
/// </summary>
[JsonPropertyName("cc_users")]
public List<User>? CcUsers { get; set; }
// ================================
// Вспомогательные свойства для UI
// ================================
/// <summary>
/// Отображаемый тип/заголовок заявления
/// </summary>
[JsonIgnore]
public string DisplayType =>
!string.IsNullOrEmpty(DisplayTitle) ? DisplayTitle :
!string.IsNullOrEmpty(Title) ? Title :
GetTypeDisplayName(Type);
/// <summary>
/// Отображаемый статус
/// </summary>
[JsonIgnore]
public string DisplayStatus => GetStatusDisplayName(Status);
/// <summary>
/// Цвет статуса для UI
/// </summary>
[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") // Серый
};
/// <summary>
/// Иконка типа заявления
/// </summary>
[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" => "📝",
_ => "📋"
};
/// <summary>
/// Отображаемый период (для заявлений с датами)
/// </summary>
[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;
}
}
/// <summary>
/// Отображаемая дата создания
/// </summary>
[JsonIgnore]
public string DisplayCreatedAt => CreatedAt.ToString("dd.MM.yyyy HH:mm");
/// <summary>
/// Отображаемое описание (из поля comment)
/// </summary>
[JsonIgnore]
public string DisplayDescription =>
!string.IsNullOrEmpty(Comment) ? Comment :
"Без описания";
/// <summary>
/// Имя сотрудника, подавшего заявление
/// </summary>
[JsonIgnore]
public string EmployeeName => Employee?.FullName ?? "Неизвестный сотрудник";
/// <summary>
/// Имя сотрудника, принявшего решение
/// </summary>
[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 ?? "Неизвестно"
};
}

View File

@ -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<Department>? 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
}
}
/// <summary>
/// Универсальное свойство для отображения аватара (поддерживает Base64 и URL)
/// </summary>
[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
}
}
}

View File

@ -14,13 +14,28 @@
BackgroundColor="{StaticResource Primary}"
Shadow="{Shadow Brush=Black, Opacity=0.3, Offset='0,2', Radius=4}">
<HorizontalStackLayout Spacing="10">
<Grid ColumnDefinitions="Auto,Auto,*">
<!-- Кнопка "Назад" -->
<Button
Grid.Column="0"
Text="←"
FontSize="24"
TextColor="White"
BackgroundColor="Transparent"
WidthRequest="40"
HeightRequest="40"
Padding="0"
VerticalOptions="Center"
Clicked="OnBackButtonClicked"/>
<!-- Аватар -->
<Border
Grid.Column="1"
WidthRequest="50"
HeightRequest="50"
StrokeShape="RoundRectangle 25"
Padding="0"
Margin="10,0,0,0"
BackgroundColor="White">
<Image
x:Name="RecipientAvatar"
@ -28,7 +43,11 @@
Aspect="AspectFill"/>
</Border>
<VerticalStackLayout VerticalOptions="Center">
<!-- Информация о собеседнике -->
<VerticalStackLayout
Grid.Column="2"
VerticalOptions="Center"
Margin="10,0,0,0">
<Label
x:Name="RecipientName"
Text="Выберите собеседника"
@ -42,7 +61,7 @@
TextColor="White"
Opacity="0.8"/>
</VerticalStackLayout>
</HorizontalStackLayout>
</Grid>
</Border>
<!-- Список сообщений -->

View File

@ -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; }
}

View File

@ -0,0 +1,207 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:CORP_MAUI.Models"
x:Class="CORP_MAUI.Pages.DepartmentDetailsPage"
Title="Информация об отделе">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsVisible="True" TextOverride="Назад" />
</Shell.BackButtonBehavior>
<ScrollView>
<VerticalStackLayout Padding="15" Spacing="20">
<!-- Информация об отделе -->
<Border Style="{StaticResource CardBorderLarge}">
<VerticalStackLayout Spacing="10">
<!-- Название отдела -->
<Label
x:Name="DepartmentNameLabel"
Style="{StaticResource PageTitle}"/>
<!-- Описание -->
<Label
x:Name="DepartmentDescriptionLabel"
Style="{StaticResource CardText}"
FontSize="16"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
Margin="0,0,0,10"/>
<!-- Количество сотрудников -->
<HorizontalStackLayout
HorizontalOptions="Center"
Spacing="5">
<Label Text="👥" FontSize="18"/>
<Label
x:Name="EmployeesCountLabel"
Style="{StaticResource MetaText}"
FontSize="16"/>
</HorizontalStackLayout>
</VerticalStackLayout>
</Border>
<!-- Руководитель отдела -->
<VerticalStackLayout
x:Name="HeadSection"
Spacing="10"
IsVisible="False">
<Label Text="Руководитель" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource SectionBorder}">
<Grid ColumnDefinitions="60,*">
<!-- Аватар руководителя -->
<Border Grid.Column="0" Style="{StaticResource AvatarMedium}">
<Image x:Name="HeadAvatarImage" Aspect="AspectFill"/>
</Border>
<!-- Информация о руководителе -->
<VerticalStackLayout
Grid.Column="1"
VerticalOptions="Center"
Spacing="3"
Margin="10,0">
<Label
x:Name="HeadNameLabel"
Style="{StaticResource CardSubtitle}"/>
<Label
x:Name="HeadPositionLabel"
Style="{StaticResource CardText}"
FontSize="13"/>
</VerticalStackLayout>
</Grid>
</Border>
</VerticalStackLayout>
<!-- Команда отдела -->
<VerticalStackLayout Spacing="10">
<Label Text="Команда отдела" Style="{StaticResource SectionTitle}"/>
<CollectionView
x:Name="TeamCollectionView"
SelectionMode="Single"
SelectionChanged="OnTeamMemberSelected">
<CollectionView.EmptyView>
<VerticalStackLayout Style="{StaticResource EmptyStateContainer}">
<Label
Text="Нет сотрудников в отделе"
Style="{StaticResource EmptyStateTitle}"/>
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:User">
<Border Style="{StaticResource CardBorder}" Margin="0,5">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnTeamMemberCardTapped" CommandParameter="{Binding .}"/>
</Border.GestureRecognizers>
<Grid ColumnDefinitions="60,*,50">
<!-- Аватар -->
<Border Grid.Column="0" Style="{StaticResource AvatarMedium}">
<Grid>
<Image
Source="{Binding DisplayAvatarSource}"
Aspect="AspectFill"/>
<!-- Индикатор онлайн статуса -->
<Border
Style="{StaticResource OnlineIndicatorMedium}"
IsVisible="{Binding IsOnline}"/>
</Grid>
</Border>
<!-- Информация о сотруднике -->
<VerticalStackLayout
Grid.Column="1"
VerticalOptions="Center"
Spacing="5"
Margin="10,0">
<Label
Text="{Binding FullName}"
Style="{StaticResource CardSubtitle}"/>
<Label
Text="{Binding PositionName}"
Style="{StaticResource CardText}"
FontSize="13"/>
<!-- Навыки -->
<FlexLayout
BindableLayout.ItemsSource="{Binding Skills}"
Wrap="Wrap"
JustifyContent="Start"
AlignItems="Start"
Margin="0,3,0,0">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="models:Skill">
<Border
Padding="6,3"
Margin="2"
StrokeShape="RoundRectangle 10"
BackgroundColor="{StaticResource Gray200}"
Stroke="Transparent">
<Label
Text="{Binding Name}"
FontSize="11"
TextColor="{StaticResource Gray700}"/>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</VerticalStackLayout>
<!-- Кнопка чата -->
<Border
Grid.Column="2"
Style="{StaticResource IconButton}"
VerticalOptions="Center">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChatIconTapped" CommandParameter="{Binding .}"/>
</Border.GestureRecognizers>
<Label
Text="💬"
FontSize="20"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</Border>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
<!-- Индикатор загрузки -->
<ActivityIndicator
x:Name="LoadingIndicator"
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
VerticalOptions="Center"
HorizontalOptions="Center"
Margin="0,50,0,0"/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@ -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<User> _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<User>();
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));
}
}
/// <summary>
/// Обработчик клика по карточке сотрудника (открытие профиля)
/// </summary>
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");
}
}
}
/// <summary>
/// Обработчик клика по иконке чата (открытие чата)
/// </summary>
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");
}
}
}
}

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:CORP_MAUI.Models"
x:Class="CORP_MAUI.Pages.DepartmentsPage"
Title="Отделы">
<Grid RowDefinitions="Auto,*">
<!-- Поиск -->
<Border
Grid.Row="0"
Style="{StaticResource SearchContainerBorder}">
<SearchBar
x:Name="SearchBar"
Placeholder="Поиск отделов..."
Style="{StaticResource AppSearchBar}"
TextChanged="OnSearchTextChanged"/>
</Border>
<!-- Список отделов -->
<RefreshView
Grid.Row="1"
x:Name="RefreshView"
IsRefreshing="False"
Refreshing="OnRefreshing">
<CollectionView
x:Name="DepartmentsCollectionView"
SelectionMode="Single"
SelectionChanged="OnDepartmentSelected">
<CollectionView.EmptyView>
<VerticalStackLayout Style="{StaticResource EmptyStateContainer}">
<Label
Text="Нет доступных отделов"
Style="{StaticResource EmptyStateTitle}"/>
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Department">
<Border Style="{StaticResource CardBorder}">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto">
<!-- Название отдела -->
<Label
Grid.Column="0"
Grid.Row="0"
Text="{Binding Name}"
Style="{StaticResource CardTitle}"/>
<!-- Описание -->
<Label
Grid.Column="0"
Grid.Row="1"
Text="{Binding DisplayDescription}"
Style="{StaticResource CardText}"
Margin="0,5,0,0"
LineBreakMode="TailTruncation"
MaxLines="2"/>
<!-- Информация -->
<HorizontalStackLayout
Grid.Column="0"
Grid.Row="2"
Spacing="15"
Margin="0,8,0,0">
<!-- Количество сотрудников -->
<HorizontalStackLayout Spacing="5">
<Label Text="👥" Style="{StaticResource MetaIcon}"/>
<Label Text="{Binding EmployeesCount}" Style="{StaticResource MetaText}"/>
</HorizontalStackLayout>
<!-- Руководитель -->
<HorizontalStackLayout Spacing="5">
<Label Text="👤" Style="{StaticResource MetaIcon}"/>
<Label
Text="{Binding Head.FullName}"
Style="{StaticResource MetaText}"
LineBreakMode="TailTruncation"/>
</HorizontalStackLayout>
</HorizontalStackLayout>
<!-- Стрелка -->
<Label
Grid.Column="1"
Grid.Row="0"
Grid.RowSpan="3"
Text="›"
FontSize="30"
TextColor="{StaticResource Gray400}"
VerticalOptions="Center"/>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
<!-- Индикатор загрузки -->
<ActivityIndicator
Grid.Row="1"
x:Name="LoadingIndicator"
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
VerticalOptions="Center"
HorizontalOptions="Center"/>
</Grid>
</ContentPage>

View File

@ -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<Department> _departments;
private List<Department> _allDepartments = new();
public DepartmentsPage()
{
try
{
Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR START ===");
Trace.WriteLine("[DepartmentsPage] >>> Вызов InitializeComponent");
InitializeComponent();
Trace.WriteLine("[DepartmentsPage] >>> InitializeComponent завершен");
_departments = new ObservableCollection<Department>();
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<Department> 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);
}
}
}
}

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:CORP_MAUI.Models"
x:Class="CORP_MAUI.Pages.EmployeeProfilePage"
Title="Профиль сотрудника">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsVisible="True" TextOverride="Назад" />
</Shell.BackButtonBehavior>
<ScrollView>
<VerticalStackLayout Padding="15" Spacing="20">
<!-- Аватар и основная информация -->
<Border Style="{StaticResource CardBorderLarge}">
<VerticalStackLayout Spacing="15" HorizontalOptions="Center">
<!-- Аватар -->
<Border Style="{StaticResource AvatarXLarge}">
<Grid>
<Image
x:Name="AvatarImage"
Aspect="AspectFill"/>
<!-- Индикатор онлайн статуса -->
<Border
Style="{StaticResource OnlineIndicatorLarge}"
x:Name="OnlineIndicator"
IsVisible="False"/>
</Grid>
</Border>
<!-- ФИО -->
<Label
x:Name="FullNameLabel"
Style="{StaticResource PageTitle}"/>
<!-- Должность -->
<Label
x:Name="PositionLabel"
Style="{StaticResource CardText}"
FontSize="16"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"/>
<!-- Кнопка "Написать сообщение" -->
<Button
x:Name="ChatButton"
Text="💬 Написать сообщение"
Style="{StaticResource PrimaryButton}"
Margin="0,10,0,0"
Clicked="OnChatButtonClicked"/>
</VerticalStackLayout>
</Border>
<!-- Контактная информация -->
<VerticalStackLayout Spacing="10">
<Label Text="Контакты" Style="{StaticResource SectionTitle}"/>
<!-- Email -->
<Border
Style="{StaticResource SectionBorder}"
x:Name="EmailSection"
IsVisible="False">
<HorizontalStackLayout Spacing="10">
<Label Text="📧" FontSize="20" VerticalOptions="Center"/>
<VerticalStackLayout Spacing="3">
<Label Text="Email" Style="{StaticResource CardMeta}"/>
<Label x:Name="EmailLabel" Style="{StaticResource CardText}" TextColor="{StaticResource Gray900}"/>
</VerticalStackLayout>
</HorizontalStackLayout>
</Border>
<!-- Телефон -->
<Border
Style="{StaticResource SectionBorder}"
x:Name="PhoneSection"
IsVisible="False">
<HorizontalStackLayout Spacing="10">
<Label Text="📱" FontSize="20" VerticalOptions="Center"/>
<VerticalStackLayout Spacing="3">
<Label Text="Телефон" Style="{StaticResource CardMeta}"/>
<Label x:Name="PhoneLabel" Style="{StaticResource CardText}" TextColor="{StaticResource Gray900}"/>
</VerticalStackLayout>
</HorizontalStackLayout>
</Border>
</VerticalStackLayout>
<!-- Отделы -->
<VerticalStackLayout
Spacing="10"
x:Name="DepartmentsSection"
IsVisible="False">
<Label Text="Отделы" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource SectionBorder}">
<VerticalStackLayout
x:Name="DepartmentsContainer"
Spacing="5"/>
</Border>
</VerticalStackLayout>
<!-- Навыки -->
<VerticalStackLayout
Spacing="10"
x:Name="SkillsSection"
IsVisible="False">
<Label Text="Навыки" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource SectionBorder}">
<FlexLayout
x:Name="SkillsContainer"
Wrap="Wrap"
JustifyContent="Start"
AlignItems="Start"/>
</Border>
</VerticalStackLayout>
<!-- Индикатор загрузки -->
<ActivityIndicator
x:Name="LoadingIndicator"
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
VerticalOptions="Center"
HorizontalOptions="Center"
Margin="0,50,0,0"/>
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@ -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;
}
}
/// <summary>
/// Конструктор для передачи готового объекта пользователя
/// </summary>
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");
}
}
}

View File

@ -2,7 +2,7 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:CORP_MAUI.Models"
x:Class="CORP_MAUI.Pages.UsersPage"
x:Class="CORP_MAUI.Pages.EmployeesPage"
Title="Сотрудники">
<Grid RowDefinitions="Auto,*">
@ -10,14 +10,11 @@
<!-- Поиск -->
<Border
Grid.Row="0"
Padding="15"
BackgroundColor="{StaticResource Gray100}">
Style="{StaticResource SearchContainerBorder}">
<SearchBar
x:Name="SearchBar"
Placeholder="Поиск сотрудников..."
PlaceholderColor="{StaticResource Gray400}"
TextColor="{StaticResource Gray900}"
BackgroundColor="White"
Style="{StaticResource AppSearchBar}"
TextChanged="OnSearchTextChanged"/>
</Border>
@ -29,58 +26,39 @@
Refreshing="OnRefreshing">
<CollectionView
x:Name="UsersCollectionView"
x:Name="EmployeesCollectionView"
SelectionMode="Single"
SelectionChanged="OnUserSelected">
SelectionChanged="OnEmployeeSelected">
<CollectionView.EmptyView>
<VerticalStackLayout
VerticalOptions="Center"
HorizontalOptions="Center"
Padding="50">
<VerticalStackLayout Style="{StaticResource EmptyStateContainer}">
<Label
Text="Нет доступных сотрудников"
FontSize="16"
TextColor="{StaticResource Gray600}"
HorizontalOptions="Center"/>
Style="{StaticResource EmptyStateTitle}"/>
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:User">
<Border
Padding="15"
Margin="10,5"
StrokeShape="RoundRectangle 10"
BackgroundColor="White"
Shadow="{Shadow Brush=Black, Opacity=0.2, Offset='0,2', Radius=4}">
<Border Style="{StaticResource CardBorder}">
<Grid ColumnDefinitions="60,*,Auto">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnEmployeeCardTapped" CommandParameter="{Binding .}"/>
</Border.GestureRecognizers>
<Grid ColumnDefinitions="60,*,50">
<!-- Аватар -->
<Border
Grid.Column="0"
WidthRequest="50"
HeightRequest="50"
StrokeShape="RoundRectangle 25"
Padding="0"
BackgroundColor="{StaticResource Gray200}">
Style="{StaticResource AvatarMedium}">
<Grid>
<Image
Source="{Binding AvatarUrl}"
Source="{Binding DisplayAvatarSource}"
Aspect="AspectFill"/>
<!-- Индикатор онлайн статуса -->
<Border
WidthRequest="15"
HeightRequest="15"
StrokeShape="RoundRectangle 7.5"
Padding="0"
BackgroundColor="LimeGreen"
Stroke="White"
StrokeThickness="2"
VerticalOptions="End"
HorizontalOptions="End"
Margin="0,0,2,2"
Style="{StaticResource OnlineIndicatorMedium}"
IsVisible="{Binding IsOnline}"/>
</Grid>
</Border>
@ -94,31 +72,36 @@
<Label
Text="{Binding FullName}"
FontSize="16"
FontAttributes="Bold"
TextColor="{StaticResource Gray900}"/>
Style="{StaticResource CardSubtitle}"/>
<Label
Text="{Binding PositionName}"
FontSize="13"
TextColor="{StaticResource Gray600}"/>
Style="{StaticResource CardText}"
FontSize="13"/>
<Label
Text="{Binding DepartmentName}"
FontSize="12"
TextColor="{StaticResource Gray500}"/>
Style="{StaticResource CardMeta}"/>
</VerticalStackLayout>
<!-- Стрелка -->
<Label
<!-- Кнопка чата -->
<Border
Grid.Column="2"
Text="›"
FontSize="30"
TextColor="{StaticResource Gray400}"
VerticalOptions="Center"/>
Style="{StaticResource IconButton}"
VerticalOptions="Center">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChatIconTapped" CommandParameter="{Binding .}"/>
</Border.GestureRecognizers>
<Label
Text="💬"
FontSize="20"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</Border>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>

View File

@ -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<User> _employees;
private List<User> _allEmployees = new();
public EmployeesPage()
{
try
{
Trace.WriteLine("=== EMPLOYEESPAGE CONSTRUCTOR START ===");
Trace.WriteLine("[EmployeesPage] >>> Вызов InitializeComponent");
InitializeComponent();
Trace.WriteLine("[EmployeesPage] >>> InitializeComponent завершен");
_employees = new ObservableCollection<User>();
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));
}
}
/// <summary>
/// Обработчик клика по карточке сотрудника (открытие профиля)
/// </summary>
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");
}
}
}
/// <summary>
/// Обработчик клика по иконке чата (открытие чата)
/// </summary>
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<User> 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;
}
}

View File

@ -39,11 +39,7 @@
HeightRequest="250"
BackgroundColor="#F5F5F5"
Source="{Binding PostViewModel.DisplayImageSource}"
IsVisible="{Binding PostViewModel.IsImageVisible}">
<Image.Clip>
<RoundRectangleGeometry CornerRadius="10"/>
</Image.Clip>
</Image>
IsVisible="{Binding PostViewModel.IsImageVisible}"/>
<!-- Полный текст (Content) -->
<!-- Используем Post.Content напрямую, так как ViewModel.PreviewContent обрезан -->

View File

@ -77,11 +77,7 @@
Margin="0,5"
BackgroundColor="#F5F5F5"
Source="{Binding DisplayImageSource}"
IsVisible="{Binding IsImageVisible}">
<Image.Clip>
<RoundRectangleGeometry CornerRadius="10"/>
</Image.Clip>
</Image>
IsVisible="{Binding IsImageVisible}"/>
<!-- Контент -->
<Label Text="{Binding PreviewContent}"

View File

@ -57,16 +57,39 @@ public partial class PostsPage
{
PostViewModel vm = new(post);
// Логируем информацию об изображении
if (!string.IsNullOrEmpty(post.Image))
{
string imagePreview = post.Image.Length > 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. Если нет в кэше - загружаем

View File

@ -26,7 +26,7 @@
<Frame
WidthRequest="100"
HeightRequest="100"
CornerRadius="50"
CornerRadius="25"
Padding="0"
IsClippedToBounds="True"
HasShadow="False"

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:models="clr-namespace:CORP_MAUI.Models"
x:Class="CORP_MAUI.Pages.RequestsPage"
Title="Заявления">
<Grid RowDefinitions="Auto,Auto,*">
<!-- Поиск -->
<Border
Grid.Row="0"
Style="{StaticResource SearchContainerBorder}">
<SearchBar
x:Name="SearchBar"
Placeholder="Поиск заявлений..."
Style="{StaticResource AppSearchBar}"
TextChanged="OnSearchTextChanged"/>
</Border>
<!-- Фильтры по статусу -->
<ScrollView
Grid.Row="1"
Orientation="Horizontal"
HorizontalScrollBarVisibility="Never">
<HorizontalStackLayout
Padding="15,10"
Spacing="10"
x:Name="FilterButtons">
<Border
x:Name="FilterAll"
Style="{StaticResource ChipButtonActive}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterAllTapped"/>
</Border.GestureRecognizers>
<Label Text="Все" FontSize="14" TextColor="White"/>
</Border>
<Border
x:Name="FilterPending"
Style="{StaticResource ChipButton}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterPendingTapped"/>
</Border.GestureRecognizers>
<Label Text="⏳ На рассмотрении" FontSize="14" TextColor="{StaticResource Gray700}"/>
</Border>
<Border
x:Name="FilterApproved"
Style="{StaticResource ChipButton}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterApprovedTapped"/>
</Border.GestureRecognizers>
<Label Text="✅ Одобрено" FontSize="14" TextColor="{StaticResource Gray700}"/>
</Border>
<Border
x:Name="FilterRejected"
Style="{StaticResource ChipButton}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterRejectedTapped"/>
</Border.GestureRecognizers>
<Label Text="❌ Отклонено" FontSize="14" TextColor="{StaticResource Gray700}"/>
</Border>
</HorizontalStackLayout>
</ScrollView>
<!-- Список заявлений -->
<RefreshView
Grid.Row="2"
x:Name="RefreshView"
IsRefreshing="False"
Refreshing="OnRefreshing">
<CollectionView
x:Name="RequestsCollectionView"
SelectionMode="Single"
SelectionChanged="OnRequestSelected">
<CollectionView.EmptyView>
<VerticalStackLayout Style="{StaticResource EmptyStateContainer}">
<Label Text="📋" Style="{StaticResource EmptyStateIcon}"/>
<Label
Text="Нет заявлений"
Style="{StaticResource EmptyStateTitle}"
Margin="0,10,0,0"/>
<Label
Text="Здесь будут отображаться ваши заявления"
Style="{StaticResource EmptyStateText}"/>
</VerticalStackLayout>
</CollectionView.EmptyView>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="models:Request">
<Border Style="{StaticResource CardBorder}" StrokeShape="RoundRectangle 12">
<Grid ColumnDefinitions="50,*,Auto" RowDefinitions="Auto,Auto,Auto">
<!-- Иконка типа -->
<Border
Grid.Column="0"
Grid.RowSpan="3"
Style="{StaticResource IconContainer}"
VerticalOptions="Start"
Margin="0,0,10,0">
<Label Text="{Binding TypeIcon}" Style="{StaticResource IconContainerText}"/>
</Border>
<!-- Тип заявления -->
<Label
Grid.Column="1"
Grid.Row="0"
Text="{Binding DisplayType}"
Style="{StaticResource CardSubtitle}"/>
<!-- Статус (бейдж) -->
<Border
Grid.Column="2"
Grid.Row="0"
Style="{StaticResource Badge}"
BackgroundColor="{Binding StatusColor}"
VerticalOptions="Center">
<Label Text="{Binding DisplayStatus}" Style="{StaticResource BadgeText}"/>
</Border>
<!-- Период / Описание -->
<Label
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="1"
Text="{Binding DisplayDescription}"
Style="{StaticResource CardText}"
Margin="0,5,0,0"
LineBreakMode="TailTruncation"
MaxLines="2"/>
<!-- Нижняя строка: дата и период -->
<HorizontalStackLayout
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="2"
Spacing="15"
Margin="0,8,0,0">
<!-- Дата создания -->
<HorizontalStackLayout Spacing="5">
<Label Text="📅" Style="{StaticResource CardMeta}"/>
<Label Text="{Binding DisplayCreatedAt}" Style="{StaticResource CardMeta}"/>
</HorizontalStackLayout>
<!-- Период (если есть) -->
<HorizontalStackLayout Spacing="5">
<Label Text="⏰" Style="{StaticResource CardMeta}"/>
<Label Text="{Binding DisplayPeriod}" Style="{StaticResource CardMeta}"/>
</HorizontalStackLayout>
<!-- Сотрудник -->
<HorizontalStackLayout Spacing="5">
<Label Text="👤" Style="{StaticResource CardMeta}"/>
<Label
Text="{Binding EmployeeName}"
Style="{StaticResource CardMeta}"
LineBreakMode="TailTruncation"
MaxLines="1"/>
</HorizontalStackLayout>
</HorizontalStackLayout>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
<!-- Индикатор загрузки -->
<ActivityIndicator
Grid.Row="2"
x:Name="LoadingIndicator"
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
VerticalOptions="Center"
HorizontalOptions="Center"/>
</Grid>
</ContentPage>

View File

@ -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<Request> _requests;
private List<Request> _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<Request>();
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<Request> 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;
}
}

View File

@ -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<User> _users;
private List<User> _allUsers = new();
public UsersPage()
{
try
{
Trace.WriteLine("=== USERSPAGE CONSTRUCTOR START ===");
Trace.WriteLine("[UsersPage] >>> Вызов InitializeComponent");
InitializeComponent();
Trace.WriteLine("[UsersPage] >>> InitializeComponent завершен");
_users = new ObservableCollection<User>();
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<User> 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;
}
}

View File

@ -0,0 +1,360 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- ================================================
CORP-MAUI Custom Application Styles
Кастомные стили приложения
================================================ -->
<!-- ================================
КАРТОЧКИ (Cards)
================================ -->
<!-- Базовая карточка для списков -->
<Style x:Key="CardBorder" TargetType="Border">
<Setter Property="Padding" Value="15"/>
<Setter Property="Margin" Value="10,5"/>
<Setter Property="StrokeShape" Value="RoundRectangle 10"/>
<Setter Property="BackgroundColor" Value="White"/>
<Setter Property="Stroke" Value="Transparent"/>
<Setter Property="Shadow">
<Setter.Value>
<Shadow Brush="Black" Opacity="0.15" Offset="0,2" Radius="4"/>
</Setter.Value>
</Setter>
</Style>
<!-- Карточка с увеличенным радиусом -->
<Style x:Key="CardBorderLarge" TargetType="Border" BasedOn="{StaticResource CardBorder}">
<Setter Property="StrokeShape" Value="RoundRectangle 15"/>
<Setter Property="Padding" Value="20"/>
</Style>
<!-- Секция/блок внутри страницы -->
<Style x:Key="SectionBorder" TargetType="Border">
<Setter Property="Padding" Value="15"/>
<Setter Property="StrokeShape" Value="RoundRectangle 10"/>
<Setter Property="BackgroundColor" Value="White"/>
<Setter Property="Stroke" Value="Transparent"/>
<Setter Property="Shadow">
<Setter.Value>
<Shadow Brush="Black" Opacity="0.2" Offset="0,2" Radius="4"/>
</Setter.Value>
</Setter>
</Style>
<!-- ================================
ПАНЕЛЬ ПОИСКА (Search)
================================ -->
<!-- Контейнер поиска -->
<Style x:Key="SearchContainerBorder" TargetType="Border">
<Setter Property="Padding" Value="15"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray100}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- SearchBar внутри контейнера -->
<Style x:Key="AppSearchBar" TargetType="SearchBar">
<Setter Property="PlaceholderColor" Value="{StaticResource Gray400}"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
<Setter Property="BackgroundColor" Value="White"/>
</Style>
<!-- ================================
АВАТАРЫ (Avatars)
================================ -->
<!-- Маленький аватар (40x40) -->
<Style x:Key="AvatarSmall" TargetType="Border">
<Setter Property="WidthRequest" Value="40"/>
<Setter Property="HeightRequest" Value="40"/>
<Setter Property="StrokeShape" Value="RoundRectangle 20"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray200}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Средний аватар (50x50) -->
<Style x:Key="AvatarMedium" TargetType="Border">
<Setter Property="WidthRequest" Value="50"/>
<Setter Property="HeightRequest" Value="50"/>
<Setter Property="StrokeShape" Value="RoundRectangle 25"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray200}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Большой аватар (80x80) -->
<Style x:Key="AvatarLarge" TargetType="Border">
<Setter Property="WidthRequest" Value="80"/>
<Setter Property="HeightRequest" Value="80"/>
<Setter Property="StrokeShape" Value="RoundRectangle 40"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray200}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Очень большой аватар для профиля (120x120) -->
<Style x:Key="AvatarXLarge" TargetType="Border">
<Setter Property="WidthRequest" Value="120"/>
<Setter Property="HeightRequest" Value="120"/>
<Setter Property="StrokeShape" Value="RoundRectangle 60"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray200}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Индикатор онлайн статуса (маленький) -->
<Style x:Key="OnlineIndicatorSmall" TargetType="Border">
<Setter Property="WidthRequest" Value="12"/>
<Setter Property="HeightRequest" Value="12"/>
<Setter Property="StrokeShape" Value="RoundRectangle 6"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="LimeGreen"/>
<Setter Property="Stroke" Value="White"/>
<Setter Property="StrokeThickness" Value="2"/>
<Setter Property="VerticalOptions" Value="End"/>
<Setter Property="HorizontalOptions" Value="End"/>
</Style>
<!-- Индикатор онлайн статуса (средний) -->
<Style x:Key="OnlineIndicatorMedium" TargetType="Border">
<Setter Property="WidthRequest" Value="15"/>
<Setter Property="HeightRequest" Value="15"/>
<Setter Property="StrokeShape" Value="RoundRectangle 7.5"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="LimeGreen"/>
<Setter Property="Stroke" Value="White"/>
<Setter Property="StrokeThickness" Value="2"/>
<Setter Property="VerticalOptions" Value="End"/>
<Setter Property="HorizontalOptions" Value="End"/>
<Setter Property="Margin" Value="0,0,2,2"/>
</Style>
<!-- Индикатор онлайн статуса (большой) -->
<Style x:Key="OnlineIndicatorLarge" TargetType="Border">
<Setter Property="WidthRequest" Value="25"/>
<Setter Property="HeightRequest" Value="25"/>
<Setter Property="StrokeShape" Value="RoundRectangle 12.5"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="LimeGreen"/>
<Setter Property="Stroke" Value="White"/>
<Setter Property="StrokeThickness" Value="3"/>
<Setter Property="VerticalOptions" Value="End"/>
<Setter Property="HorizontalOptions" Value="End"/>
<Setter Property="Margin" Value="0,0,5,5"/>
</Style>
<!-- ================================
КНОПКИ (Buttons)
================================ -->
<!-- Круглая кнопка (иконка) -->
<Style x:Key="IconButton" TargetType="Border">
<Setter Property="WidthRequest" Value="40"/>
<Setter Property="HeightRequest" Value="40"/>
<Setter Property="StrokeShape" Value="RoundRectangle 20"/>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Кнопка-чип (для фильтров) -->
<Style x:Key="ChipButton" TargetType="Border">
<Setter Property="Padding" Value="12,8"/>
<Setter Property="StrokeShape" Value="RoundRectangle 20"/>
<Setter Property="BackgroundColor" Value="White"/>
<Setter Property="Stroke" Value="{StaticResource Gray300}"/>
</Style>
<!-- Активный чип -->
<Style x:Key="ChipButtonActive" TargetType="Border">
<Setter Property="Padding" Value="12,8"/>
<Setter Property="StrokeShape" Value="RoundRectangle 20"/>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Основная кнопка -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource Primary}"/>
<Setter Property="TextColor" Value="White"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="HeightRequest" Value="50"/>
</Style>
<!-- Вторичная кнопка -->
<Style x:Key="SecondaryButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource Gray200}"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="HeightRequest" Value="50"/>
</Style>
<!-- ================================
ТЕКСТ (Typography)
================================ -->
<!-- Заголовок карточки -->
<Style x:Key="CardTitle" TargetType="Label">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
</Style>
<!-- Подзаголовок карточки -->
<Style x:Key="CardSubtitle" TargetType="Label">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
</Style>
<!-- Обычный текст в карточке -->
<Style x:Key="CardText" TargetType="Label">
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextColor" Value="{StaticResource Gray600}"/>
</Style>
<!-- Мелкий текст (мета-информация) -->
<Style x:Key="CardMeta" TargetType="Label">
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextColor" Value="{StaticResource Gray500}"/>
</Style>
<!-- Заголовок секции на странице -->
<Style x:Key="SectionTitle" TargetType="Label">
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
</Style>
<!-- Заголовок страницы (большой) -->
<Style x:Key="PageTitle" TargetType="Label">
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="{StaticResource Gray900}"/>
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
</Style>
<!-- Текст для пустых состояний -->
<Style x:Key="EmptyStateTitle" TargetType="Label">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontAttributes" Value="Bold"/>
<Setter Property="TextColor" Value="{StaticResource Gray600}"/>
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
</Style>
<Style x:Key="EmptyStateText" TargetType="Label">
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextColor" Value="{StaticResource Gray500}"/>
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="HorizontalTextAlignment" Value="Center"/>
</Style>
<!-- Иконка для пустого состояния -->
<Style x:Key="EmptyStateIcon" TargetType="Label">
<Setter Property="FontSize" Value="60"/>
<Setter Property="HorizontalOptions" Value="Center"/>
</Style>
<!-- ================================
БЕЙДЖИ/ТЕГИ (Badges)
================================ -->
<!-- Базовый бейдж -->
<Style x:Key="Badge" TargetType="Border">
<Setter Property="Padding" Value="10,4"/>
<Setter Property="StrokeShape" Value="RoundRectangle 12"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<!-- Бейдж успеха (зелёный) -->
<Style x:Key="BadgeSuccess" TargetType="Border" BasedOn="{StaticResource Badge}">
<Setter Property="BackgroundColor" Value="#22C55E"/>
</Style>
<!-- Бейдж ошибки (красный) -->
<Style x:Key="BadgeError" TargetType="Border" BasedOn="{StaticResource Badge}">
<Setter Property="BackgroundColor" Value="#EF4444"/>
</Style>
<!-- Бейдж предупреждения (оранжевый) -->
<Style x:Key="BadgeWarning" TargetType="Border" BasedOn="{StaticResource Badge}">
<Setter Property="BackgroundColor" Value="#F59E0B"/>
</Style>
<!-- Бейдж информации (синий) -->
<Style x:Key="BadgeInfo" TargetType="Border" BasedOn="{StaticResource Badge}">
<Setter Property="BackgroundColor" Value="#3B82F6"/>
</Style>
<!-- Текст бейджа -->
<Style x:Key="BadgeText" TargetType="Label">
<Setter Property="FontSize" Value="11"/>
<Setter Property="TextColor" Value="White"/>
<Setter Property="FontAttributes" Value="Bold"/>
</Style>
<!-- Тег/навык -->
<Style x:Key="SkillTag" TargetType="Border">
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="StrokeShape" Value="RoundRectangle 15"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray100}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<Style x:Key="SkillTagText" TargetType="Label">
<Setter Property="FontSize" Value="13"/>
<Setter Property="TextColor" Value="{StaticResource Gray600}"/>
</Style>
<!-- ================================
ИКОНКИ С ТЕКСТОМ (Icon + Text)
================================ -->
<!-- Мета-информация с иконкой -->
<Style x:Key="MetaIcon" TargetType="Label">
<Setter Property="FontSize" Value="14"/>
</Style>
<Style x:Key="MetaText" TargetType="Label">
<Setter Property="FontSize" Value="13"/>
<Setter Property="TextColor" Value="{StaticResource Gray700}"/>
</Style>
<!-- ================================
КОНТЕЙНЕРЫ (Containers)
================================ -->
<!-- Контейнер для пустого состояния -->
<Style x:Key="EmptyStateContainer" TargetType="VerticalStackLayout">
<Setter Property="VerticalOptions" Value="Center"/>
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="Padding" Value="50"/>
</Style>
<!-- Иконка-контейнер (для заявлений и т.д.) -->
<Style x:Key="IconContainer" TargetType="Border">
<Setter Property="WidthRequest" Value="45"/>
<Setter Property="HeightRequest" Value="45"/>
<Setter Property="StrokeShape" Value="RoundRectangle 10"/>
<Setter Property="BackgroundColor" Value="{StaticResource Gray100}"/>
<Setter Property="Stroke" Value="Transparent"/>
</Style>
<Style x:Key="IconContainerText" TargetType="Label">
<Setter Property="FontSize" Value="24"/>
<Setter Property="HorizontalOptions" Value="Center"/>
<Setter Property="VerticalOptions" Value="Center"/>
</Style>
</ResourceDictionary>

View File

@ -26,6 +26,7 @@
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray700">#303030</Color>
<Color x:Key="Gray800">#282828</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
@ -44,6 +45,7 @@
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray700Brush" Color="{StaticResource Gray700}"/>
<SolidColorBrush x:Key="Gray800Brush" Color="{StaticResource Gray800}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

View File

@ -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
}
/// <summary>
/// Получить список всех сотрудников (все страницы)
/// Получить список всех активных сотрудников (все страницы)
/// </summary>
public async Task<List<User>> GetAllEmployeesAsync()
{
@ -742,7 +742,15 @@ public class ApiService : IDisposable
do
{
response = await GetEmployeesPaginatedAsync(page: currentPage);
Dictionary<string, string> queryParams = new Dictionary<string, string>
{
["active"] = "true",
["page"] = currentPage.ToString()
};
ApiResponse<PaginatedResponse<User>> apiResponse = await GetAsync<PaginatedResponse<User>>("api/v1/employees/", queryParams);
response = apiResponse.Ok && apiResponse.Data != null ? apiResponse.Data : new PaginatedResponse<User>();
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<User>();
}
}
/// <summary>
/// Получить информацию о конкретном пользователе по ID
/// </summary>
public async Task<User?> GetUserByIdAsync(int userId)
{
try
{
ApiResponse<User> response = await GetAsync<User>($"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;
}
}
/// <summary>
/// Получить информацию о текущем пользователе
/// Использует эндпоинт /api/v1/employees/me/
/// </summary>
public async Task<User?> 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<User> response = await GetAsync<User>("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<PaginatedResponse<Department>> response = await GetAsync<PaginatedResponse<Department>>("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<Department>();
}
catch (Exception ex)
@ -848,6 +934,68 @@ public class ApiService : IDisposable
}
}
/// <summary>
/// Получить информацию о конкретном отделе
/// </summary>
public async Task<Department?> GetDepartmentByIdAsync(int departmentId)
{
try
{
ApiResponse<Department> response = await GetAsync<Department>($"api/v1/departments/{departmentId}/");
return response.Ok && response.Data != null ? response.Data : null;
}
catch (Exception ex)
{
Trace.WriteLine($"Ошибка получения отдела {departmentId}: {ex.Message}");
return null;
}
}
/// <summary>
/// Получить список сотрудников отдела
/// </summary>
public async Task<List<User>> GetDepartmentEmployeesAsync(int departmentId)
{
try
{
Dictionary<string, string> queryParams = new Dictionary<string, string>
{
{ "department", departmentId.ToString() }
};
List<User> allEmployees = new List<User>();
int currentPage = 1;
do
{
queryParams["page"] = currentPage.ToString();
var apiResponse = await GetAsync<PaginatedResponse<User>>("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<User>();
}
}
// ============================================
// POSITIONS (Должности)
// ============================================
@ -961,6 +1109,46 @@ public class ApiService : IDisposable
}
}
/// <summary>
/// Получить все заявления (с автоматической пагинацией)
/// </summary>
public async Task<List<Request>> GetAllRequestsAsync()
{
List<Request> allRequests = new List<Request>();
int page = 1;
bool hasMore = true;
try
{
Trace.WriteLine("[ApiService] >>> Загрузка всех заявлений...");
while (hasMore)
{
PaginatedResponse<Request> 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 (Действия сотрудников)
// ============================================

View File

@ -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;
}
}

View File

@ -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}");
// Не пробрасываем исключение дальше - ошибка загрузки аватара не критична
}
}

View File

@ -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** - профиль пользователя

View File

@ -126,7 +126,7 @@ private async void OnLogoutClicked(object sender, EventArgs e)
### Получение списка сотрудников
```csharp
// В UsersPage.xaml.cs
// В EmployeesPage.xaml.cs
private List<User> _allUsers;
private ObservableCollection<User> _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
<!-- В UsersPage.xaml -->
<!-- В EmployeesPage.xaml -->
<RefreshView x:Name="RefreshView" Refreshing="OnRefreshing">
<CollectionView x:Name="UsersCollectionView">
<CollectionView x:Name="EmployeesCollectionView">
<!-- ItemTemplate -->
</CollectionView>
</RefreshView>
@ -454,7 +454,7 @@ private async Task EnsureWebSocketConnection()
### Обработка изменения статуса пользователя
```csharp
// В UsersPage.xaml.cs
// В EmployeesPage.xaml.cs
protected override void OnAppearing()
{
base.OnAppearing();

26
clean-and-build.bat Normal file
View File

@ -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

84
clean-and-build.ps1 Normal file
View File

@ -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")

44
test-departments-api.ps1 Normal file
View File

@ -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")

74
test-deserialization.csx Normal file
View File

@ -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<TestDepartment>(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<TestDepartment>(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("Тест завершен успешно!");