Add ChatsPage and Desktop layout components: Implement initial ChatsPage UI, update AppShell for navigation, and create DesktopHeader and DesktopSidebar for desktop layout.

This commit is contained in:
Данил Ярош 2026-01-21 14:49:26 +03:00
parent 293179b3c0
commit c1de303c22
45 changed files with 3825 additions and 290 deletions

View File

@ -5,7 +5,7 @@
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
<option name="order" value="DESCENDING" />
</ColumnSorterState>
</list>
</option>

View File

@ -7,6 +7,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbd1d5c50194fea68ff3559c160230b0ab50f5acf4ce3061bffd6d62958e2182_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMauiWinUIApplication_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Feb4628845efd624a4bdad5d73b53774c81232eee27ca2aa293d869e5e566_003FMauiWinUIApplication_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObject_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F9a6b1457cbcf17db31a383ba49ef9bcc786cf3ef77146d997eee499b27a46d_003FObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APages_005FChatsPage_002Examl_002Esg_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F663367c3d62bfbf0f5a54e78bb74ef48b661e1_003FPages_005FChatsPage_002Examl_002Esg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APages_005FProfilePage_002Examl_002Esg_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F2dc829dd3f62e98b28b779bb99a37aa7f9c521_003FPages_005FProfilePage_002Examl_002Esg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APages_005FProfilePage_002Examl_002Esg_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F72277aaebd0209ffbfec32fae65b113854609f_003FPages_005FProfilePage_002Examl_002Esg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStaticResourceExtension_002Ecs_002Fl_003AC_0021_003FUsers_003F_041D_0430_0442_0430_043B_044C_044F_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fc0ecf84680bdd51716cf1bd5ed45e7dc5099b08ef4398c534d8192a1146_003FStaticResourceExtension_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>

View File

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

View File

@ -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 @@
</Shell.ItemTemplate>
<!-- TabBar для узких экранов (мобильные устройства) -->
<!-- Порядок: Меню, Сообщения, Лента, Заявления, Сотрудники -->
<!-- Иконки белые на синем фоне, без подписей -->
<TabBar x:Name="MobileTabBar">
<ShellContent Title="Новости" Icon="feed.png" ContentTemplate="{DataTemplate pages:PostsPage}" />
<ShellContent Title="Сотрудники" Icon="employees.png" ContentTemplate="{DataTemplate pages:EmployeesPage}" />
<ShellContent Title="Отделы" Icon="department.png" ContentTemplate="{DataTemplate pages:DepartmentsPage}" />
<ShellContent Title="Заявления" Icon="statements.png" ContentTemplate="{DataTemplate pages:RequestsPage}" />
<ShellContent Title="Профиль" Icon="profile.png" ContentTemplate="{DataTemplate pages:ProfilePage}" />
<ShellContent Icon="menu.svg" Route="MenuTab" ContentTemplate="{DataTemplate pages:MenuTabPage}" />
<ShellContent Icon="chats.svg" ContentTemplate="{DataTemplate pages:ChatsPage}" />
<ShellContent Icon="feed.svg" ContentTemplate="{DataTemplate pages:PostsPage}" />
<ShellContent Icon="statements.svg" ContentTemplate="{DataTemplate pages:RequestsPage}" />
<ShellContent Icon="employees.svg" ContentTemplate="{DataTemplate pages:EmployeesPage}" />
</TabBar>
<!-- FlyoutItems для широких экранов (скрыт по умолчанию, управляется программно) -->
@ -80,10 +82,18 @@
<ShellContent ContentTemplate="{DataTemplate pages:DepartmentsPage}" />
</FlyoutItem>
<FlyoutItem x:Name="DesktopFlyoutChats" Title="Чаты" Icon="chats.svg" FlyoutDisplayOptions="AsSingleItem" IsVisible="False">
<ShellContent ContentTemplate="{DataTemplate pages:ChatsPage}" />
</FlyoutItem>
<FlyoutItem x:Name="DesktopFlyoutRequests" Title="Заявления" Icon="statements.png" FlyoutDisplayOptions="AsSingleItem" IsVisible="False">
<ShellContent ContentTemplate="{DataTemplate pages:RequestsPage}" />
</FlyoutItem>
<FlyoutItem x:Name="DesktopFlyoutDocuments" Title="Документы" Icon="documents.png" FlyoutDisplayOptions="AsSingleItem" IsVisible="False">
<ShellContent ContentTemplate="{DataTemplate pages:DocumentsPage}" />
</FlyoutItem>
<FlyoutItem x:Name="DesktopFlyoutProfile" Title="Профиль" Icon="profile.png" FlyoutDisplayOptions="AsSingleItem" IsVisible="False">
<ShellContent ContentTemplate="{DataTemplate pages:ProfilePage}" />
</FlyoutItem>

View File

@ -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
}
}
/// <summary>
/// Сохраняет индекс текущей вкладки перед переходом на меню
/// </summary>
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

View File

@ -55,8 +55,7 @@
<!-- Splash Screen -->
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Include="Resources\Images\Icons\SVG\*" />
<MauiImage Include="Resources\Images\**\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Custom Fonts -->
@ -74,9 +73,5 @@
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\Images\Icons\SVG\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Controls.DesktopCalendarPanel"
WidthRequest="280">
<!-- Правая панель с календарём -->
<Border BackgroundColor="White" Stroke="Transparent" Padding="15">
<ScrollView>
<VerticalStackLayout Spacing="15">
<!-- Заголовок -->
<Grid ColumnDefinitions="*,Auto">
<Label Grid.Column="0" Text="Календарь" FontSize="18" FontAttributes="Bold" TextColor="#333333"/>
<HorizontalStackLayout Grid.Column="1" Spacing="5">
<Label Text="Событие" TextColor="#0D6EFD" FontSize="14" VerticalOptions="Center"/>
<Border StrokeShape="RoundRectangle 12" BackgroundColor="#0D6EFD"
HeightRequest="24" WidthRequest="24">
<Label Text="+" TextColor="White" FontSize="16"
HorizontalOptions="Center" VerticalOptions="Center"/>
</Border>
</HorizontalStackLayout>
</Grid>
<!-- Фильтр -->
<HorizontalStackLayout Spacing="5">
<Label Text="Компания" TextColor="#666666" FontSize="13"/>
<Label Text="▼" TextColor="#666666" FontSize="10"/>
</HorizontalStackLayout>
<!-- Навигация по месяцам -->
<Grid ColumnDefinitions="Auto,*,Auto">
<Label Grid.Column="0" Text="‹" FontSize="20" TextColor="#0D6EFD">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnPreviousMonthClicked"/>
</Label.GestureRecognizers>
</Label>
<Label x:Name="MonthYearLabel" Grid.Column="1" Text="Январь 2026"
FontSize="16" FontAttributes="Bold" TextColor="#333333"
HorizontalOptions="Center"/>
<Label Grid.Column="2" Text="›" FontSize="20" TextColor="#0D6EFD">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnNextMonthClicked"/>
</Label.GestureRecognizers>
</Label>
<Border Grid.Column="2" StrokeShape="RoundRectangle 15"
BackgroundColor="#0D6EFD" Padding="12,6" Margin="20,0,0,0">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnTodayClicked"/>
</Border.GestureRecognizers>
<Label Text="Сегодня" TextColor="White" FontSize="12"/>
</Border>
</Grid>
<!-- Дни недели -->
<Grid ColumnDefinitions="*,*,*,*,*,*,*" ColumnSpacing="2">
<Label Grid.Column="0" Text="пн" TextColor="#999999" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="1" Text="вт" TextColor="#999999" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="2" Text="ср" TextColor="#999999" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="3" Text="чт" TextColor="#999999" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="4" Text="пт" TextColor="#999999" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="5" Text="сб" TextColor="#0D6EFD" FontSize="12" HorizontalOptions="Center"/>
<Label Grid.Column="6" Text="вс" TextColor="#0D6EFD" FontSize="12" HorizontalOptions="Center"/>
</Grid>
<!-- Сетка календаря -->
<Grid x:Name="CalendarGrid" RowDefinitions="35,35,35,35,35,35"
ColumnDefinitions="*,*,*,*,*,*,*" RowSpacing="2" ColumnSpacing="2">
<!-- Дни будут добавлены программно -->
</Grid>
<!-- Разделитель -->
<BoxView HeightRequest="1" BackgroundColor="#EEEEEE" Margin="0,10"/>
<!-- События на неделе -->
<Grid ColumnDefinitions="*,Auto">
<Label Grid.Column="0" Text="На этой неделе" TextColor="#666666" FontSize="13"/>
<Label x:Name="WeekRangeLabel" Grid.Column="1" Text="19 января – 25 января"
TextColor="#666666" FontSize="13"/>
</Grid>
<!-- Список событий -->
<VerticalStackLayout x:Name="EventsList" Spacing="8">
<!-- Пример события -->
<HorizontalStackLayout Spacing="8">
<Label Text="🎂" FontSize="14"/>
<Label Text="День рождения Ивана Иванова" TextColor="#333333" FontSize="13"/>
</HorizontalStackLayout>
</VerticalStackLayout>
</VerticalStackLayout>
</ScrollView>
</Border>
</ContentView>

