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:
parent
b168416fa6
commit
979d79780b
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="RiderAndroidProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
@ -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>
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -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 ===");
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 ?? "Неизвестно"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- Список сообщений -->
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
|
||||
207
CORP-MAUI/Pages/DepartmentDetailsPage.xaml
Normal file
207
CORP-MAUI/Pages/DepartmentDetailsPage.xaml
Normal 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>
|
||||
|
||||
160
CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
Normal file
160
CORP-MAUI/Pages/DepartmentDetailsPage.xaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
CORP-MAUI/Pages/DepartmentsPage.xaml
Normal file
119
CORP-MAUI/Pages/DepartmentsPage.xaml
Normal 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>
|
||||
|
||||
174
CORP-MAUI/Pages/DepartmentsPage.xaml.cs
Normal file
174
CORP-MAUI/Pages/DepartmentsPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
CORP-MAUI/Pages/EmployeeProfilePage.xaml
Normal file
140
CORP-MAUI/Pages/EmployeeProfilePage.xaml
Normal 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>
|
||||
|
||||
180
CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
Normal file
180
CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
213
CORP-MAUI/Pages/EmployeesPage.xaml.cs
Normal file
213
CORP-MAUI/Pages/EmployeesPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 обрезан -->
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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. Если нет в кэше - загружаем
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
<Frame
|
||||
WidthRequest="100"
|
||||
HeightRequest="100"
|
||||
CornerRadius="50"
|
||||
CornerRadius="25"
|
||||
Padding="0"
|
||||
IsClippedToBounds="True"
|
||||
HasShadow="False"
|
||||
|
||||
191
CORP-MAUI/Pages/RequestsPage.xaml
Normal file
191
CORP-MAUI/Pages/RequestsPage.xaml
Normal 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>
|
||||
285
CORP-MAUI/Pages/RequestsPage.xaml.cs
Normal file
285
CORP-MAUI/Pages/RequestsPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
360
CORP-MAUI/Resources/Styles/AppStyles.xaml
Normal file
360
CORP-MAUI/Resources/Styles/AppStyles.xaml
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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 (Действия сотрудников)
|
||||
// ============================================
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}");
|
||||
// Не пробрасываем исключение дальше - ошибка загрузки аватара не критична
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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** - профиль пользователя
|
||||
|
||||
|
||||
@ -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
26
clean-and-build.bat
Normal 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
84
clean-and-build.ps1
Normal 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
44
test-departments-api.ps1
Normal 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
74
test-deserialization.csx
Normal 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("Тест завершен успешно!");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user