using System.Diagnostics; namespace CORP_MAUI.Controls; public partial class DesktopLayout : ContentView { public DesktopLayout() { InitializeComponent(); Sidebar.NavigationRequested += OnNavigationRequested; // По умолчанию показываем ленту новостей LoadDefaultContent(); } private void LoadDefaultContent() { // Создаём контент для ленты новостей в десктопном стиле var newsContent = CreateDesktopPostsContent(); SetContent(newsContent); } private View CreateDesktopPostsContent() { // Заголовок страницы с поиском и кнопкой добавления var header = new Grid { ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition(GridLength.Auto), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Auto) }, Margin = new Thickness(0, 0, 0, 20) }; var title = new Label { Text = "Лента новостей", FontSize = 24, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333"), VerticalOptions = LayoutOptions.Center }; // Поисковая строка (по центру) var searchBox = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 20 }, BackgroundColor = Color.FromArgb("#F5F5F5"), Stroke = Color.FromArgb("#E0E0E0"), StrokeThickness = 1, Padding = new Thickness(15, 8), MaximumWidthRequest = 300, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; var searchLayout = new HorizontalStackLayout { Spacing = 10 }; searchLayout.Children.Add(new Label { Text = "🔍", FontSize = 14, TextColor = Color.FromArgb("#888888"), VerticalOptions = LayoutOptions.Center }); searchLayout.Children.Add(new Entry { Placeholder = "Поиск по названию, автору или...", PlaceholderColor = Color.FromArgb("#888888"), BackgroundColor = Colors.Transparent, WidthRequest = 200 }); searchBox.Content = searchLayout; // Кнопка добавления новости var addButton = new HorizontalStackLayout { Spacing = 8, VerticalOptions = LayoutOptions.Center }; var plusBorder = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 12 }, BackgroundColor = Color.FromArgb("#0D6EFD"), HeightRequest = 24, WidthRequest = 24, Content = new Label { Text = "+", FontSize = 16, TextColor = Colors.White, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center } }; var addLabel = new Label { Text = "Новость", FontSize = 16, TextColor = Color.FromArgb("#0D6EFD"), VerticalOptions = LayoutOptions.Center }; addButton.Children.Add(plusBorder); addButton.Children.Add(addLabel); Grid.SetColumn(title, 0); Grid.SetColumn(searchBox, 1); Grid.SetColumn(addButton, 2); header.Children.Add(title); header.Children.Add(searchBox); header.Children.Add(addButton); // Контейнер для контента // Вместо простого VerticalStackLayout используем Grid, чтобы выделить header и scrollable area var root = new Grid { RowDefinitions = new RowDefinitionCollection { new RowDefinition { Height = GridLength.Auto }, new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } }, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }; root.Add(header); Grid.SetRow(header, 0); // Здесь будет ScrollView с постами (загружается асинхронно) var postsContainer = new VerticalStackLayout { Spacing = 15, HorizontalOptions = LayoutOptions.Fill }; // Placeholder пока загружаются посты var loadingIndicator = new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }; postsContainer.Children.Add(loadingIndicator); // Корректно настраиваем ScrollView: Fill и layout-constraints var scrollView = new ScrollView { Content = postsContainer, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }; // Помещаем scrollView в фиксированную ячейку Grid root.Add(scrollView); Grid.SetRow(scrollView, 1); // Загружаем посты асинхронно _ = LoadPostsAsync(postsContainer); return root; } private async Task LoadPostsAsync(VerticalStackLayout container) { try { var response = await ServiceProvider.ApiService.GetPostsPaginatedAsync(); var posts = response?.Results; await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); if (posts == null || !posts.Any()) { container.Children.Add(new Label { Text = "Нет новостей", TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }); return; } foreach (var post in posts) { var postCard = CreatePostCard(post); // Даем карточке разумные ограничения по ширине — чтобы не становилась слишком большой if (postCard is View v) { v.HorizontalOptions = LayoutOptions.Fill; v.WidthRequest = double.NaN; // позволяет занять доступную ширину v.MaximumWidthRequest = 900; // не растягивать на весь экран } container.Children.Add(postCard); } }); } catch (Exception ex) { await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(new Label { Text = $"Ошибка загрузки: {ex.Message}", TextColor = Color.FromArgb("#FF0000"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }); }); } } private View CreatePostCard(CORP_MAUI.Models.Post post) { var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 12 }, Stroke = Colors.Transparent, Padding = new Thickness(20), Shadow = new Shadow { Brush = new SolidColorBrush(Colors.Black), Opacity = 0.1f, Offset = new Point(0, 2), Radius = 8 }, HorizontalOptions = LayoutOptions.Fill }; var content = new VerticalStackLayout { Spacing = 12 }; // Автор и дата var authorRow = new HorizontalStackLayout { Spacing = 10 }; var avatarImage = new Image { Source = post.Author?.DisplayAvatarSource, Aspect = Aspect.AspectFill, HeightRequest = 40, WidthRequest = 40 }; var avatar = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 20 }, HeightRequest = 40, WidthRequest = 40, BackgroundColor = Color.FromArgb("#E0E0E0"), Stroke = Colors.Transparent, Content = avatarImage }; var authorInfo = new VerticalStackLayout(); authorInfo.Children.Add(new Label { Text = post.Author?.FullName ?? "Автор", FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333") }); authorInfo.Children.Add(new Label { Text = post.CreatedAt.ToString("dd.MM.yyyy HH:mm"), FontSize = 12, TextColor = Color.FromArgb("#888888") }); authorRow.Children.Add(avatar); authorRow.Children.Add(authorInfo); content.Children.Add(authorRow); // Изображение поста (если есть) if (!string.IsNullOrEmpty(post.DisplayImageUrl)) { var imageContainer = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, Stroke = Colors.Transparent, HeightRequest = 300, Margin = new Thickness(0, 10) }; var postImage = new Image { Source = post.DisplayImageUrl, Aspect = Aspect.AspectFill, HeightRequest = 300, HorizontalOptions = LayoutOptions.Fill }; imageContainer.Content = postImage; content.Children.Add(imageContainer); } // Заголовок if (!string.IsNullOrEmpty(post.Title)) { content.Children.Add(new Label { Text = post.Title, FontSize = 18, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#0D6EFD") }); } // Текст if (!string.IsNullOrEmpty(post.Content)) { var isTruncated = post.Content.Length > 200; var textContent = isTruncated ? post.Content.Substring(0, 200) + "..." : post.Content; var textRow = new HorizontalStackLayout { Spacing = 5 }; textRow.Children.Add(new Label { Text = textContent, TextColor = Color.FromArgb("#333333"), LineBreakMode = LineBreakMode.WordWrap, HorizontalOptions = LayoutOptions.FillAndExpand }); if (isTruncated) { textRow.Children.Add(new Label { Text = "Показать дальше", TextColor = Color.FromArgb("#0D6EFD"), FontSize = 14, VerticalOptions = LayoutOptions.End }); } content.Children.Add(textRow); } // Лайки и комментарии var statsRow = new HorizontalStackLayout { Spacing = 20, Margin = new Thickness(0, 10, 0, 0) }; statsRow.Children.Add(new HorizontalStackLayout { Spacing = 5, Children = { new Label { Text = "👍", FontSize = 14 }, new Label { Text = post.LikesCount.ToString(), TextColor = Color.FromArgb("#888888") } } }); statsRow.Children.Add(new HorizontalStackLayout { Spacing = 5, Children = { new Label { Text = "💬", FontSize = 14 }, new Label { Text = post.CommentsCount.ToString(), TextColor = Color.FromArgb("#888888") } } }); content.Children.Add(statsRow); card.Content = content; return card; } public void SetContent(View content) { MainContent.Content = content; } private void OnNavigationRequested(object? sender, string route) { switch (route) { case "Profile": LoadProfileContent(); break; case "News": LoadDefaultContent(); break; case "Employees": LoadEmployeesContent(); break; case "Departments": LoadDepartmentsContent(); break; case "Requests": LoadRequestsContent(); break; case "Chats": LoadChatsContent(); break; case "Documents": LoadDocumentsContent(); break; } } private void LoadProfileContent() { var content = new VerticalStackLayout { Spacing = 15 }; content.Children.Add(CreatePageHeader("Мой профиль")); var loadingIndicator = new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }; content.Children.Add(loadingIndicator); SetContent(content); _ = LoadProfileAsync(content); } private async Task LoadProfileAsync(VerticalStackLayout container) { try { var user = await ServiceProvider.ApiService.GetCurrentUserAsync(); // Предзагружаем аватар if (user != null) { await ServiceProvider.ApiService.LoadUserAvatarAsync(user); } await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(CreatePageHeader("Мой профиль")); if (user == null) { container.Children.Add(new Label { Text = "Не удалось загрузить профиль", TextColor = Colors.Red }); return; } var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 12 }, Stroke = Colors.Transparent, Padding = new Thickness(30), Shadow = new Shadow { Brush = new SolidColorBrush(Colors.Black), Opacity = 0.1f, Offset = new Point(0, 2), Radius = 8 } }; var profileContent = new HorizontalStackLayout { Spacing = 30 }; // Аватар var avatarImage = new Image { Source = user.DisplayAvatarSource, Aspect = Aspect.AspectFill, HeightRequest = 120, WidthRequest = 120 }; var avatarBorder = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 60 }, HeightRequest = 120, WidthRequest = 120, BackgroundColor = Color.FromArgb("#E0E0E0"), Stroke = Colors.Transparent, Content = avatarImage }; // Индикатор онлайн if (user.IsOnline) { var onlineIndicator = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, HeightRequest = 16, WidthRequest = 16, BackgroundColor = Color.FromArgb("#28A745"), Stroke = Colors.White, StrokeThickness = 2, VerticalOptions = LayoutOptions.End, HorizontalOptions = LayoutOptions.End, Margin = new Thickness(0, 0, 8, 8) }; var avatarGrid = new Grid { Children = { avatarBorder, onlineIndicator } }; profileContent.Children.Add(avatarGrid); } else { profileContent.Children.Add(avatarBorder); } // Информация о пользователе var infoStack = new VerticalStackLayout { Spacing = 10 }; infoStack.Children.Add(new Label { Text = user.FullName, FontSize = 24, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333") }); if (!string.IsNullOrEmpty(user.PositionName)) infoStack.Children.Add(new Label { Text = user.PositionName, FontSize = 16, TextColor = Color.FromArgb("#0D6EFD") }); infoStack.Children.Add(new Label { Text = user.Email, FontSize = 14, TextColor = Color.FromArgb("#666666") }); if (!string.IsNullOrEmpty(user.PhoneNumber)) infoStack.Children.Add(new Label { Text = user.PhoneNumber, FontSize = 14, TextColor = Color.FromArgb("#666666") }); // Отделы if (user.Departments != null && user.Departments.Any()) { infoStack.Children.Add(new Label { Text = "Отделы:", FontSize = 13, TextColor = Color.FromArgb("#888888"), Margin = new Thickness(0, 10, 0, 0) }); foreach (var dept in user.Departments) { infoStack.Children.Add(new Label { Text = $"• {dept.Name}", FontSize = 14, TextColor = Color.FromArgb("#404040") }); } } // Навыки if (user.Skills != null && user.Skills.Any()) { infoStack.Children.Add(new Label { Text = "Навыки:", FontSize = 13, TextColor = Color.FromArgb("#888888"), Margin = new Thickness(0, 10, 0, 0) }); var skillsLayout = new FlexLayout { Wrap = Microsoft.Maui.Layouts.FlexWrap.Wrap, JustifyContent = Microsoft.Maui.Layouts.FlexJustify.Start }; foreach (var skill in user.Skills) { var skillBadge = new Border { Padding = new Thickness(10, 5), Margin = new Thickness(0, 5, 8, 0), StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 12 }, BackgroundColor = Color.FromArgb("#E8F4FD"), Content = new Label { Text = skill.Name, FontSize = 12, TextColor = Color.FromArgb("#0D6EFD") } }; skillsLayout.Children.Add(skillBadge); } infoStack.Children.Add(skillsLayout); } profileContent.Children.Add(infoStack); card.Content = profileContent; container.Children.Add(card); }); } catch (Exception ex) { await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(new Label { Text = $"Ошибка: {ex.Message}", TextColor = Colors.Red }); }); } } private void LoadEmployeesContent() { var content = new VerticalStackLayout { Spacing = 15 }; content.Children.Add(CreatePageHeader("Сотрудники")); var container = new VerticalStackLayout { Spacing = 10 }; container.Children.Add(new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }); // Устанавливаем FillAndExpand чтобы контейнер занял пространство внутри MainContent content.Children.Add(new ScrollView { Content = container, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }); SetContent(content); _ = LoadEmployeesAsync(container); } private async Task LoadEmployeesAsync(VerticalStackLayout container) { try { var response = await ServiceProvider.ApiService.GetEmployeesPaginatedAsync(); var employees = response?.Results; await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); if (employees == null || !employees.Any()) { container.Children.Add(new Label { Text = "Нет сотрудников", TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center }); return; } foreach (var emp in employees) { var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, Stroke = Colors.Transparent, Padding = new Thickness(15), Margin = new Thickness(0, 0, 0, 5), HorizontalOptions = LayoutOptions.Fill }; var row = new HorizontalStackLayout { Spacing = 15 }; // Аватарка сотрудника var avatarImage = new Image { Source = emp.DisplayAvatarSource, Aspect = Aspect.AspectFill, HeightRequest = 40, WidthRequest = 40 }; var avatarBorder = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 20 }, HeightRequest = 40, WidthRequest = 40, BackgroundColor = Color.FromArgb("#E0E0E0"), Stroke = Colors.Transparent, Content = avatarImage }; row.Children.Add(avatarBorder); var info = new VerticalStackLayout(); info.Children.Add(new Label { Text = emp.FullName, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333") }); info.Children.Add(new Label { Text = emp.PositionName ?? emp.Email, FontSize = 12, TextColor = Color.FromArgb("#888888") }); row.Children.Add(info); card.Content = row; // Добавляем обработчик нажатия для открытия профиля сотрудника var tapGesture = new TapGestureRecognizer(); int employeeId = emp.Id; // Захватываем ID для замыкания tapGesture.Tapped += async (_, _) => { try { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] >>> Открытие профиля сотрудника ID: {employeeId}"); var nav = ResolveNavigation(); if (nav != null) { await nav.PushAsync(new CORP_MAUI.Pages.EmployeeProfilePage(employeeId)); return; } System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Не удалось найти навигацию для открытия профиля {employeeId}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Ошибка навигации к профилю: {ex.Message}"); } }; card.GestureRecognizers.Add(tapGesture); container.Children.Add(card); } }); } catch (Exception ex) { await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(new Label { Text = $"Ошибка: {ex.Message}", TextColor = Colors.Red }); }); } } private void LoadDepartmentsContent() { // Заголовок с количеством и поиском var headerRow = new Grid { ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition(GridLength.Auto), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Auto) }, Margin = new Thickness(0, 0, 0, 20) }; var titleStack = new VerticalStackLayout(); titleStack.Children.Add(new Label { Text = "Отделы", FontSize = 24, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333") }); var countLabel = new Label { Text = "Всего отделов: ...", FontSize = 13, TextColor = Color.FromArgb("#888888") }; titleStack.Children.Add(countLabel); Grid.SetColumn(titleStack, 0); headerRow.Children.Add(titleStack); // Поисковая строка var searchBox = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 20 }, BackgroundColor = Color.FromArgb("#F5F5F5"), Stroke = Color.FromArgb("#E0E0E0"), StrokeThickness = 1, Padding = new Thickness(15, 8), MaximumWidthRequest = 300, HorizontalOptions = LayoutOptions.End, VerticalOptions = LayoutOptions.Center }; var searchLayout = new HorizontalStackLayout { Spacing = 10 }; searchLayout.Children.Add(new Image { Source = "search", HeightRequest = 16, WidthRequest = 16 }); searchLayout.Children.Add(new Entry { Placeholder = "Поиск по названию, автору", PlaceholderColor = Color.FromArgb("#888888"), BackgroundColor = Colors.Transparent, WidthRequest = 200 }); searchBox.Content = searchLayout; Grid.SetColumn(searchBox, 2); headerRow.Children.Add(searchBox); // Контейнер для сетки отделов с прокруткой var gridContainer = new VerticalStackLayout { Spacing = 12, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }; // Добавляем ActivityIndicator пока загружается var activityIndicator = new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }; gridContainer.Children.Add(activityIndicator); // Используем ScrollView вокруг контейнера (и обеспечиваем FillAndExpand у scrollView) var scrollView = new ScrollView { Content = gridContainer, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }; // Основной контейнер var content = new Grid { RowDefinitions = new RowDefinitionCollection { new RowDefinition { Height = GridLength.Auto }, new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } }, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill }; Grid.SetRow(headerRow, 0); content.Children.Add(headerRow); Grid.SetRow(scrollView, 1); content.Children.Add(scrollView); SetContent(content); _ = LoadDepartmentsAsync(gridContainer, countLabel); } private async Task LoadDepartmentsAsync(VerticalStackLayout container, Label countLabel) { try { System.Diagnostics.Debug.WriteLine("[DesktopLayout] >>> Начало загрузки отделов"); var departments = await ServiceProvider.ApiService.GetAllDepartmentsAsync(); System.Diagnostics.Debug.WriteLine($"[DesktopLayout] Получено отделов: {departments?.Count ?? 0}"); await MainThread.InvokeOnMainThreadAsync(() => { System.Diagnostics.Debug.WriteLine("[DesktopLayout] Очистка контейнера..."); container.Children.Clear(); if (departments == null || !departments.Any()) { System.Diagnostics.Debug.WriteLine("[DesktopLayout] ⚠️ Нет отделов для отображения"); var emptyLabel = new Label { Text = "Нет отделов", TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center }; container.Children.Add(emptyLabel); return; } countLabel.Text = $"Всего отделов: {departments.Count}"; System.Diagnostics.Debug.WriteLine($"[DesktopLayout] Создание {departments.Count} карточек отделов..."); // Для корректной прокрутки и размеров используем Grid с равномерными колонками (Star) и поместим его в ScrollView var grid = new Grid { ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) } }, ColumnSpacing = 16, RowSpacing = 16, HorizontalOptions = LayoutOptions.Fill }; // Добавляем карточки в Grid for (int i = 0; i < departments.Count; i++) { var card = CreateDepartmentCard(departments[i]); int row = i / 2; int col = i % 2; // Добавляем строку если нужно if (row >= grid.RowDefinitions.Count) { grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); } Grid.SetRow(card, row); Grid.SetColumn(card, col); grid.Children.Add(card); System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ✅ Добавлена карточка {i + 1} в позицию [{row}, {col}]"); } // Оборачиваем grid в контейнер, даём ему FillAndExpand чтобы прокрутка работала корректно grid.HorizontalOptions = LayoutOptions.Fill; container.Children.Add(grid); System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ✅ Grid создан с {grid.RowDefinitions.Count} строками"); // Загружаем аватарки сотрудников для каждого отдела foreach (var dept in departments) { _ = LoadDepartmentEmployeesAsync(dept); } }); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Ошибка загрузки отделов: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"[DesktopLayout] Stack trace: {ex.StackTrace}"); await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); var errorLabel = new Label { Text = $"Ошибка: {ex.Message}", TextColor = Colors.Red }; container.Children.Add(errorLabel); }); } } private View CreateDepartmentCard(CORP_MAUI.Models.Department dept) { var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 12 }, Stroke = Color.FromArgb("#E0E0E0"), StrokeThickness = 1, Padding = new Thickness(16), Margin = new Thickness(0), HorizontalOptions = LayoutOptions.Fill }; var content = new VerticalStackLayout { Spacing = 8 }; // Название отдела content.Children.Add(new Label { Text = dept.Name, FontSize = 15, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333"), LineBreakMode = LineBreakMode.TailTruncation, MaxLines = 1 }); // Руководитель content.Children.Add(new Label { Text = "Руководитель:", FontSize = 11, TextColor = Color.FromArgb("#888888") }); string headName = dept.Head?.FullName ?? "Не назначен"; content.Children.Add(new Label { Text = headName, FontSize = 12, TextColor = Color.FromArgb("#0D6EFD"), FontAttributes = FontAttributes.None, LineBreakMode = LineBreakMode.TailTruncation, MaxLines = 1 }); // Количество сотрудников content.Children.Add(new Label { Text = $"{dept.EmployeesCount} сотрудников", FontSize = 11, TextColor = Color.FromArgb("#666666"), Margin = new Thickness(0, 4, 0, 4) }); // Аватарки сотрудников (серые кружки, которые потом заменятся на реальные) var avatarsRow = new HorizontalStackLayout { Spacing = -8, HorizontalOptions = LayoutOptions.Start }; // Сохраняем ссылку на контейнер аватарок в свойстве отдела для последующего обновления dept.AvatarsContainer = avatarsRow; int avatarCount = Math.Min(dept.EmployeesCount, 5); for (int i = 0; i < avatarCount; i++) { var avatar = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 15 }, HeightRequest = 30, WidthRequest = 30, BackgroundColor = Color.FromArgb("#E0E0E0"), Stroke = Colors.White, StrokeThickness = 2 }; avatarsRow.Children.Add(avatar); } content.Children.Add(avatarsRow); card.Content = content; // Добавляем обработчик нажатия на карточку для навигации var tapGesture = new TapGestureRecognizer(); int deptId = dept.Id; // Захватываем ID для замыкания tapGesture.Tapped += async (_, _) => { try { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] >>> Открытие отдела ID: {deptId}"); var nav = ResolveNavigation(); if (nav != null) { await nav.PushAsync(new CORP_MAUI.Pages.DepartmentDetailsPage(deptId)); return; } System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Не удалось найти навигацию для открытия отдела {deptId}"); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Ошибка навигации к отделу: {ex.Message}"); } }; card.GestureRecognizers.Add(tapGesture); return card; } private async Task LoadDepartmentEmployeesAsync(CORP_MAUI.Models.Department dept) { try { // Получаем сотрудников отдела var employees = await ServiceProvider.ApiService.GetDepartmentEmployeesAsync(dept.Id); if (employees != null && employees.Any()) { // Обновляем аватарки в UI await MainThread.InvokeOnMainThreadAsync(() => { // Найдем карточку отдела и обновим аватарки if (dept.AvatarsContainer != null) { dept.AvatarsContainer.Children.Clear(); int avatarCount = Math.Min(employees.Count, 5); for (int i = 0; i < avatarCount; i++) { var emp = employees[i]; var avatarImage = new Image { Source = emp.DisplayAvatarSource, Aspect = Aspect.AspectFill, HeightRequest = 30, WidthRequest = 30 }; var avatar = new Border { StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 15 }, HeightRequest = 30, WidthRequest = 30, BackgroundColor = Color.FromArgb("#E0E0E0"), Stroke = Colors.White, StrokeThickness = 2, Content = avatarImage }; dept.AvatarsContainer.Children.Add(avatar); } } }); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[DesktopLayout] Ошибка загрузки сотрудников отдела {dept.Id}: {ex.Message}"); } } private void LoadRequestsContent() { var content = new VerticalStackLayout { Spacing = 15 }; content.Children.Add(CreatePageHeader("Заявления")); var container = new VerticalStackLayout { Spacing = 10 }; container.Children.Add(new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }); content.Children.Add(new ScrollView { Content = container }); SetContent(content); _ = LoadRequestsAsync(container); } private async Task LoadRequestsAsync(VerticalStackLayout container) { try { var response = await ServiceProvider.ApiService.GetRequestsPaginatedAsync(); var requests = response?.Results; await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); if (requests == null || !requests.Any()) { container.Children.Add(new Label { Text = "Нет заявлений", TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center }); return; } foreach (var req in requests) { var statusColor = req.Status switch { "approved" => Color.FromArgb("#28A745"), "rejected" => Color.FromArgb("#DC3545"), _ => Color.FromArgb("#FFC107") }; var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, Stroke = Colors.Transparent, Padding = new Thickness(15), Margin = new Thickness(0, 0, 0, 5) }; var info = new VerticalStackLayout { Spacing = 5 }; info.Children.Add(new Label { Text = req.DisplayTitle ?? req.Title, FontSize = 16, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333") }); info.Children.Add(new Label { Text = req.Employee?.FullName ?? "", FontSize = 13, TextColor = Color.FromArgb("#666666") }); var statusLabel = new Label { Text = req.DisplayStatus ?? req.Status, FontSize = 12, TextColor = statusColor }; info.Children.Add(statusLabel); card.Content = info; container.Children.Add(card); } }); } catch (Exception ex) { await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(new Label { Text = $"Ошибка: {ex.Message}", TextColor = Colors.Red }); }); } } private void LoadChatsContent() { var content = new VerticalStackLayout { Spacing = 15 }; content.Children.Add(CreatePageHeader("Чаты")); var placeholder = new VerticalStackLayout { HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Margin = new Thickness(0, 100) }; placeholder.Children.Add(new Label { Text = "💬", FontSize = 48, HorizontalOptions = LayoutOptions.Center }); placeholder.Children.Add(new Label { Text = "Раздел чатов находится в разработке", FontSize = 16, TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center }); content.Children.Add(placeholder); SetContent(content); } private void LoadDocumentsContent() { var content = new VerticalStackLayout { Spacing = 15 }; content.Children.Add(CreatePageHeader("Документы")); var container = new VerticalStackLayout { Spacing = 10 }; container.Children.Add(new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }); content.Children.Add(new ScrollView { Content = container }); SetContent(content); _ = LoadDocumentsAsync(container); } private async Task LoadDocumentsAsync(VerticalStackLayout container) { try { var documents = await ServiceProvider.ApiService.GetAllDocumentsAsync(); await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); if (documents == null || !documents.Any()) { var placeholder = new VerticalStackLayout { HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) }; placeholder.Children.Add(new Label { Text = "📄", FontSize = 48, HorizontalOptions = LayoutOptions.Center }); placeholder.Children.Add(new Label { Text = "Нет документов для отображения", FontSize = 16, TextColor = Color.FromArgb("#888888"), HorizontalOptions = LayoutOptions.Center }); container.Children.Add(placeholder); return; } foreach (var doc in documents) { var statusColor = doc.IsAcknowledged ? Color.FromArgb("#28A745") // Зеленый - ознакомлен : Color.FromArgb("#FFC107"); // Желтый - не ознакомлен var card = new Border { BackgroundColor = Colors.White, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, Stroke = Colors.Transparent, Padding = new Thickness(15), Margin = new Thickness(0, 0, 0, 5), Shadow = new Shadow { Brush = new SolidColorBrush(Colors.Black), Opacity = 0.05f, Offset = new Point(0, 2), Radius = 4 } }; var row = new Grid { ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition(new GridLength(50)), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Auto) } }; // Иконка документа var iconBorder = new Border { WidthRequest = 40, HeightRequest = 40, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 }, BackgroundColor = Color.FromArgb("#E8F0FE"), Stroke = Colors.Transparent, VerticalOptions = LayoutOptions.Center }; iconBorder.Content = new Label { Text = doc.FileIcon, FontSize = 20, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; Grid.SetColumn(iconBorder, 0); row.Children.Add(iconBorder); // Информация о документе var info = new VerticalStackLayout { Spacing = 3, VerticalOptions = LayoutOptions.Center }; info.Children.Add(new Label { Text = doc.Title, FontSize = 15, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333"), LineBreakMode = LineBreakMode.TailTruncation, MaxLines = 1 }); info.Children.Add(new Label { Text = doc.ShortDescription, FontSize = 12, TextColor = Color.FromArgb("#666666"), LineBreakMode = LineBreakMode.TailTruncation, MaxLines = 1 }); var detailsRow = new HorizontalStackLayout { Spacing = 10 }; detailsRow.Children.Add(new Label { Text = $"👤 {doc.UploaderName}", FontSize = 11, TextColor = Color.FromArgb("#888888") }); detailsRow.Children.Add(new Label { Text = "•", FontSize = 11, TextColor = Color.FromArgb("#CCCCCC") }); detailsRow.Children.Add(new Label { Text = doc.DisplayUploadedAt, FontSize = 11, TextColor = Color.FromArgb("#888888") }); info.Children.Add(detailsRow); Grid.SetColumn(info, 1); row.Children.Add(info); // Статус ознакомления var statusBadge = new Border { BackgroundColor = statusColor, StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 10 }, Stroke = Colors.Transparent, Padding = new Thickness(10, 5), VerticalOptions = LayoutOptions.Center }; statusBadge.Content = new Label { Text = doc.AcknowledgedText, FontSize = 11, TextColor = Colors.White }; Grid.SetColumn(statusBadge, 2); row.Children.Add(statusBadge); card.Content = row; // Обработчик нажатия - открытие документа var tapGesture = new TapGestureRecognizer(); tapGesture.Tapped += async (s, e) => { if (!string.IsNullOrEmpty(doc.FileUrl)) { try { await Launcher.OpenAsync(new Uri(doc.FileUrl)); } catch (Exception ex) { if (Application.Current?.Windows.Count > 0) await Application.Current.Windows[0].Page!.DisplayAlert("Ошибка", $"Не удалось открыть документ: {ex.Message}", "OK"); } } }; card.GestureRecognizers.Add(tapGesture); container.Children.Add(card); } }); } catch (Exception ex) { await MainThread.InvokeOnMainThreadAsync(() => { container.Children.Clear(); container.Children.Add(new Label { Text = $"Ошибка загрузки документов: {ex.Message}", TextColor = Colors.Red, HorizontalOptions = LayoutOptions.Center }); }); } } private View CreatePageHeader(string title, string? buttonText = null) { var header = new Grid { ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition(GridLength.Auto), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Auto) }, Margin = new Thickness(0, 0, 0, 20) }; var titleLabel = new Label { Text = title, FontSize = 24, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb("#333333"), VerticalOptions = LayoutOptions.Center }; Grid.SetColumn(titleLabel, 0); header.Children.Add(titleLabel); if (!string.IsNullOrEmpty(buttonText)) { var addButton = new HorizontalStackLayout { Spacing = 8, VerticalOptions = LayoutOptions.Center }; addButton.Children.Add(new Label { Text = "+", FontSize = 20, TextColor = Color.FromArgb("#0D6EFD"), VerticalOptions = LayoutOptions.Center }); addButton.Children.Add(new Label { Text = buttonText, FontSize = 16, TextColor = Color.FromArgb("#0D6EFD"), VerticalOptions = LayoutOptions.Center }); Grid.SetColumn(addButton, 2); header.Children.Add(addButton); } return header; } private INavigation? ResolveNavigation() { // Попробуем найти Page, обойдя родителей Element? el = this; while (el != null) { if (el is Page p) return p.Navigation; el = el.Parent; } // Application.Current.MainPage может быть Shell или NavigationPage if (Application.Current?.MainPage?.Navigation != null) return Application.Current.MainPage.Navigation; // Shell.Current.Navigation if (Shell.Current?.Navigation != null) return Shell.Current.Navigation; // Попробуем взять первую открытую Window if (Application.Current?.Windows != null && Application.Current.Windows.Count > 0) return Application.Current.Windows[0].Page?.Navigation; return null; } }