View File

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

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Controls.DesktopHeader">
<!-- Верхняя панель для десктопа -->
<Grid BackgroundColor="#0D6EFD" HeightRequest="56" Padding="20,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Логотип слева -->
<HorizontalStackLayout Grid.Column="0" Spacing="10" VerticalOptions="Center">
<Image Source="logo.png" HeightRequest="32" WidthRequest="32"/>
<Label Text="HiRo" TextColor="White" FontSize="20" FontAttributes="Bold" VerticalOptions="Center"/>
</HorizontalStackLayout>
<!-- Поисковая строка по центру -->
<Border Grid.Column="1"
StrokeShape="RoundRectangle 20"
BackgroundColor="#3D85FD"
Stroke="Transparent"
Padding="15,0"
HeightRequest="40"
MaximumWidthRequest="500"
HorizontalOptions="Center"
VerticalOptions="Center">
<Grid ColumnDefinitions="Auto,*">
<Image Grid.Column="0" Source="search" HeightRequest="18" WidthRequest="18" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
<Entry Grid.Column="1"
Placeholder="Поиск по названию, автору или..."
PlaceholderColor="#CCDDFF"
TextColor="White"
BackgroundColor="Transparent"
Margin="10,0,0,0"
VerticalOptions="Center"/>
</Grid>
</Border>
<!-- Иконки справа -->
<HorizontalStackLayout Grid.Column="2" Spacing="15" VerticalOptions="Center">
<!-- Уведомления -->
<Grid>
<Border StrokeShape="RoundRectangle 20"
BackgroundColor="Transparent"
HeightRequest="40"
WidthRequest="40"
Stroke="Transparent">
<Image Source="notifications" HeightRequest="24" WidthRequest="24"
HorizontalOptions="Center" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<!-- Бейдж с количеством -->
<Border StrokeShape="RoundRectangle 10"
BackgroundColor="#FF4444"
HeightRequest="18"
WidthRequest="18"
HorizontalOptions="End"
VerticalOptions="Start"
Margin="0,-2,-2,0">
<Label Text="1" TextColor="White" FontSize="10"
HorizontalOptions="Center" VerticalOptions="Center"/>
</Border>
</Grid>
<!-- Аватар пользователя -->
<Border x:Name="UserAvatarBorder"
StrokeShape="RoundRectangle 20"
HeightRequest="40"
WidthRequest="40"
BackgroundColor="#E0E0E0"
Stroke="White"
StrokeThickness="2">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnProfileClicked"/>
</Border.GestureRecognizers>
<Image x:Name="UserAvatar"
Source="default_avatar.png"
Aspect="AspectFill"/>
</Border>
</HorizontalStackLayout>
</Grid>
</ContentView>

View File

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

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:CORP_MAUI.Controls"
x:Class="CORP_MAUI.Controls.DesktopLayout">
<!-- Основной layout для десктопа: Header + (Sidebar | Content | Calendar) -->
<Grid RowDefinitions="56,*" BackgroundColor="#F5F5F5">
<!-- Header -->
<controls:DesktopHeader Grid.Row="0" x:Name="Header"/>
<!-- Основной контент -->
<Grid Grid.Row="1" ColumnDefinitions="200,*,280">
<!-- Левый Sidebar -->
<controls:DesktopSidebar Grid.Column="0" x:Name="Sidebar"/>
<!-- Центральный контент (сюда встраивается страница) -->
<ContentView Grid.Column="1" x:Name="MainContent" Padding="20" BackgroundColor="#F5F5F5"/>
<!-- Правая панель с календарём -->
<controls:DesktopCalendarPanel Grid.Column="2" x:Name="CalendarPanel"/>
</Grid>
</Grid>
</ContentView>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Controls.DesktopSidebar"
WidthRequest="200">
<!-- Левая боковая панель навигации -->
<Border BackgroundColor="White" Stroke="Transparent">
<ScrollView>
<VerticalStackLayout Padding="10,20" Spacing="2">
<!-- Профиль -->
<Border x:Name="ProfileItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnProfileClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="profile" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Профиль" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Новости -->
<Border x:Name="NewsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnNewsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="feed" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Новости" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Сотрудники -->
<Border x:Name="EmployeesItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnEmployeesClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="employees" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Сотрудники" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Отделы -->
<Border x:Name="DepartmentsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDepartmentsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="department" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Отделы" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Заявления -->
<Border x:Name="RequestsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnRequestsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="statements" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Заявления" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Чаты -->
<Border x:Name="ChatsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChatsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="chats" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Чаты" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Документы -->
<Border x:Name="DocumentsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDocumentsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="documents" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Документы" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Финансы -->
<Border x:Name="FinanceItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFinanceClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="finances" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Финансы" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
<!-- Настройки -->
<Border x:Name="SettingsItem" StrokeShape="RoundRectangle 8" Stroke="Transparent" BackgroundColor="Transparent" Padding="12,10">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnSettingsClicked"/>
</Border.GestureRecognizers>
<HorizontalStackLayout Spacing="12">
<Image Source="settings" HeightRequest="20" WidthRequest="20">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
<Label Text="Настройки" TextColor="#333333" FontSize="15" VerticalOptions="Center"/>
</HorizontalStackLayout>
</Border>
</VerticalStackLayout>
</ScrollView>
</Border>
</ContentView>

View File

@ -0,0 +1,100 @@
namespace CORP_MAUI.Controls;
public partial class DesktopSidebar : ContentView
{
public event EventHandler<string>? 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");
}
}

View File

@ -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());

View File

@ -17,6 +17,12 @@ public class Department
public User? Head { get; set; }
/// <summary>
/// Контейнер для аватарок сотрудников (используется в UI)
/// </summary>
[JsonIgnore]
public HorizontalStackLayout? AvatarsContainer { get; set; }
/// <summary>
/// Описание для отображения (с fallback если пусто)
/// </summary>

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace CORP_MAUI.Models;
/// <summary>
@ -5,13 +7,138 @@ namespace CORP_MAUI.Models;
/// </summary>
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<object>? Departments { get; set; }
[JsonPropertyName("recipients")]
public List<object>? Recipients { get; set; }
[JsonPropertyName("file_url")]
public string FileUrl { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public User? Author { get; set; }
public string Category { get; set; } = string.Empty;
[JsonPropertyName("is_acknowledged")]
public bool IsAcknowledged { get; set; }
// Вычисляемые свойства для отображения
/// <summary>
/// Имя загрузившего документ
/// </summary>
[JsonIgnore]
public string UploaderName => UploadedBy?.FullName ?? "Неизвестный";
/// <summary>
/// URL аватара загрузившего
/// </summary>
[JsonIgnore]
public string UploaderAvatarUrl => UploadedBy?.Avatar ?? UploadedBy?.AvatarUrl ?? string.Empty;
/// <summary>
/// Форматированная дата загрузки
/// </summary>
[JsonIgnore]
public string DisplayUploadedAt => UploadedAt.ToString("dd.MM.yyyy HH:mm");
/// <summary>
/// Расширение файла (для иконки)
/// </summary>
[JsonIgnore]
public string FileExtension
{
get
{
if (string.IsNullOrEmpty(FileUrl)) return string.Empty;
try
{
// Декодируем URL и получаем расширение
string decodedUrl = Uri.UnescapeDataString(FileUrl);
int lastDot = decodedUrl.LastIndexOf('.');
if (lastDot > 0 && lastDot < decodedUrl.Length - 1)
{
return decodedUrl.Substring(lastDot + 1).ToLower();
}
}
catch
{
// Игнорируем ошибки декодирования
}
return string.Empty;
}
}
/// <summary>
/// Иконка типа документа (эмодзи)
/// </summary>
[JsonIgnore]
public string FileIcon
{
get
{
return FileExtension switch
{
"pdf" => "📕",
"doc" or "docx" => "📘",
"xls" or "xlsx" => "📗",
"ppt" or "pptx" => "📙",
"txt" => "📄",
"jpg" or "jpeg" or "png" or "gif" or "webp" => "🖼️",
"zip" or "rar" or "7z" => "📦",
_ => "📄"
};
}
}
/// <summary>
/// Цвет статуса ознакомления
/// </summary>
[JsonIgnore]
public Color AcknowledgedColor => IsAcknowledged
? Color.FromArgb("#28A745") // Зеленый
: Color.FromArgb("#FFC107"); // Желтый
/// <summary>
/// Текст статуса ознакомления
/// </summary>
[JsonIgnore]
public string AcknowledgedText => IsAcknowledged ? "Ознакомлен" : "Не ознакомлен";
/// <summary>
/// Иконка статуса ознакомления
/// </summary>
[JsonIgnore]
public string AcknowledgedIcon => IsAcknowledged ? "✅" : "⏳";
/// <summary>
/// Короткое описание файла
/// </summary>
[JsonIgnore]
public string ShortDescription
{
get
{
if (!string.IsNullOrEmpty(Description))
{
return Description.Length > 100 ? Description.Substring(0, 100) + "..." : Description;
}
return SentToAll ? "Для всех сотрудников" : "Персональный документ";
}
}
}

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.ChatPage"
Title="Чат"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,*,Auto">

