diff --git a/.idea/.idea.CORP-MAUI/.idea/deviceManager.xml b/.idea/.idea.CORP-MAUI/.idea/deviceManager.xml
index 91f9558..eebfe8e 100644
--- a/.idea/.idea.CORP-MAUI/.idea/deviceManager.xml
+++ b/.idea/.idea.CORP-MAUI/.idea/deviceManager.xml
@@ -5,7 +5,7 @@
-
+
diff --git a/CORP-MAUI.sln.DotSettings.user b/CORP-MAUI.sln.DotSettings.user
index fcfc28a..6c09311 100644
--- a/CORP-MAUI.sln.DotSettings.user
+++ b/CORP-MAUI.sln.DotSettings.user
@@ -7,6 +7,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/CORP-MAUI/App.xaml.cs b/CORP-MAUI/App.xaml.cs
index 2646859..d8a94f0 100644
--- a/CORP-MAUI/App.xaml.cs
+++ b/CORP-MAUI/App.xaml.cs
@@ -1,4 +1,4 @@
-using CORP_MAUI.Pages;
+using CORP_MAUI.Pages;
using CORP_MAUI.Models;
using CORP_MAUI.Services;
using System.Diagnostics;
@@ -136,12 +136,30 @@ public partial class App : Application
// Продолжаем работу без WebSocket - это не критично
}
- // Переходим сразу на главную страницу (AppShell)
- Trace.WriteLine("[App] >>> Navigating to AppShell");
+ // Переходим сразу на главную страницу
+ // На Windows/Desktop используем DesktopMainPage, на мобильных - AppShell
+ Trace.WriteLine("[App] >>> Navigating to main page");
await MainThread.InvokeOnMainThreadAsync(() =>
{
+#if WINDOWS || MACCATALYST
+ // На десктопе проверяем ширину экрана
+ double screenWidth = DeviceDisplay.MainDisplayInfo.Width / DeviceDisplay.MainDisplayInfo.Density;
+ Trace.WriteLine($"[App] Screen width: {screenWidth}");
+
+ if (screenWidth >= 768)
+ {
+ window.Page = new DesktopMainPage();
+ Trace.WriteLine("[App] ✅ Navigation to DesktopMainPage completed");
+ }
+ else
+ {
+ window.Page = new AppShell();
+ Trace.WriteLine("[App] ✅ Navigation to AppShell completed");
+ }
+#else
window.Page = new AppShell();
Trace.WriteLine("[App] ✅ Navigation to AppShell completed");
+#endif
});
}
else
diff --git a/CORP-MAUI/AppShell.xaml b/CORP-MAUI/AppShell.xaml
index 2f5acfa..55015d3 100644
--- a/CORP-MAUI/AppShell.xaml
+++ b/CORP-MAUI/AppShell.xaml
@@ -7,8 +7,8 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
Shell.TabBarBackgroundColor="#0D6EFD"
Shell.TabBarForegroundColor="White"
- Shell.TabBarTitleColor="White"
- Shell.TabBarUnselectedColor="#FFFFFF99"
+ Shell.TabBarTitleColor="Transparent"
+ Shell.TabBarUnselectedColor="White"
FlyoutBackgroundColor="#0D6EFD"
FlyoutWidth="220"
Shell.NavBarIsVisible="False"
@@ -59,12 +59,14 @@
+
+
-
-
-
-
-
+
+
+
+
+
@@ -80,10 +82,18 @@
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/AppShell.xaml.cs b/CORP-MAUI/AppShell.xaml.cs
index b308f2a..187190b 100644
--- a/CORP-MAUI/AppShell.xaml.cs
+++ b/CORP-MAUI/AppShell.xaml.cs
@@ -8,12 +8,13 @@ public partial class AppShell : Shell
// Порог ширины для переключения на вертикальное меню (в пикселях)
private const double WideScreenThreshold = 768;
private bool _isDesktopMode;
+ private int _lastNonMenuTabIndex = 2; // По умолчанию - Лента
public AppShell()
{
try
{
- Trace.WriteLine("=== APPSHELL CONSTRUCTOR START ===");
+ Trace.WriteLine("=== APP SHELL CONSTRUCTOR START ===");
Trace.WriteLine("[AppShell] >>> Вызов InitializeComponent");
InitializeComponent();
@@ -25,19 +26,32 @@ public partial class AppShell : Shell
Routing.RegisterRoute(nameof(ChatPage), typeof(ChatPage));
Routing.RegisterRoute(nameof(DepartmentDetailsPage), typeof(DepartmentDetailsPage));
Routing.RegisterRoute(nameof(EmployeeProfilePage), typeof(EmployeeProfilePage));
+ Routing.RegisterRoute(nameof(MenuPage), typeof(MenuPage));
+ Routing.RegisterRoute(nameof(DepartmentsPage), typeof(DepartmentsPage));
+ Routing.RegisterRoute(nameof(ProfilePage), typeof(ProfilePage));
+ Routing.RegisterRoute(nameof(DocumentsPage), typeof(DocumentsPage));
+
+ // Подписываемся на навигацию для сохранения предыдущей вкладки
+ Navigating += OnShellNavigating;
Trace.WriteLine("[AppShell] ✅ AppShell инициализирован");
- Trace.WriteLine("=== APPSHELL CONSTRUCTOR END ===");
+ Trace.WriteLine("=== APP SHELL CONSTRUCTOR END ===");
// Устанавливаем начальный режим меню
UpdateLayoutMode();
+ // При запуске переключаемся на ленту (индекс 2), а не на меню (индекс 0)
+ if (MobileTabBar?.Items?.Count > 2)
+ {
+ CurrentItem = MobileTabBar.Items[2];
+ }
+
// Инициализация без ожидания (так await нельзя из синхронного конструктора)
_ = InitializeAsync();
}
catch (Exception ex)
{
- Trace.WriteLine("=== APPSHELL CONSTRUCTOR ERROR ===");
+ Trace.WriteLine("=== APP SHELL CONSTRUCTOR ERROR ===");
Trace.WriteLine($"[AppShell] ❌ Тип ошибки: {ex.GetType().FullName}");
Trace.WriteLine($"[AppShell] ❌ Сообщение: {ex.Message}");
Trace.WriteLine($"[AppShell] ❌ StackTrace: {ex.StackTrace}");
@@ -114,6 +128,31 @@ public partial class AppShell : Shell
}
}
+ ///
+ /// Сохраняет индекс текущей вкладки перед переходом на меню
+ ///
+ private void OnShellNavigating(object? sender, ShellNavigatingEventArgs e)
+ {
+ // Если переходим на MenuTabPage, сохраняем текущую вкладку
+ if (e.Target?.Location?.OriginalString?.Contains("MenuTab") == true)
+ {
+ // Сохраняем индекс текущей вкладки (не меню)
+ if (MobileTabBar != null)
+ {
+ for (int i = 1; i < MobileTabBar.Items.Count; i++)
+ {
+ if (MobileTabBar.Items[i] == MobileTabBar.CurrentItem)
+ {
+ _lastNonMenuTabIndex = i;
+ MenuPage.SetPreviousTabIndex(i);
+ Trace.WriteLine($"[AppShell] Сохранена предыдущая вкладка: {i}");
+ break;
+ }
+ }
+ }
+ }
+ }
+
protected override void OnSizeAllocated(double width, double height)
{
base.OnSizeAllocated(width, height);
@@ -130,20 +169,48 @@ public partial class AppShell : Shell
try
{
// Получаем текущую ширину окна
- double currentWidth = width ?? (Application.Current?.Windows.FirstOrDefault()?.Width ?? 0);
+ double currentWidth = width ?? 0;
+
+ // Если ширина не передана, пытаемся получить из разных источников
+ if (currentWidth <= 0)
+ {
+ // Пробуем получить из Window
+ var window = Application.Current?.Windows.FirstOrDefault();
+ if (window != null && window.Width > 0)
+ {
+ currentWidth = window.Width;
+ }
+ }
if (currentWidth <= 0)
{
- // Если ширина не определена, используем DeviceDisplay
- currentWidth = DeviceDisplay.MainDisplayInfo.Width / DeviceDisplay.MainDisplayInfo.Density;
+ // Пробуем получить из DeviceDisplay
+ var displayInfo = DeviceDisplay.MainDisplayInfo;
+ if (displayInfo.Width > 0 && displayInfo.Density > 0)
+ {
+ currentWidth = displayInfo.Width / displayInfo.Density;
+ }
+ }
+
+ // На Windows/Desktop по умолчанию считаем широким экраном если не удалось определить
+ if (currentWidth <= 0)
+ {
+#if WINDOWS || MACCATALYST
+ currentWidth = 1200; // Предполагаем десктоп
+#else
+ currentWidth = 400; // Предполагаем мобильное устройство
+#endif
}
bool shouldBeDesktop = currentWidth >= WideScreenThreshold;
Trace.WriteLine($"[AppShell] UpdateLayoutMode: width={currentWidth}, threshold={WideScreenThreshold}, shouldBeDesktop={shouldBeDesktop}, currentMode={_isDesktopMode}");
+ // При первом запуске (_isDesktopMode = false) или при изменении режима
+ bool isFirstRun = !_isDesktopMode && shouldBeDesktop;
+
// Проверяем, нужно ли менять режим
- if (shouldBeDesktop == _isDesktopMode)
+ if (shouldBeDesktop == _isDesktopMode && !isFirstRun)
{
return; // Режим не изменился
}
@@ -171,24 +238,29 @@ public partial class AppShell : Shell
{
try
{
- Trace.WriteLine("[AppShell] >>> Переключение на десктопный режим (Flyout)");
+ Trace.WriteLine("[AppShell] >>> Переключение на десктопный режим (DesktopLayout)");
// Скрываем TabBar
MobileTabBar.IsVisible = false;
- // Показываем FlyoutItems
- DesktopFlyoutNews.IsVisible = true;
- DesktopFlyoutEmployees.IsVisible = true;
- DesktopFlyoutDepartments.IsVisible = true;
- DesktopFlyoutRequests.IsVisible = true;
- DesktopFlyoutProfile.IsVisible = true;
+ // Скрываем FlyoutItems (не используем Flyout)
+ DesktopFlyoutNews.IsVisible = false;
+ DesktopFlyoutEmployees.IsVisible = false;
+ DesktopFlyoutDepartments.IsVisible = false;
+ DesktopFlyoutChats.IsVisible = false;
+ DesktopFlyoutRequests.IsVisible = false;
+
+ DesktopFlyoutDocuments.IsVisible = false;
+ DesktopFlyoutProfile.IsVisible = false;
- // Включаем Flyout в режим Locked (всегда видим)
- FlyoutBehavior = FlyoutBehavior.Locked;
- FlyoutIsPresented = true;
+ // Отключаем Flyout
+ FlyoutBehavior = FlyoutBehavior.Disabled;
- // Переходим на первый FlyoutItem
- CurrentItem = DesktopFlyoutNews;
+ // Переключаем MainPage на DesktopMainPage
+ if (Application.Current?.Windows.Count > 0)
+ {
+ Application.Current.Windows[0].Page = new DesktopMainPage();
+ }
Trace.WriteLine("[AppShell] ✅ Десктопный режим активирован");
}
@@ -204,11 +276,19 @@ public partial class AppShell : Shell
{
Trace.WriteLine("[AppShell] >>> Переключение на мобильный режим (TabBar)");
+ // Если текущая страница - DesktopMainPage, возвращаем AppShell
+ if (Application.Current?.Windows.Count > 0 && Application.Current.Windows[0].Page is DesktopMainPage)
+ {
+ Application.Current.Windows[0].Page = this;
+ }
+
// Скрываем FlyoutItems
DesktopFlyoutNews.IsVisible = false;
DesktopFlyoutEmployees.IsVisible = false;
DesktopFlyoutDepartments.IsVisible = false;
+ DesktopFlyoutChats.IsVisible = false;
DesktopFlyoutRequests.IsVisible = false;
+ DesktopFlyoutDocuments.IsVisible = false;
DesktopFlyoutProfile.IsVisible = false;
// Отключаем Flyout
diff --git a/CORP-MAUI/CORP-MAUI.csproj b/CORP-MAUI/CORP-MAUI.csproj
index 119b159..a648348 100644
--- a/CORP-MAUI/CORP-MAUI.csproj
+++ b/CORP-MAUI/CORP-MAUI.csproj
@@ -55,8 +55,7 @@
-
-
+
@@ -74,9 +73,5 @@
-
-
-
-
diff --git a/CORP-MAUI/Controls/DesktopCalendarPanel.xaml b/CORP-MAUI/Controls/DesktopCalendarPanel.xaml
new file mode 100644
index 0000000..7e44289
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopCalendarPanel.xaml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Controls/DesktopCalendarPanel.xaml.cs b/CORP-MAUI/Controls/DesktopCalendarPanel.xaml.cs
new file mode 100644
index 0000000..24ca8f3
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopCalendarPanel.xaml.cs
@@ -0,0 +1,101 @@
+using System.Globalization;
+using Microsoft.Maui.Controls.Shapes;
+
+namespace CORP_MAUI.Controls;
+
+public partial class DesktopCalendarPanel : ContentView
+{
+ private DateTime _currentMonth;
+ private static readonly CultureInfo RuCulture = new("ru-RU");
+
+ public DesktopCalendarPanel()
+ {
+ InitializeComponent();
+ _currentMonth = DateTime.Today;
+ UpdateCalendar();
+ }
+
+ private void UpdateCalendar()
+ {
+ MonthYearLabel.Text = _currentMonth.ToString("MMMM yyyy", RuCulture);
+ MonthYearLabel.Text = char.ToUpper(MonthYearLabel.Text[0]) + MonthYearLabel.Text[1..];
+ CalendarGrid.Children.Clear();
+
+ var firstDay = new DateTime(_currentMonth.Year, _currentMonth.Month, 1);
+ int startColumn = ((int)firstDay.DayOfWeek + 6) % 7;
+ int daysInMonth = DateTime.DaysInMonth(_currentMonth.Year, _currentMonth.Month);
+
+ int row = 0;
+ int col = startColumn;
+
+ for (int day = 1; day <= daysInMonth; day++)
+ {
+ var dayDate = new DateTime(_currentMonth.Year, _currentMonth.Month, day);
+ bool isToday = dayDate.Date == DateTime.Today;
+ bool isWeekend = col >= 5;
+
+ var dayLabel = new Label
+ {
+ Text = day.ToString(),
+ FontSize = 14,
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center,
+ TextColor = isToday ? Colors.White : (isWeekend ? Color.FromArgb("#0D6EFD") : Color.FromArgb("#333333"))
+ };
+
+ View dayView;
+ if (isToday)
+ {
+ var border = new Border
+ {
+ StrokeShape = new RoundRectangle { CornerRadius = 17 },
+ BackgroundColor = Color.FromArgb("#0D6EFD"),
+ HeightRequest = 34,
+ WidthRequest = 34,
+ Content = dayLabel
+ };
+ dayView = border;
+ }
+ else
+ {
+ dayView = dayLabel;
+ }
+
+ Grid.SetRow(dayView, row);
+ Grid.SetColumn(dayView, col);
+ CalendarGrid.Children.Add(dayView);
+
+ col++;
+ if (col > 6) { col = 0; row++; }
+ }
+
+ UpdateWeekRange();
+ }
+
+ private void UpdateWeekRange()
+ {
+ var today = DateTime.Today;
+ int diff = ((int)today.DayOfWeek + 6) % 7;
+ var weekStart = today.AddDays(-diff);
+ var weekEnd = weekStart.AddDays(6);
+ WeekRangeLabel.Text = $"{weekStart:d MMMM} – {weekEnd:d MMMM}";
+ }
+
+ private void OnPreviousMonthClicked(object? sender, EventArgs e)
+ {
+ _currentMonth = _currentMonth.AddMonths(-1);
+ UpdateCalendar();
+ }
+
+ private void OnNextMonthClicked(object? sender, EventArgs e)
+ {
+ _currentMonth = _currentMonth.AddMonths(1);
+ UpdateCalendar();
+ }
+
+ private void OnTodayClicked(object? sender, EventArgs e)
+ {
+ _currentMonth = DateTime.Today;
+ UpdateCalendar();
+ }
+}
diff --git a/CORP-MAUI/Controls/DesktopHeader.xaml b/CORP-MAUI/Controls/DesktopHeader.xaml
new file mode 100644
index 0000000..1e0ed3b
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopHeader.xaml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Controls/DesktopHeader.xaml.cs b/CORP-MAUI/Controls/DesktopHeader.xaml.cs
new file mode 100644
index 0000000..e01bb0a
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopHeader.xaml.cs
@@ -0,0 +1,42 @@
+namespace CORP_MAUI.Controls;
+
+public partial class DesktopHeader : ContentView
+{
+ public DesktopHeader()
+ {
+ InitializeComponent();
+ LoadUserAvatar();
+ }
+
+ private async void LoadUserAvatar()
+ {
+ try
+ {
+ var user = await ServiceProvider.ApiService.GetCurrentUserAsync();
+ if (user != null && !string.IsNullOrEmpty(user.Avatar))
+ {
+ // Если Avatar содержит base64 данные
+ if (user.Avatar.StartsWith("data:image"))
+ {
+ var base64Data = user.Avatar.Substring(user.Avatar.IndexOf(",") + 1);
+ UserAvatar.Source = ImageSource.FromStream(() =>
+ new MemoryStream(Convert.FromBase64String(base64Data)));
+ }
+ else
+ {
+ // Иначе это URL
+ UserAvatar.Source = user.Avatar;
+ }
+ }
+ }
+ catch
+ {
+ // Используем аватар по умолчанию
+ }
+ }
+
+ private async void OnProfileClicked(object sender, EventArgs e)
+ {
+ await Shell.Current.GoToAsync("//ProfilePage");
+ }
+}
diff --git a/CORP-MAUI/Controls/DesktopLayout.xaml b/CORP-MAUI/Controls/DesktopLayout.xaml
new file mode 100644
index 0000000..97baa86
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopLayout.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Controls/DesktopLayout.xaml.cs b/CORP-MAUI/Controls/DesktopLayout.xaml.cs
new file mode 100644
index 0000000..7bfae25
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopLayout.xaml.cs
@@ -0,0 +1,1189 @@
+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);
+
+ // Контейнер для контента
+ var content = new VerticalStackLayout
+ {
+ Spacing = 15
+ };
+ content.Children.Add(header);
+
+ // Здесь будет ScrollView с постами (загружается асинхронно)
+ var postsContainer = new VerticalStackLayout
+ {
+ Spacing = 15
+ };
+
+ // Placeholder пока загружаются посты
+ var loadingIndicator = new ActivityIndicator
+ {
+ IsRunning = true,
+ Color = Color.FromArgb("#0D6EFD"),
+ HorizontalOptions = LayoutOptions.Center,
+ Margin = new Thickness(0, 50)
+ };
+ postsContainer.Children.Add(loadingIndicator);
+
+ var scrollView = new ScrollView { Content = postsContainer };
+ content.Children.Add(scrollView);
+
+ // Загружаем посты асинхронно
+ _ = LoadPostsAsync(postsContainer);
+
+ return content;
+ }
+
+ 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);
+ 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
+ }
+ };
+
+ 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
+ };
+ 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
+ });
+
+ 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) });
+ content.Children.Add(new ScrollView { Content = container });
+
+ 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)
+ };
+
+ 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}");
+ await Shell.Current.Navigation.PushAsync(new CORP_MAUI.Pages.EmployeeProfilePage(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 };
+
+ // Добавляем 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);
+
+ 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 с 2 колонками
+ 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}]");
+ }
+
+ 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 };
+
+ // Сохраняем ссылку на контейнер аватарок в свойстве отдела для последующего обновления
+ 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}");
+ await Shell.Current.Navigation.PushAsync(new CORP_MAUI.Pages.DepartmentDetailsPage(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;
+ }
+}
diff --git a/CORP-MAUI/Controls/DesktopSidebar.xaml b/CORP-MAUI/Controls/DesktopSidebar.xaml
new file mode 100644
index 0000000..8cdf0b0
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopSidebar.xaml
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Controls/DesktopSidebar.xaml.cs b/CORP-MAUI/Controls/DesktopSidebar.xaml.cs
new file mode 100644
index 0000000..f5d9672
--- /dev/null
+++ b/CORP-MAUI/Controls/DesktopSidebar.xaml.cs
@@ -0,0 +1,100 @@
+namespace CORP_MAUI.Controls;
+
+public partial class DesktopSidebar : ContentView
+{
+ public event EventHandler? NavigationRequested;
+ private string _currentRoute = "News";
+ private readonly Color _activeBackground = Color.FromArgb("#E8F0FE");
+ private readonly Color _inactiveBackground = Colors.Transparent;
+
+ public DesktopSidebar()
+ {
+ InitializeComponent();
+ // По умолчанию выделяем "Новости"
+ UpdateActiveItem("News");
+ }
+
+ private void UpdateActiveItem(string route)
+ {
+ _currentRoute = route;
+
+ // Сбрасываем все фоны
+ ProfileItem.BackgroundColor = _inactiveBackground;
+ NewsItem.BackgroundColor = _inactiveBackground;
+ EmployeesItem.BackgroundColor = _inactiveBackground;
+ DepartmentsItem.BackgroundColor = _inactiveBackground;
+ RequestsItem.BackgroundColor = _inactiveBackground;
+ ChatsItem.BackgroundColor = _inactiveBackground;
+ DocumentsItem.BackgroundColor = _inactiveBackground;
+ FinanceItem.BackgroundColor = _inactiveBackground;
+ SettingsItem.BackgroundColor = _inactiveBackground;
+
+ // Выделяем активный пункт
+ switch (route)
+ {
+ case "Profile": ProfileItem.BackgroundColor = _activeBackground; break;
+ case "News": NewsItem.BackgroundColor = _activeBackground; break;
+ case "Employees": EmployeesItem.BackgroundColor = _activeBackground; break;
+ case "Departments": DepartmentsItem.BackgroundColor = _activeBackground; break;
+ case "Requests": RequestsItem.BackgroundColor = _activeBackground; break;
+ case "Chats": ChatsItem.BackgroundColor = _activeBackground; break;
+ case "Documents": DocumentsItem.BackgroundColor = _activeBackground; break;
+ case "Finance": FinanceItem.BackgroundColor = _activeBackground; break;
+ case "Settings": SettingsItem.BackgroundColor = _activeBackground; break;
+ }
+ }
+
+ private void OnProfileClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Profile");
+ NavigationRequested?.Invoke(this, "Profile");
+ }
+
+ private void OnNewsClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("News");
+ NavigationRequested?.Invoke(this, "News");
+ }
+
+ private void OnEmployeesClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Employees");
+ NavigationRequested?.Invoke(this, "Employees");
+ }
+
+ private void OnDepartmentsClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Departments");
+ NavigationRequested?.Invoke(this, "Departments");
+ }
+
+ private void OnRequestsClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Requests");
+ NavigationRequested?.Invoke(this, "Requests");
+ }
+
+ private void OnChatsClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Chats");
+ NavigationRequested?.Invoke(this, "Chats");
+ }
+
+ private void OnDocumentsClicked(object? sender, EventArgs e)
+ {
+ UpdateActiveItem("Documents");
+ NavigationRequested?.Invoke(this, "Documents");
+ }
+
+ private async void OnFinanceClicked(object? sender, EventArgs e)
+ {
+ if (Application.Current?.Windows.Count > 0)
+ await Application.Current.Windows[0].Page!.DisplayAlert("Финансы", "Раздел финансов находится в разработке", "OK");
+ }
+
+ private async void OnSettingsClicked(object? sender, EventArgs e)
+ {
+ if (Application.Current?.Windows.Count > 0)
+ await Application.Current.Windows[0].Page!.DisplayAlert("Настройки", "Раздел настроек находится в разработке", "OK");
+ }
+}
diff --git a/CORP-MAUI/MauiProgram.cs b/CORP-MAUI/MauiProgram.cs
index 263eb53..726da1a 100644
--- a/CORP-MAUI/MauiProgram.cs
+++ b/CORP-MAUI/MauiProgram.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using System.Diagnostics;
using CommunityToolkit.Maui;
using CORP_MAUI.Services;
using CORP_MAUI.Pages;
@@ -19,6 +19,7 @@ public static class MauiProgram
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
+
#if DEBUG
Trace.Listeners.Clear();
Trace.Listeners.Add(new DefaultTraceListener());
diff --git a/CORP-MAUI/Models/Department.cs b/CORP-MAUI/Models/Department.cs
index e570f01..32ef57d 100644
--- a/CORP-MAUI/Models/Department.cs
+++ b/CORP-MAUI/Models/Department.cs
@@ -17,6 +17,12 @@ public class Department
public User? Head { get; set; }
+ ///
+ /// Контейнер для аватарок сотрудников (используется в UI)
+ ///
+ [JsonIgnore]
+ public HorizontalStackLayout? AvatarsContainer { get; set; }
+
///
/// Описание для отображения (с fallback если пусто)
///
diff --git a/CORP-MAUI/Models/Document.cs b/CORP-MAUI/Models/Document.cs
index ccbd0e0..21d44c0 100644
--- a/CORP-MAUI/Models/Document.cs
+++ b/CORP-MAUI/Models/Document.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace CORP_MAUI.Models;
///
@@ -5,13 +7,138 @@ namespace CORP_MAUI.Models;
///
public class Document
{
+ [JsonPropertyName("id")]
public int Id { get; set; }
+
+ [JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
+
+ [JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
+
+ [JsonPropertyName("uploaded_by")]
+ public User? UploadedBy { get; set; }
+
+ [JsonPropertyName("uploaded_at")]
+ public DateTime UploadedAt { get; set; }
+
+ [JsonPropertyName("sent_to_all")]
+ public bool SentToAll { get; set; }
+
+ [JsonPropertyName("departments")]
+ public List