View File

@ -0,0 +1,79 @@
<?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"
x:Class="CORP_MAUI.Pages.ChatsPage"
Title="Чаты"
BackgroundColor="White">
<Grid RowDefinitions="Auto,*">
<!-- Заголовок страницы -->
<Border Grid.Row="0"
BackgroundColor="{StaticResource Primary}"
Padding="16"
StrokeThickness="0">
<Label Text="Чаты"
TextColor="White"
FontSize="20"
FontAttributes="Bold"
HorizontalOptions="Start"
VerticalOptions="Center"/>
</Border>
<!-- Контент-заглушка -->
<VerticalStackLayout Grid.Row="1"
VerticalOptions="Center"
HorizontalOptions="Center"
Spacing="24"
Padding="32">
<!-- Иконка -->
<Border WidthRequest="120"
HeightRequest="120"
StrokeShape="RoundRectangle 60"
BackgroundColor="{StaticResource Primary}"
Opacity="0.15"
HorizontalOptions="Center">
<Image Source="chats"
WidthRequest="60"
HeightRequest="60"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</Border>
<!-- Заголовок -->
<Label Text="Чаты скоро появятся"
FontSize="24"
FontAttributes="Bold"
TextColor="{StaticResource Gray900}"
HorizontalTextAlignment="Center"/>
<!-- Описание -->
<Label Text="Функционал чатов находится в разработке. Скоро вы сможете общаться с коллегами прямо в приложении."
FontSize="14"
TextColor="{StaticResource Gray600}"
HorizontalTextAlignment="Center"
MaximumWidthRequest="300"
LineHeight="1.4"/>
<!-- Дополнительная информация -->
<Border BackgroundColor="#E8F4FD"
StrokeShape="RoundRectangle 12"
Padding="16"
StrokeThickness="0"
Margin="0,16,0,0">
<HorizontalStackLayout Spacing="12">
<Label Text="💡"
FontSize="20"
VerticalOptions="Center"/>
<Label Text="Пока что вы можете открыть чат с конкретным сотрудником через его профиль"
FontSize="13"
TextColor="{StaticResource Primary}"
MaximumWidthRequest="250"
LineHeight="1.3"/>
</HorizontalStackLayout>
</Border>
</VerticalStackLayout>
</Grid>
</ContentPage>

View File

@ -0,0 +1,9 @@
namespace CORP_MAUI.Pages;
public partial class ChatsPage : ContentPage
{
public ChatsPage()
{
InitializeComponent();
}
}

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.DepartmentDetailsPage"
Title="Информация об отделе"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsVisible="True" TextOverride="Назад" />

View File

@ -129,7 +129,20 @@ public partial class DepartmentDetailsPage : ContentPage
/// </summary>
private async void OnTeamMemberCardTapped(object sender, TappedEventArgs e)
{
if (e.Parameter is User selectedUser)
User? selectedUser = null;
// Попробуем получить параметр из TappedEventArgs
if (e.Parameter is User paramUser)
{
selectedUser = paramUser;
}
// Иначе попробуем получить из BindingContext отправителя
else if (sender is BindableObject bindable && bindable.BindingContext is User contextUser)
{
selectedUser = contextUser;
}
if (selectedUser != null)
{
Trace.WriteLine($"[DepartmentDetailsPage] >>> Открытие профиля: {selectedUser.FullName}");
@ -143,6 +156,10 @@ public partial class DepartmentDetailsPage : ContentPage
await DisplayAlert("Ошибка", "Не удалось открыть профиль сотрудника", "OK");
}
}
else
{
Trace.WriteLine("[DepartmentDetailsPage] ⚠️ Не удалось определить выбранного сотрудника");
}
}
/// <summary>
@ -150,7 +167,27 @@ public partial class DepartmentDetailsPage : ContentPage
/// </summary>
private async void OnChatIconTapped(object sender, TappedEventArgs e)
{
if (e.Parameter is User selectedUser)
User? selectedUser = null;
// Попробуем получить параметр из TappedEventArgs
if (e.Parameter is User paramUser)
{
selectedUser = paramUser;
}
// Иначе попробуем получить из BindingContext отправителя или родительского элемента
else if (sender is BindableObject bindable)
{
if (bindable.BindingContext is User contextUser)
{
selectedUser = contextUser;
}
else if (bindable is View view && view.Parent is View parent && parent.BindingContext is User parentUser)
{
selectedUser = parentUser;
}
}
if (selectedUser != null)
{
Trace.WriteLine($"[DepartmentDetailsPage] >>> Открытие чата с: {selectedUser.FullName}");
@ -164,6 +201,10 @@ public partial class DepartmentDetailsPage : ContentPage
await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
}
}
else
{
Trace.WriteLine("[DepartmentDetailsPage] ⚠️ Не удалось определить пользователя для чата");
}
}
private async Task PreloadAvatarsAsync()

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.DepartmentsPage"
Title="Отделы"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,*">
@ -87,98 +87,19 @@
IsRefreshing="False"
Refreshing="OnRefreshing">
<CollectionView
x:Name="DepartmentsCollectionView"
SelectionMode="Single"
SelectionChanged="OnDepartmentSelected"
BackgroundColor="Transparent">
<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:Department">
<Border
Style="{StaticResource CardBorder}"
Margin="10,5">
<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">
<Image Source="employees.png" WidthRequest="16" HeightRequest="16" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="{StaticResource Primary}"/>
</Image.Behaviors>
</Image>
<Label Text="{Binding EmployeesCount}" Style="{StaticResource CardMeta}"/>
</HorizontalStackLayout>
<!-- Руководитель -->
<HorizontalStackLayout Spacing="5">
<Image Source="profile.png" WidthRequest="16" HeightRequest="16" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="{StaticResource Primary}"/>
</Image.Behaviors>
</Image>
<Label
Text="{Binding Head.FullName}"
Style="{StaticResource CardMeta}"
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>
<ScrollView
Orientation="Vertical"
HorizontalScrollBarVisibility="Never">
<FlexLayout
x:Name="DepartmentsFlexLayout"
Direction="Row"
Wrap="Wrap"
JustifyContent="Start"
AlignItems="Start"
AlignContent="Start"
Padding="10,5">
</FlexLayout>
</ScrollView>
</RefreshView>
<!-- Индикатор загрузки -->

View File

@ -1,12 +1,12 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using CORP_MAUI.Models;
using CommunityToolkit.Maui.Behaviors;
using Microsoft.Maui.Controls.Shapes;
namespace CORP_MAUI.Pages;
public partial class DepartmentsPage : ContentPage
{
private readonly ObservableCollection<Department> _departments;
private List<Department> _allDepartments = new();
public DepartmentsPage()
@ -14,43 +14,13 @@ public partial class DepartmentsPage : ContentPage
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");
}
}
});
Trace.WriteLine($"[DepartmentsPage] ❌ Ошибка: {ex.Message}");
throw;
}
}
@ -59,51 +29,14 @@ public partial class DepartmentsPage : ContentPage
{
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] ✅ Отделы загружены успешно");
RenderDepartments(_allDepartments.OrderBy(d => d.Name).ToList());
}
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
@ -114,61 +47,241 @@ public partial class DepartmentsPage : ContentPage
}
}
private void RenderDepartments(List<Department> departments)
{
DepartmentsFlexLayout.Children.Clear();
if (departments.Count == 0)
{
var emptyLabel = new Label
{
Text = "Нет отделов",
HorizontalOptions = LayoutOptions.Center,
VerticalOptions = LayoutOptions.Center,
Margin = new Thickness(20)
};
emptyLabel.SetAppThemeColor(Label.TextColorProperty, Color.FromArgb("#1F1F1F"), Color.FromArgb("#E1E1E1"));
DepartmentsFlexLayout.Children.Add(emptyLabel);
return;
}
double screenWidth = DeviceDisplay.MainDisplayInfo.Width / DeviceDisplay.MainDisplayInfo.Density;
// Для мобильных устройств (< 600px) - одна карточка в ряд, иначе две
double cardWidth = screenWidth < 600 ? screenWidth - 30 : (screenWidth / 2) - 25;
foreach (var department in departments)
{
var card = CreateDepartmentCard(department, cardWidth);
DepartmentsFlexLayout.Children.Add(card);
}
_ = LoadDepartmentAvatarsAsync(departments);
}
private Border CreateDepartmentCard(Department department, double cardWidth)
{
var avatarStack = new HorizontalStackLayout
{
Spacing = -8,
Margin = new Thickness(0, 5, 0, 0),
HeightRequest = 32,
MinimumHeightRequest = 32
};
var card = new Border
{
StrokeShape = new RoundRectangle { CornerRadius = 16 },
BackgroundColor = Colors.White,
Stroke = Colors.Transparent,
Padding = new Thickness(16),
Margin = new Thickness(5),
WidthRequest = cardWidth,
HeightRequest = 150,
MaximumHeightRequest = 150,
Content = new VerticalStackLayout
{
Spacing = 6,
Children =
{
new Label
{
Text = department.Name,
FontSize = 15,
FontAttributes = FontAttributes.Bold,
TextColor = Color.FromArgb("#1F1F1F"),
LineBreakMode = LineBreakMode.TailTruncation,
MaxLines = 1
},
new Label
{
Text = department.DisplayDescription,
FontSize = 12,
TextColor = Color.FromArgb("#6B6B6B"),
LineBreakMode = LineBreakMode.TailTruncation,
MaxLines = 2
},
new HorizontalStackLayout
{
Spacing = 5,
Margin = new Thickness(0, 3, 0, 0),
Children =
{
CreateIconWithText("employees.png", department.EmployeesCount.ToString()),
new Label
{
Text = "•",
FontSize = 12,
TextColor = Color.FromArgb("#999999"),
VerticalOptions = LayoutOptions.Center
},
CreateIconWithText("profile.png", department.Head?.FullName ?? "Не назначен")
}
},
avatarStack
}
}
};
card.Shadow = new Shadow
{
Brush = new SolidColorBrush(Color.FromArgb("#40000000")),
Offset = new Point(0, 2),
Radius = 8,
Opacity = 0.15f
};
var tapGesture = new TapGestureRecognizer();
tapGesture.Tapped += async (_, _) =>
{
try
{
await Navigation.PushAsync(new DepartmentDetailsPage(department.Id));
}
catch
{
await DisplayAlert("Ошибка", "Не удалось открыть страницу отдела", "OK");
}
};
card.GestureRecognizers.Add(tapGesture);
card.ClassId = department.Id.ToString();
return card;
}
private HorizontalStackLayout CreateIconWithText(string iconSource, string text)
{
var image = new Image
{
Source = iconSource,
WidthRequest = 14,
HeightRequest = 14,
VerticalOptions = LayoutOptions.Center
};
var iconBehavior = new IconTintColorBehavior
{
TintColor = Color.FromArgb("#007AFF")
};
image.Behaviors.Add(iconBehavior);
return new HorizontalStackLayout
{
Spacing = 4,
Children =
{
image,
new Label
{
Text = text,
FontSize = 12,
TextColor = Color.FromArgb("#6B6B6B"),
VerticalOptions = LayoutOptions.Center,
LineBreakMode = LineBreakMode.TailTruncation
}
}
};
}
private async Task LoadDepartmentAvatarsAsync(List<Department> departments)
{
try
{
foreach (var dept in departments)
{
if (dept.Head?.Id == null)
continue;
var card = DepartmentsFlexLayout.Children
.OfType<Border>()
.FirstOrDefault(b => b.ClassId == dept.Id.ToString());
if (card?.Content is VerticalStackLayout vstack)
{
var avatarStack = vstack.Children.OfType<HorizontalStackLayout>().LastOrDefault();
if (avatarStack != null)
{
try
{
var user = await ServiceProvider.ApiService.GetUserByIdAsync(dept.Head.Id);
if (!string.IsNullOrEmpty(user?.AvatarUrl))
{
MainThread.BeginInvokeOnMainThread(() =>
{
avatarStack.Children.Clear();
var avatarBorder = new Border
{
StrokeShape = new RoundRectangle { CornerRadius = 15 },
WidthRequest = 30,
HeightRequest = 30,
StrokeThickness = 2,
Stroke = Colors.White,
Content = new Image
{
Source = user.AvatarUrl,
Aspect = Aspect.AspectFill
}
};
avatarStack.Children.Add(avatarBorder);
});
}
}
catch (Exception)
{
// Игнорируем ошибки загрузки отдельных аватаров
}
}
}
}
}
catch (Exception ex)
{
Trace.WriteLine($"[DepartmentsPage] ⚠️ Ошибка загрузки аватаров: {ex.Message}");
}
}
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;
List<Department> filtered;
if (string.IsNullOrWhiteSpace(searchText))
{
// Показываем все отделы
_departments.Clear();
foreach (Department department in _allDepartments.OrderBy(d => d.Name))
{
_departments.Add(department);
}
filtered = _allDepartments;
}
else
{
// Фильтруем отделы
List<Department> filtered = _allDepartments.Where(d =>
filtered = _allDepartments.Where(d =>
d.Name.ToLower().Contains(searchText) ||
d.Description.ToLower().Contains(searchText) ||
(d.Description?.ToLower().Contains(searchText) ?? false) ||
(d.Head?.FullName.ToLower().Contains(searchText) ?? false)
).ToList();
_departments.Clear();
foreach (Department department in filtered.OrderBy(d => d.Name))
{
_departments.Add(department);
}
}
RenderDepartments(filtered.OrderBy(d => d.Name).ToList());
}
}

View File

@ -0,0 +1,12 @@
<?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:controls="clr-namespace:CORP_MAUI.Controls"
x:Class="CORP_MAUI.Pages.DesktopMainPage"
Shell.NavBarIsVisible="False"
Shell.TabBarIsVisible="False"
BackgroundColor="#F5F5F5">
<controls:DesktopLayout x:Name="DesktopLayout"/>
</ContentPage>

View File

@ -0,0 +1,21 @@
using CORP_MAUI.Controls;
namespace CORP_MAUI.Pages;
public partial class DesktopMainPage : ContentPage
{
public DesktopMainPage()
{
InitializeComponent();
}
public DesktopMainPage(View content) : this()
{
DesktopLayout.SetContent(content);
}
public void SetContent(View content)
{
DesktopLayout.SetContent(content);
}
}

View File

@ -0,0 +1,268 @@
<?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"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.DocumentsPage"
Title="Документы"
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,Auto,*">
<!-- Шапка -->
<Grid
Grid.Row="0"
ColumnDefinitions="50,*,50"
Padding="15,10"
BackgroundColor="White">
<!-- Кнопка назад (для навигации) -->
<ImageButton
x:Name="BackButton"
Grid.Column="0"
Source="arrow_back.png"
WidthRequest="40"
HeightRequest="40"
BackgroundColor="Transparent"
Clicked="OnBackClicked"
IsVisible="False">
<ImageButton.Behaviors>
<toolkit:IconTintColorBehavior TintColor="{StaticResource Primary}"/>
</ImageButton.Behaviors>
</ImageButton>
<!-- Заголовок -->
<Label
Grid.Column="1"
Text="Документы"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource Gray900}"
HorizontalOptions="Center"
VerticalOptions="Center"/>
<!-- Пустое место справа -->
<BoxView Grid.Column="2" Color="Transparent"/>
</Grid>
<!-- Поиск с серым фоном -->
<Grid
Grid.Row="1"
ColumnDefinitions="Auto,*"
Padding="15,5,15,10"
BackgroundColor="White">
<!-- Круглая кнопка поиска -->
<Border
Grid.Column="0"
WidthRequest="45"
HeightRequest="45"
StrokeShape="RoundRectangle 22.5"
BackgroundColor="{StaticResource Primary}"
Stroke="Transparent"
Margin="0,0,10,0">
<Image
Source="search.png"
WidthRequest="24"
HeightRequest="24"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<!-- Поле поиска с серым фоном -->
<Border
Grid.Column="1"
StrokeShape="RoundRectangle 22.5"
BackgroundColor="{StaticResource Gray300}"
Stroke="Transparent"
HeightRequest="45"
Padding="15,0">
<Entry
x:Name="SearchEntry"
Placeholder="Поиск документов..."
PlaceholderColor="{StaticResource Gray500}"
TextColor="{StaticResource Gray900}"
BackgroundColor="Transparent"
VerticalOptions="Center"
TextChanged="OnSearchTextChanged"/>
</Border>
</Grid>
<!-- Фильтры -->
<ScrollView
Grid.Row="2"
Orientation="Horizontal"
HorizontalScrollBarVisibility="Never"
BackgroundColor="White">
<HorizontalStackLayout
Padding="15,5,15,15"
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="FilterAcknowledged"
Style="{StaticResource ChipButton}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterAcknowledgedTapped"/>
</Border.GestureRecognizers>
<Label Text="✅ Ознакомлен" FontSize="14" TextColor="{StaticResource Gray700}"/>
</Border>
<Border
x:Name="FilterNotAcknowledged"
Style="{StaticResource ChipButton}">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFilterNotAcknowledgedTapped"/>
</Border.GestureRecognizers>
<Label Text="⏳ Не ознакомлен" FontSize="14" TextColor="{StaticResource Gray700}"/>
</Border>
</HorizontalStackLayout>
</ScrollView>
<!-- Список документов -->
<RefreshView
Grid.Row="3"
x:Name="RefreshView"
IsRefreshing="False"
Refreshing="OnRefreshing">
<CollectionView
x:Name="DocumentsCollectionView"
SelectionMode="Single"
SelectionChanged="OnDocumentSelected"
BackgroundColor="Transparent">
<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:Document">
<Border
Style="{StaticResource CardBorder}"
Margin="10,5"
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 FileIcon}" Style="{StaticResource IconContainerText}"/>
</Border>
<!-- Название документа -->
<Label
Grid.Column="1"
Grid.Row="0"
Text="{Binding Title}"
Style="{StaticResource CardTitle}"
LineBreakMode="TailTruncation"
MaxLines="2"/>
<!-- Статус ознакомления (бейдж) -->
<Border
Grid.Column="2"
Grid.Row="0"
Style="{StaticResource Badge}"
BackgroundColor="{Binding AcknowledgedColor}"
VerticalOptions="Center">
<Label Text="{Binding AcknowledgedText}" Style="{StaticResource BadgeText}"/>
</Border>
<!-- Описание / Тип документа -->
<Label
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="1"
Text="{Binding ShortDescription}"
Style="{StaticResource CardSubtitle}"
LineBreakMode="TailTruncation"
MaxLines="2"
Margin="0,5,0,0"/>
<!-- Информация о загрузке -->
<HorizontalStackLayout
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="2"
Spacing="10"
Margin="0,8,0,0">
<!-- Кто загрузил -->
<HorizontalStackLayout Spacing="5">
<Label
Text="👤"
FontSize="12"
VerticalOptions="Center"/>
<Label
Text="{Binding UploaderName}"
FontSize="12"
TextColor="{StaticResource Gray500}"
VerticalOptions="Center"
LineBreakMode="TailTruncation"
MaxLines="1"/>
</HorizontalStackLayout>
<!-- Разделитель -->
<Label
Text="•"
FontSize="12"
TextColor="{StaticResource Gray400}"
VerticalOptions="Center"/>
<!-- Дата загрузки -->
<Label
Text="{Binding DisplayUploadedAt}"
FontSize="12"
TextColor="{StaticResource Gray500}"
VerticalOptions="Center"/>
</HorizontalStackLayout>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
<!-- Индикатор загрузки -->
<ActivityIndicator
x:Name="LoadingIndicator"
Grid.Row="3"
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
WidthRequest="50"
HeightRequest="50"
HorizontalOptions="Center"
VerticalOptions="Center"/>
</Grid>
</ContentPage>

View File

@ -0,0 +1,272 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using CORP_MAUI.Models;
namespace CORP_MAUI.Pages;
public partial class DocumentsPage : ContentPage
{
private readonly ObservableCollection<Document> _documents;
private List<Document> _allDocuments = new();
private string _currentFilter = "all";
private string _searchText = string.Empty;
public DocumentsPage()
{
try
{
Trace.WriteLine("=== DOCUMENTS PAGE CONSTRUCTOR START ===");
Trace.WriteLine("[DocumentsPage] >>> Вызов InitializeComponent");
InitializeComponent();
Trace.WriteLine("[DocumentsPage] >>> InitializeComponent завершен");
_documents = new ObservableCollection<Document>();
DocumentsCollectionView.ItemsSource = _documents;
Trace.WriteLine("[DocumentsPage] >>> Запуск LoadDocumentsAsync");
// Загружаем документы
_ = LoadDocumentsAsync();
Trace.WriteLine("=== DOCUMENTS PAGE CONSTRUCTOR END ===");
}
catch (Exception ex)
{
Trace.WriteLine("=== DOCUMENTS PAGE CONSTRUCTOR ERROR ===");
Trace.WriteLine($"[DocumentsPage] ❌ Тип ошибки: {ex.GetType().FullName}");
Trace.WriteLine($"[DocumentsPage] ❌ Сообщение: {ex.Message}");
Trace.WriteLine($"[DocumentsPage] ❌ 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("Ошибка DocumentsPage",
$"Ошибка инициализации:\n{ex.Message}\n\nStackTrace:\n{ex.StackTrace}", "OK");
}
}
});
throw;
}
}
private async Task LoadDocumentsAsync()
{
try
{
Trace.WriteLine("[DocumentsPage] >>> LoadDocumentsAsync START");
LoadingIndicator.IsVisible = true;
LoadingIndicator.IsRunning = true;
Trace.WriteLine("[DocumentsPage] >>> Вызов GetAllDocumentsAsync");
_allDocuments = await ServiceProvider.ApiService.GetAllDocumentsAsync();
Trace.WriteLine($"[DocumentsPage] >>> Получено документов: {_allDocuments.Count}");
ApplyFilters();
Trace.WriteLine($"[DocumentsPage] ✅ Документы загружены успешно");
}
catch (Exception ex)
{
Trace.WriteLine($"[DocumentsPage] ❌ Ошибка загрузки: {ex.GetType().Name}");
Trace.WriteLine($"[DocumentsPage] Message: {ex.Message}");
Trace.WriteLine($"[DocumentsPage] StackTrace: {ex.StackTrace}");
await DisplayAlert("Ошибка", $"Не удалось загрузить список документов:\n{ex.Message}", "OK");
}
finally
{
LoadingIndicator.IsVisible = false;
LoadingIndicator.IsRunning = false;
RefreshView.IsRefreshing = false;
}
}
private void ApplyFilters()
{
IEnumerable<Document> filtered = _allDocuments;
// Фильтр по статусу ознакомления
filtered = _currentFilter switch
{
"acknowledged" => filtered.Where(d => d.IsAcknowledged),
"not_acknowledged" => filtered.Where(d => !d.IsAcknowledged),
_ => filtered
};
// Фильтр по поиску
if (!string.IsNullOrWhiteSpace(_searchText))
{
string search = _searchText.ToLower();
filtered = filtered.Where(d =>
d.Title.ToLower().Contains(search) ||
d.Description.ToLower().Contains(search) ||
d.UploaderName.ToLower().Contains(search));
}
// Сортировка по дате загрузки (новые сверху)
filtered = filtered.OrderByDescending(d => d.UploadedAt);
_documents.Clear();
foreach (var document in filtered)
{
_documents.Add(document);
}
Trace.WriteLine($"[DocumentsPage] >>> Отображено документов: {_documents.Count}");
}
private async void OnRefreshing(object sender, EventArgs e)
{
await LoadDocumentsAsync();
}
private void OnSearchTextChanged(object sender, TextChangedEventArgs e)
{
_searchText = e.NewTextValue ?? string.Empty;
ApplyFilters();
}
private async void OnDocumentSelected(object sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is Document selectedDocument)
{
// Сбрасываем выделение
DocumentsCollectionView.SelectedItem = null;
Trace.WriteLine($"[DocumentsPage] >>> Выбран документ: {selectedDocument.Title}");
// Показываем диалог с выбором действия
string action = await DisplayActionSheet(
selectedDocument.Title,
"Отмена",
null,
"📥 Открыть документ",
"ℹ️ Информация о документе");
if (action == "📥 Открыть документ")
{
await OpenDocumentAsync(selectedDocument);
}
else if (action == "ℹ️ Информация о документе")
{
await ShowDocumentInfoAsync(selectedDocument);
}
}
}
private async Task OpenDocumentAsync(Document document)
{
try
{
if (string.IsNullOrEmpty(document.FileUrl))
{
await DisplayAlert("Ошибка", "URL документа не найден", "OK");
return;
}
Trace.WriteLine($"[DocumentsPage] >>> Открытие документа: {document.FileUrl}");
// Открываем документ в браузере
await Launcher.OpenAsync(new Uri(document.FileUrl));
}
catch (Exception ex)
{
Trace.WriteLine($"[DocumentsPage] ❌ Ошибка открытия документа: {ex.Message}");
await DisplayAlert("Ошибка", $"Не удалось открыть документ:\n{ex.Message}", "OK");
}
}
private async Task ShowDocumentInfoAsync(Document document)
{
string info = $"📄 {document.Title}\n\n";
if (!string.IsNullOrEmpty(document.Description))
{
info += $"📝 Описание:\n{document.Description}\n\n";
}
info += $"👤 Загрузил: {document.UploaderName}\n";
info += $"📅 Дата загрузки: {document.DisplayUploadedAt}\n";
info += $"📁 Тип файла: {document.FileExtension.ToUpper()}\n";
info += $"📨 Для всех: {(document.SentToAll ? "Да" : "Нет")}\n";
info += $"{document.AcknowledgedIcon} Статус: {document.AcknowledgedText}";
await DisplayAlert("Информация о документе", info, "OK");
}
private async void OnBackClicked(object sender, EventArgs e)
{
await Shell.Current.GoToAsync("..");
}
#region Фильтры
private void OnFilterAllTapped(object sender, TappedEventArgs e)
{
SetActiveFilter("all");
ApplyFilters();
}
private void OnFilterAcknowledgedTapped(object sender, TappedEventArgs e)
{
SetActiveFilter("acknowledged");
ApplyFilters();
}
private void OnFilterNotAcknowledgedTapped(object sender, TappedEventArgs e)
{
SetActiveFilter("not_acknowledged");
ApplyFilters();
}
private void SetActiveFilter(string filter)
{
_currentFilter = filter;
// Получаем стили из Application.Current.Resources
Style? chipButton = Application.Current?.Resources["ChipButton"] as Style;
Style? chipButtonActive = Application.Current?.Resources["ChipButtonActive"] as Style;
Color? grayColor = Application.Current?.Resources["Gray700"] as Color ?? Colors.Gray;
// Сбрасываем все фильтры на неактивные
if (chipButton != null)
{
FilterAll.Style = chipButton;
FilterAcknowledged.Style = chipButton;
FilterNotAcknowledged.Style = chipButton;
}
// Обновляем цвет текста у неактивных
if (FilterAll.Content is Label allLabel)
allLabel.TextColor = grayColor;
if (FilterAcknowledged.Content is Label acknowledgedLabel)
acknowledgedLabel.TextColor = grayColor;
if (FilterNotAcknowledged.Content is Label notAcknowledgedLabel)
notAcknowledgedLabel.TextColor = grayColor;
// Активируем выбранный фильтр
Border activeFilter = filter switch
{
"acknowledged" => FilterAcknowledged,
"not_acknowledged" => FilterNotAcknowledged,
_ => FilterAll
};
if (chipButtonActive != null)
activeFilter.Style = chipButtonActive;
if (activeFilter.Content is Label activeLabel)
activeLabel.TextColor = Colors.White;
}
#endregion
}

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.EmployeeProfilePage"
Title="Профиль сотрудника"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Shell.BackButtonBehavior>
<BackButtonBehavior IsVisible="True" TextOverride="Назад" />

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.EmployeesPage"
Title="Сотрудники"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,*">

View File

@ -43,53 +43,66 @@
Stroke="Transparent"
Shadow="{Shadow Brush=Black, Opacity=0.2, Offset='0,4', Radius=8}">
<VerticalStackLayout Spacing="15">
<VerticalStackLayout Spacing="0">
<!-- Имя пользователя -->
<!-- Email -->
<Label
Text="Имя пользователя"
Text="Email"
FontSize="14"
FontAttributes="Bold"
TextColor="{StaticResource Gray700}"/>
TextColor="{StaticResource Gray700}"
Margin="0,0,0,8"/>
<Border
StrokeShape="RoundRectangle 10"
StrokeShape="RoundRectangle 10,10,0,0"
BackgroundColor="{StaticResource Gray100}"
Stroke="{StaticResource Gray200}"
StrokeThickness="1"
Padding="12,0"
HeightRequest="50">
<Entry
x:Name="UsernameEntry"
Placeholder="Введите имя пользователя"
Placeholder="Введите email"
PlaceholderColor="{StaticResource Gray400}"
TextColor="{StaticResource Gray900}"
BackgroundColor="Transparent"
Keyboard="Email"
ReturnType="Next"
Completed="OnEmailCompleted"
VerticalOptions="Center"/>
</Border>
<!-- Пароль -->
<Label
Text="Пароль"
FontSize="14"
FontAttributes="Bold"
TextColor="{StaticResource Gray700}"
Margin="0,10,0,0"/>
<!-- Пароль (объединён с полем Email) -->
<Border
StrokeShape="RoundRectangle 10"
StrokeShape="RoundRectangle 0,0,10,10"
BackgroundColor="{StaticResource Gray100}"
Stroke="{StaticResource Gray200}"
StrokeThickness="1"
Padding="12,0"
HeightRequest="50">
<Entry
x:Name="PasswordEntry"
Placeholder="Введите пароль"
IsPassword="True"
PlaceholderColor="{StaticResource Gray400}"
TextColor="{StaticResource Gray900}"
BackgroundColor="Transparent"
VerticalOptions="Center"/>
HeightRequest="50"
Margin="0,-1,0,0">
<Grid ColumnDefinitions="Auto,*">
<BoxView
Grid.Column="0"
WidthRequest="1"
BackgroundColor="{StaticResource Gray200}"
IsVisible="False"/>
<Entry
Grid.Column="1"
x:Name="PasswordEntry"
Placeholder="Введите пароль"
IsPassword="True"
PlaceholderColor="{StaticResource Gray400}"
TextColor="{StaticResource Gray900}"
BackgroundColor="Transparent"
ReturnType="Done"
Completed="OnPasswordCompleted"
VerticalOptions="Center"/>
</Grid>
</Border>
<!-- Разделитель между полями и кнопкой -->
<BoxView HeightRequest="20" BackgroundColor="Transparent"/>
<!-- Сообщение об ошибке -->
<Label
@ -97,14 +110,14 @@
IsVisible="False"
TextColor="{StaticResource Error}"
FontSize="13"
HorizontalOptions="Center"/>
HorizontalOptions="Center"
Margin="0,0,0,10"/>
<!-- Кнопка входа -->
<Button
x:Name="LoginButton"
Text="Войти"
Style="{StaticResource PrimaryButton}"
Margin="0,15,0,0"
Clicked="OnLoginClicked"/>
<!-- Индикатор загрузки -->
@ -113,7 +126,8 @@
IsRunning="False"
IsVisible="False"
Color="{StaticResource Primary}"
HorizontalOptions="Center"/>
HorizontalOptions="Center"
Margin="0,10,0,0"/>
</VerticalStackLayout>
</Border>

View File

@ -142,6 +142,22 @@ public partial class LoginPage : ContentPage
SetLoadingState(false);
}
}
/// <summary>
/// При нажатии Enter/Next в поле Email - переходим к полю пароля
/// </summary>
private void OnEmailCompleted(object? sender, EventArgs e)
{
PasswordEntry.Focus();
}
/// <summary>
/// При нажатии Enter/Done в поле пароля - отправляем форму
/// </summary>
private void OnPasswordCompleted(object? sender, EventArgs e)
{
OnLoginClicked(sender!, e);
}
private async Task ShowError(string message)
{

View File

@ -0,0 +1,392 @@
<?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:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.MenuPage"
Title="Меню"
BackgroundColor="White"
Shell.NavBarIsVisible="False"
Shell.TabBarIsVisible="False">
<Grid RowDefinitions="Auto,*,Auto" Padding="20">
<!-- Заголовок с кнопкой закрытия -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,20,0,40">
<Label Text="Меню"
FontSize="24"
FontAttributes="Bold"
TextColor="#212529"
HorizontalOptions="Center"
Grid.ColumnSpan="2"/>
<Border Grid.Column="1"
WidthRequest="40"
HeightRequest="40"
StrokeShape="RoundRectangle 20"
BackgroundColor="Transparent"
Stroke="Transparent"
HorizontalOptions="End">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnCloseClicked"/>
</Border.GestureRecognizers>
<Image Source="cross"
WidthRequest="24"
HeightRequest="24"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="#888888"/>
</Image.Behaviors>
</Image>
</Border>
</Grid>
<!-- Сетка разделов -->
<Grid Grid.Row="1"
ColumnDefinitions="*,*,*"
RowDefinitions="Auto,Auto,Auto"
ColumnSpacing="16"
RowSpacing="24"
VerticalOptions="Center"
HorizontalOptions="Center"
MaximumWidthRequest="400">
<!-- Новости -->
<VerticalStackLayout Grid.Row="0" Grid.Column="0" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnNewsClicked"/>
</Border.GestureRecognizers>
<Image Source="feed"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Новости"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Отделы -->
<VerticalStackLayout Grid.Row="0" Grid.Column="1" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDepartmentsClicked"/>
</Border.GestureRecognizers>
<Image Source="department"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Отделы"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Сотрудники -->
<VerticalStackLayout Grid.Row="0" Grid.Column="2" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnEmployeesClicked"/>
</Border.GestureRecognizers>
<Image Source="employees"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Сотрудники"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Заявления -->
<VerticalStackLayout Grid.Row="1" Grid.Column="0" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnRequestsClicked"/>
</Border.GestureRecognizers>
<Image Source="statements"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Заявления"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Чаты -->
<VerticalStackLayout Grid.Row="1" Grid.Column="1" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnChatsClicked"/>
</Border.GestureRecognizers>
<Image Source="chats"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Чаты"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Документы -->
<VerticalStackLayout Grid.Row="1" Grid.Column="2" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDocumentsClicked"/>
</Border.GestureRecognizers>
<Image Source="documents"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Документы"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Финансы -->
<VerticalStackLayout Grid.Row="2" Grid.Column="0" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnFinancesClicked"/>
</Border.GestureRecognizers>
<Image Source="finances"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Финансы"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Профиль -->
<VerticalStackLayout Grid.Row="2" Grid.Column="1" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnProfileClicked"/>
</Border.GestureRecognizers>
<Image Source="profile"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Профиль"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
<!-- Настройки -->
<VerticalStackLayout Grid.Row="2" Grid.Column="2" Spacing="10">
<Border StrokeShape="RoundRectangle 20"
StrokeThickness="0"
HeightRequest="100"
WidthRequest="100">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#5B9DF9" Offset="0"/>
<GradientStop Color="#3D7BE8" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnSettingsClicked"/>
</Border.GestureRecognizers>
<Image Source="settings"
WidthRequest="48"
HeightRequest="48"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
<Label Text="Настройки"
FontSize="14"
TextColor="#212529"
HorizontalTextAlignment="Center"/>
</VerticalStackLayout>
</Grid>
<!-- Переключатель темы -->
<Border Grid.Row="2"
StrokeShape="RoundRectangle 25"
BackgroundColor="#0D6EFD"
StrokeThickness="0"
Padding="4"
HorizontalOptions="Start"
Margin="0,20,0,20">
<HorizontalStackLayout Spacing="0">
<!-- Светлая тема -->
<Border x:Name="LightThemeButton"
WidthRequest="40"
HeightRequest="40"
StrokeShape="RoundRectangle 20"
BackgroundColor="White"
StrokeThickness="0">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnLightThemeClicked"/>
</Border.GestureRecognizers>
<Image x:Name="LightThemeIcon"
Source="lightmode"
WidthRequest="20"
HeightRequest="20"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior x:Name="LightThemeIconTint" TintColor="#0D6EFD"/>
</Image.Behaviors>
</Image>
</Border>
<!-- Тёмная тема -->
<Border x:Name="DarkThemeButton"
WidthRequest="40"
HeightRequest="40"
StrokeShape="RoundRectangle 20"
BackgroundColor="Transparent"
StrokeThickness="0">
<Border.GestureRecognizers>
<TapGestureRecognizer Tapped="OnDarkThemeClicked"/>
</Border.GestureRecognizers>
<Image x:Name="DarkThemeIcon"
Source="darkmode"
WidthRequest="20"
HeightRequest="20"
HorizontalOptions="Center"
VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior x:Name="DarkThemeIconTint" TintColor="White"/>
</Image.Behaviors>
</Image>
</Border>
</HorizontalStackLayout>
</Border>
</Grid>
</ContentPage>

View File

@ -0,0 +1,168 @@
using System.Diagnostics;
namespace CORP_MAUI.Pages;
public partial class MenuPage : ContentPage
{
// Сохраняем индекс вкладки, с которой открыли меню
private static int _previousTabIndex = 2; // По умолчанию - Лента
public static void SetPreviousTabIndex(int index)
{
if (index != 0) // 0 - это меню, не сохраняем
{
_previousTabIndex = index;
}
}
public MenuPage()
{
InitializeComponent();
}
private async void OnCloseClicked(object? sender, EventArgs e)
{
// Сначала переключаемся на предыдущую вкладку
if (Shell.Current?.CurrentItem is TabBar tabBar && tabBar.Items.Count > _previousTabIndex)
{
Shell.Current.CurrentItem = tabBar.Items[_previousTabIndex];
}
await Navigation.PopModalAsync(animated: false);
}
private async Task NavigateToTabAsync(int tabIndex)
{
// Переключаем на нужную вкладку
if (Shell.Current?.CurrentItem is TabBar tabBar && tabBar.Items.Count > tabIndex)
{
Shell.Current.CurrentItem = tabBar.Items[tabIndex];
}
await Navigation.PopModalAsync(animated: false);
}
private async void OnNewsClicked(object? sender, EventArgs e)
{
await NavigateToTabAsync(2); // Лента - третья вкладка (индекс 2)
}
private async void OnDepartmentsClicked(object? sender, EventArgs e)
{
await Navigation.PopModalAsync(animated: false);
// Отделы нет в нижней панели - переходим через Shell navigation
try
{
await Shell.Current.GoToAsync(nameof(DepartmentsPage));
}
catch
{
// Fallback - показываем сообщение
if (Application.Current?.Windows.FirstOrDefault()?.Page is Page page)
{
await page.DisplayAlert("Отделы", "Перейдите в раздел 'Отделы' через меню", "OK");
}
}
}
private async void OnEmployeesClicked(object? sender, EventArgs e)
{
await NavigateToTabAsync(4); // Сотрудники - пятая вкладка (индекс 4)
}
private async void OnRequestsClicked(object? sender, EventArgs e)
{
await NavigateToTabAsync(3); // Заявления - четвёртая вкладка (индекс 3)
}
private async void OnChatsClicked(object? sender, EventArgs e)
{
await NavigateToTabAsync(1); // Сообщения - вторая вкладка (индекс 1)
}
private async void OnDocumentsClicked(object? sender, EventArgs e)
{
await Navigation.PopModalAsync(animated: false);
// Переходим на страницу документов
try
{
await Shell.Current.GoToAsync(nameof(DocumentsPage));
}
catch
{
// Fallback - показываем сообщение
if (Application.Current?.Windows.FirstOrDefault()?.Page is Page page)
{
await page.DisplayAlert("Документы", "Не удалось открыть раздел документов", "OK");
}
}
}
private async void OnFinancesClicked(object? sender, EventArgs e)
{
await Navigation.PopModalAsync(animated: false);
// Финансы пока не реализованы
if (Application.Current?.Windows.FirstOrDefault()?.Page is Page page)
{
await page.DisplayAlert("Финансы", "Раздел финансов находится в разработке", "OK");
}
}
private async void OnProfileClicked(object? sender, EventArgs e)
{
await Navigation.PopModalAsync(animated: false);
// Профиль нет в нижней панели - переходим через Shell navigation
try
{
await Shell.Current.GoToAsync(nameof(ProfilePage));
}
catch
{
// Fallback - показываем сообщение
if (Application.Current?.Windows.FirstOrDefault()?.Page is Page page)
{
await page.DisplayAlert("Профиль", "Перейдите в раздел 'Профиль' через меню", "OK");
}
}
}
private async void OnSettingsClicked(object? sender, EventArgs e)
{
await Navigation.PopModalAsync(animated: false);
// Настройки пока не реализованы
if (Application.Current?.Windows.FirstOrDefault()?.Page is Page page)
{
await page.DisplayAlert("Настройки", "Раздел настроек находится в разработке", "OK");
}
}
private void OnLightThemeClicked(object? sender, EventArgs e)
{
// Активируем светлую тему - белый ползунок слева
LightThemeButton.BackgroundColor = Colors.White;
DarkThemeButton.BackgroundColor = Colors.Transparent;
// Иконка на белом фоне - акцентный цвет
LightThemeIconTint.TintColor = Color.FromArgb("#0D6EFD");
// Иконка на прозрачном (синем) фоне - белая
DarkThemeIconTint.TintColor = Colors.White;
// TODO: Применить светлую тему к приложению
Trace.WriteLine("[MenuPage] Светлая тема выбрана");
}
private void OnDarkThemeClicked(object? sender, EventArgs e)
{
// Активируем тёмную тему - белый ползунок справа
DarkThemeButton.BackgroundColor = Colors.White;
LightThemeButton.BackgroundColor = Colors.Transparent;
// Иконка на белом фоне - акцентный цвет
DarkThemeIconTint.TintColor = Color.FromArgb("#0D6EFD");
// Иконка на прозрачном (синем) фоне - белая
LightThemeIconTint.TintColor = Colors.White;
// TODO: Применить тёмную тему к приложению
Trace.WriteLine("[MenuPage] Тёмная тема выбрана");
}
}

View File

@ -0,0 +1,11 @@
<?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"
x:Class="CORP_MAUI.Pages.MenuTabPage"
Title="Меню"
BackgroundColor="White">
<!-- Эта страница служит только для открытия меню -->
<!-- Автоматически открывает модальное меню при появлении -->
</ContentPage>

View File

@ -0,0 +1,30 @@
namespace CORP_MAUI.Pages;
public partial class MenuTabPage : ContentPage
{
public MenuTabPage()
{
InitializeComponent();
}
protected override async void OnAppearing()
{
base.OnAppearing();
// Проверяем, нет ли уже открытого модального окна
if (Navigation.ModalStack.Count > 0) return;
// Небольшая задержка для UI
await Task.Delay(10);
// Открываем модальное меню
try
{
await Navigation.PushModalAsync(new MenuPage(), animated: false);
}
catch
{
// Игнорируем ошибки навигации
}
}
}

View File

@ -7,7 +7,7 @@
x:Class="CORP_MAUI.Pages.PostDetailsPage"
x:DataType="viewModels:PostDetailsViewModel"
Title="Публикация"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,*">
@ -16,7 +16,7 @@
Grid.Row="0"
ColumnDefinitions="50,*,50"
Padding="15,10"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<!-- Кнопка назад -->
<Border

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.PostsPage"
Title="Лента новостей"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,*">
@ -67,7 +67,7 @@
Grid.Row="1"
ColumnDefinitions="Auto,*"
Padding="15,5,15,15"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<!-- Круглая кнопка поиска -->
<Border

View File

@ -42,12 +42,10 @@ public partial class PostsPage
var user = await ServiceProvider.ApiService.GetCurrentUserAsync();
if (user != null)
{
Trace.WriteLine($"[PostsPage] >>> Загрузка аватара для {user.FullName}, URL: {user.DisplayAvatarUrl}");
Trace.WriteLine($"[PostsPage] >>> Загрузка аватара для {user.FullName}");
await ServiceProvider.ApiService.LoadUserAvatarAsync(user);
Trace.WriteLine($"[PostsPage] IsAvatarLoaded: {user.IsAvatarLoaded}, CachedPath: {user.CachedAvatarFilePath}");
// Обновляем на главном потоке
await MainThread.InvokeOnMainThreadAsync(() =>
{

View File

@ -7,7 +7,7 @@
x:Class="CORP_MAUI.Pages.ProfilePage"
x:DataType="viewModels:ProfileViewModel"
Title="Профиль"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,*">
@ -16,7 +16,7 @@
Grid.Row="0"
ColumnDefinitions="50,*,50"
Padding="15,10"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<!-- Пустое место слева -->
<BoxView Grid.Column="0" Color="Transparent"/>
@ -113,6 +113,21 @@
<BoxView HeightRequest="1" Color="{StaticResource Gray200}"/>
<!-- Телефон -->
<Grid ColumnDefinitions="Auto,*" IsVisible="{Binding HasPhone}">
<HorizontalStackLayout Grid.Column="0" Spacing="8">
<Image Source="phone.png" WidthRequest="20" HeightRequest="20" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="{StaticResource Primary}"/>
</Image.Behaviors>
</Image>
<Label Text="Телефон" Style="{StaticResource CardText}" FontAttributes="Bold" VerticalOptions="Center"/>
</HorizontalStackLayout>
<Label Grid.Column="1" Text="{Binding Phone}" Style="{StaticResource CardText}" HorizontalOptions="End" VerticalOptions="Center"/>
</Grid>
<BoxView HeightRequest="1" Color="{StaticResource Gray200}" IsVisible="{Binding HasPhone}"/>
<!-- Отдел -->
<Grid ColumnDefinitions="Auto,*">
<HorizontalStackLayout Grid.Column="0" Spacing="8">
@ -128,6 +143,30 @@
</VerticalStackLayout>
</Border>
<!-- Все отделы (если несколько) -->
<Border Style="{StaticResource CardBorderLarge}" IsVisible="{Binding HasDepartments}">
<VerticalStackLayout Spacing="12">
<Label Text="Все отделы" Style="{StaticResource SectionTitle}"/>
<VerticalStackLayout BindableLayout.ItemsSource="{Binding Departments}" Spacing="8">
<BindableLayout.ItemTemplate>
<DataTemplate x:DataType="models:Department">
<HorizontalStackLayout Spacing="8">
<Image Source="departments.png" WidthRequest="16" HeightRequest="16" VerticalOptions="Center">
<Image.Behaviors>
<toolkit:IconTintColorBehavior TintColor="{StaticResource Primary}"/>
</Image.Behaviors>
</Image>
<Label Text="{Binding Name}" Style="{StaticResource CardText}" VerticalOptions="Center"/>
</HorizontalStackLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
<!-- Навыки -->
<Border Style="{StaticResource CardBorderLarge}" IsVisible="{Binding HasSkills}">

View File

@ -5,7 +5,7 @@
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="CORP_MAUI.Pages.RequestsPage"
Title="Заявления"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<Grid RowDefinitions="Auto,Auto,Auto,*">
@ -14,7 +14,7 @@
Grid.Row="0"
ColumnDefinitions="50,*,50"
Padding="15,10"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<!-- Пустое место слева -->
<BoxView Grid.Column="0" Color="Transparent"/>
@ -38,7 +38,7 @@
Grid.Row="1"
ColumnDefinitions="Auto,*"
Padding="15,5,15,10"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<!-- Круглая кнопка поиска -->
<Border
@ -85,7 +85,7 @@
Grid.Row="2"
Orientation="Horizontal"
HorizontalScrollBarVisibility="Never"
BackgroundColor="#E5E5E5">
BackgroundColor="White">
<HorizontalStackLayout
Padding="15,5,15,15"
Spacing="10"

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Переопределяем стиль BottomNavigationView для Shell TabBar -->
<!-- Увеличиваем размер иконок и убираем текст -->
<style name="Widget.Design.BottomNavigationView" parent="Widget.MaterialComponents.BottomNavigationView">
<!-- Увеличиваем размер иконок на ~15% (стандартный 24dp -> 28dp) -->
<item name="itemIconSize">28dp</item>
<!-- Убираем текст под иконками - иконки автоматически центрируются -->
<item name="labelVisibilityMode">unlabeled</item>
<!-- Высота панели -->
<item name="android:minHeight">56dp</item>
</style>
<!-- Дополнительный стиль для Material3 -->
<style name="Widget.Material3.BottomNavigationView" parent="Widget.MaterialComponents.BottomNavigationView">
<item name="itemIconSize">28dp</item>
<item name="labelVisibilityMode">unlabeled</item>
</style>
</resources>

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path fill="#0D6EFD" d="m12 14.122l5.303 5.303a1.5 1.5 0 0 0 2.122-2.122L14.12 12l5.304-5.303a1.5 1.5 0 1 0-2.122-2.121L12 9.879L6.697 4.576a1.5 1.5 0 1 0-2.122 2.12L9.88 12l-5.304 5.304a1.5 1.5 0 1 0 2.122 2.12z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 858 B

View File

@ -783,6 +783,23 @@ public class ApiService : IDisposable
}
}
/// <summary>
/// Загрузить аватарки для списка пользователей
/// </summary>
public Task LoadUsersAvatarsAsync(List<User>? users)
{
if (users == null) return Task.CompletedTask;
foreach (var user in users)
{
if (!string.IsNullOrEmpty(user.AvatarUrl) && !user.AvatarUrl.StartsWith("http"))
{
user.AvatarUrl = $"{_config.BaseUrl.TrimEnd('/')}/{user.AvatarUrl.TrimStart('/')}";
}
}
return Task.CompletedTask;
}
/// <summary>
/// Получить информацию о конкретном пользователе по ID
/// </summary>
@ -1099,6 +1116,46 @@ public class ApiService : IDisposable
}
}
/// <summary>
/// Получить все документы (с автоматической пагинацией)
/// </summary>
public async Task<List<Document>> GetAllDocumentsAsync()
{
List<Document> allDocuments = new List<Document>();
int page = 1;
bool hasMore = true;
try
{
Trace.WriteLine("[ApiService] >>> Загрузка всех документов...");
while (hasMore)
{
PaginatedResponse<Document> response = await GetDocumentsPaginatedAsync(page);
if (response.Results != null && response.Results.Any())
{
allDocuments.AddRange(response.Results);
Trace.WriteLine($"[ApiService] >>> Загружено документов: {allDocuments.Count} / {response.Count}");
hasMore = !string.IsNullOrEmpty(response.Next);
page++;
}
else
{
hasMore = false;
}
}
Trace.WriteLine($"[ApiService] ✅ Всего загружено документов: {allDocuments.Count}");
return allDocuments;
}
catch (Exception ex)
{
Trace.WriteLine($"[ApiService] ❌ Ошибка загрузки документов: {ex.Message}");
return allDocuments;
}
}
// ============================================
// REQUESTS (Запросы)
// ============================================

View File

@ -29,8 +29,12 @@ public class ProfileViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(FullName));
OnPropertyChanged(nameof(Username));
OnPropertyChanged(nameof(Email));
OnPropertyChanged(nameof(Phone));
OnPropertyChanged(nameof(HasPhone));
OnPropertyChanged(nameof(Position));
OnPropertyChanged(nameof(Department));
OnPropertyChanged(nameof(Departments));
OnPropertyChanged(nameof(HasDepartments));
OnPropertyChanged(nameof(StatusText));
OnPropertyChanged(nameof(StatusColor));
OnPropertyChanged(nameof(Skills));
@ -53,8 +57,12 @@ public class ProfileViewModel : INotifyPropertyChanged
public string FullName => _user?.FullName ?? "Загрузка...";
public string Username => _user != null ? $"@{_user.Username}" : "";
public string Email => _user?.Email ?? "-";
public string Phone => _user?.PhoneNumber ?? "-";
public bool HasPhone => !string.IsNullOrEmpty(_user?.PhoneNumber);
public string Position => _user?.PositionName ?? "-";
public string Department => _user?.DepartmentName ?? "-";
public List<Department>? Departments => _user?.Departments;
public bool HasDepartments => _user?.Departments != null && _user.Departments.Any();
public bool IsOnline => _user?.IsOnline ?? false;
public string StatusText => IsOnline ? "В сети" : "Не в сети";
public Color StatusColor => IsOnline ? Colors.LimeGreen : Colors.Gray;