diff --git a/.idea/.idea.CORP-MAUI/.idea/dictionaries/project.xml b/.idea/.idea.CORP-MAUI/.idea/dictionaries/project.xml
new file mode 100644
index 0000000..902d194
--- /dev/null
+++ b/.idea/.idea.CORP-MAUI/.idea/dictionaries/project.xml
@@ -0,0 +1,7 @@
+
+
+
+ robotail
+
+
+
\ No newline at end of file
diff --git a/CORP-MAUI/Controls/DesktopLayout.xaml.cs b/CORP-MAUI/Controls/DesktopLayout.xaml.cs
index 7bfae25..0c82a6c 100644
--- a/CORP-MAUI/Controls/DesktopLayout.xaml.cs
+++ b/CORP-MAUI/Controls/DesktopLayout.xaml.cs
@@ -93,35 +93,48 @@ public partial class DesktopLayout : ContentView
header.Children.Add(addButton);
// Контейнер для контента
- var content = new VerticalStackLayout
- {
- Spacing = 15
- };
- content.Children.Add(header);
-
+ // Вместо простого VerticalStackLayout используем Grid, чтобы выделить header и scrollable area
+ var root = new Grid
+ {
+ RowDefinitions = new RowDefinitionCollection
+ {
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }
+ },
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.Fill
+ };
+ root.Add(header);
+ Grid.SetRow(header, 0);
+
// Здесь будет ScrollView с постами (загружается асинхронно)
var postsContainer = new VerticalStackLayout
- {
- Spacing = 15
- };
+ {
+ Spacing = 15,
+ HorizontalOptions = LayoutOptions.Fill
+ };
// Placeholder пока загружаются посты
var loadingIndicator = new ActivityIndicator
- {
- IsRunning = true,
- Color = Color.FromArgb("#0D6EFD"),
- HorizontalOptions = LayoutOptions.Center,
- Margin = new Thickness(0, 50)
- };
+ {
+ IsRunning = true,
+ Color = Color.FromArgb("#0D6EFD"),
+ HorizontalOptions = LayoutOptions.Center,
+ Margin = new Thickness(0, 50)
+ };
postsContainer.Children.Add(loadingIndicator);
- var scrollView = new ScrollView { Content = postsContainer };
- content.Children.Add(scrollView);
+ // Корректно настраиваем ScrollView: Fill и layout-constraints
+ var scrollView = new ScrollView { Content = postsContainer, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill };
+
+ // Помещаем scrollView в фиксированную ячейку Grid
+ root.Add(scrollView);
+ Grid.SetRow(scrollView, 1);
// Загружаем посты асинхронно
_ = LoadPostsAsync(postsContainer);
- return content;
+ return root;
}
private async Task LoadPostsAsync(VerticalStackLayout container)
@@ -150,6 +163,13 @@ public partial class DesktopLayout : ContentView
foreach (var post in posts)
{
var postCard = CreatePostCard(post);
+ // Даем карточке разумные ограничения по ширине — чтобы не становилась слишком большой
+ if (postCard is View v)
+ {
+ v.HorizontalOptions = LayoutOptions.Fill;
+ v.WidthRequest = double.NaN; // позволяет занять доступную ширину
+ v.MaximumWidthRequest = 900; // не растягивать на весь экран
+ }
container.Children.Add(postCard);
}
});
@@ -184,7 +204,8 @@ public partial class DesktopLayout : ContentView
Opacity = 0.1f,
Offset = new Point(0, 2),
Radius = 8
- }
+ },
+ HorizontalOptions = LayoutOptions.Fill
};
var content = new VerticalStackLayout { Spacing = 12 };
@@ -241,7 +262,8 @@ public partial class DesktopLayout : ContentView
{
Source = post.DisplayImageUrl,
Aspect = Aspect.AspectFill,
- HeightRequest = 300
+ HeightRequest = 300,
+ HorizontalOptions = LayoutOptions.Fill
};
imageContainer.Content = postImage;
content.Children.Add(imageContainer);
@@ -272,7 +294,8 @@ public partial class DesktopLayout : ContentView
{
Text = textContent,
TextColor = Color.FromArgb("#333333"),
- LineBreakMode = LineBreakMode.WordWrap
+ LineBreakMode = LineBreakMode.WordWrap,
+ HorizontalOptions = LayoutOptions.FillAndExpand
});
if (isTruncated)
@@ -505,7 +528,8 @@ public partial class DesktopLayout : ContentView
var container = new VerticalStackLayout { Spacing = 10 };
container.Children.Add(new ActivityIndicator { IsRunning = true, Color = Color.FromArgb("#0D6EFD"), HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, 50) });
- content.Children.Add(new ScrollView { Content = container });
+ // Устанавливаем FillAndExpand чтобы контейнер занял пространство внутри MainContent
+ content.Children.Add(new ScrollView { Content = container, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill });
SetContent(content);
_ = LoadEmployeesAsync(container);
@@ -536,7 +560,8 @@ public partial class DesktopLayout : ContentView
StrokeShape = new Microsoft.Maui.Controls.Shapes.RoundRectangle { CornerRadius = 8 },
Stroke = Colors.Transparent,
Padding = new Thickness(15),
- Margin = new Thickness(0, 0, 0, 5)
+ Margin = new Thickness(0, 0, 0, 5),
+ HorizontalOptions = LayoutOptions.Fill
};
var row = new HorizontalStackLayout { Spacing = 15 };
@@ -577,7 +602,14 @@ public partial class DesktopLayout : ContentView
try
{
System.Diagnostics.Debug.WriteLine($"[DesktopLayout] >>> Открытие профиля сотрудника ID: {employeeId}");
- await Shell.Current.Navigation.PushAsync(new CORP_MAUI.Pages.EmployeeProfilePage(employeeId));
+ var nav = ResolveNavigation();
+ if (nav != null)
+ {
+ await nav.PushAsync(new CORP_MAUI.Pages.EmployeeProfilePage(employeeId));
+ return;
+ }
+
+ System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Не удалось найти навигацию для открытия профиля {employeeId}");
}
catch (Exception ex)
{
@@ -641,7 +673,7 @@ public partial class DesktopLayout : ContentView
headerRow.Children.Add(searchBox);
// Контейнер для сетки отделов с прокруткой
- var gridContainer = new VerticalStackLayout { Spacing = 12 };
+ var gridContainer = new VerticalStackLayout { Spacing = 12, VerticalOptions = LayoutOptions.Fill, HorizontalOptions = LayoutOptions.Fill };
// Добавляем ActivityIndicator пока загружается
var activityIndicator = new ActivityIndicator
@@ -654,6 +686,7 @@ public partial class DesktopLayout : ContentView
};
gridContainer.Children.Add(activityIndicator);
+ // Используем ScrollView вокруг контейнера (и обеспечиваем FillAndExpand у scrollView)
var scrollView = new ScrollView
{
Content = gridContainer,
@@ -663,15 +696,15 @@ public partial class DesktopLayout : ContentView
// Основной контейнер
var content = new Grid
- {
- RowDefinitions = new RowDefinitionCollection
- {
- new RowDefinition { Height = GridLength.Auto },
- new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }
- },
- VerticalOptions = LayoutOptions.Fill,
- HorizontalOptions = LayoutOptions.Fill
- };
+ {
+ RowDefinitions = new RowDefinitionCollection
+ {
+ new RowDefinition { Height = GridLength.Auto },
+ new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }
+ },
+ VerticalOptions = LayoutOptions.Fill,
+ HorizontalOptions = LayoutOptions.Fill
+ };
Grid.SetRow(headerRow, 0);
content.Children.Add(headerRow);
@@ -707,7 +740,7 @@ public partial class DesktopLayout : ContentView
countLabel.Text = $"Всего отделов: {departments.Count}";
System.Diagnostics.Debug.WriteLine($"[DesktopLayout] Создание {departments.Count} карточек отделов...");
- // Создаём Grid с 2 колонками
+ // Для корректной прокрутки и размеров используем Grid с равномерными колонками (Star) и поместим его в ScrollView
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitionCollection
@@ -740,6 +773,8 @@ public partial class DesktopLayout : ContentView
System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ✅ Добавлена карточка {i + 1} в позицию [{row}, {col}]");
}
+ // Оборачиваем grid в контейнер, даём ему FillAndExpand чтобы прокрутка работала корректно
+ grid.HorizontalOptions = LayoutOptions.Fill;
container.Children.Add(grid);
System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ✅ Grid создан с {grid.RowDefinitions.Count} строками");
@@ -818,7 +853,7 @@ public partial class DesktopLayout : ContentView
});
// Аватарки сотрудников (серые кружки, которые потом заменятся на реальные)
- var avatarsRow = new HorizontalStackLayout { Spacing = -8 };
+ var avatarsRow = new HorizontalStackLayout { Spacing = -8, HorizontalOptions = LayoutOptions.Start };
// Сохраняем ссылку на контейнер аватарок в свойстве отдела для последующего обновления
dept.AvatarsContainer = avatarsRow;
@@ -849,7 +884,14 @@ public partial class DesktopLayout : ContentView
try
{
System.Diagnostics.Debug.WriteLine($"[DesktopLayout] >>> Открытие отдела ID: {deptId}");
- await Shell.Current.Navigation.PushAsync(new CORP_MAUI.Pages.DepartmentDetailsPage(deptId));
+ var nav = ResolveNavigation();
+ if (nav != null)
+ {
+ await nav.PushAsync(new CORP_MAUI.Pages.DepartmentDetailsPage(deptId));
+ return;
+ }
+
+ System.Diagnostics.Debug.WriteLine($"[DesktopLayout] ❌ Не удалось найти навигацию для открытия отдела {deptId}");
}
catch (Exception ex)
{
@@ -1186,4 +1228,30 @@ public partial class DesktopLayout : ContentView
return header;
}
+
+ private INavigation? ResolveNavigation()
+ {
+ // Попробуем найти Page, обойдя родителей
+ Element? el = this;
+ while (el != null)
+ {
+ if (el is Page p)
+ return p.Navigation;
+ el = el.Parent;
+ }
+
+ // Application.Current.MainPage может быть Shell или NavigationPage
+ if (Application.Current?.MainPage?.Navigation != null)
+ return Application.Current.MainPage.Navigation;
+
+ // Shell.Current.Navigation
+ if (Shell.Current?.Navigation != null)
+ return Shell.Current.Navigation;
+
+ // Попробуем взять первую открытую Window
+ if (Application.Current?.Windows != null && Application.Current.Windows.Count > 0)
+ return Application.Current.Windows[0].Page?.Navigation;
+
+ return null;
+ }
}
diff --git a/CORP-MAUI/Models/User.cs b/CORP-MAUI/Models/User.cs
index 34fa239..2e7e85f 100644
--- a/CORP-MAUI/Models/User.cs
+++ b/CORP-MAUI/Models/User.cs
@@ -41,6 +41,21 @@ public class User : INotifyPropertyChanged
[JsonPropertyName("avatar")]
public string? Avatar { get; set; }
+
+ // Backwards-compatible property: some code expects PhotoUrl
+ [JsonIgnore]
+ public string? PhotoUrl
+ {
+ get => AvatarUrl ?? Avatar;
+ set
+ {
+ if (value == AvatarUrl) return;
+ AvatarUrl = value;
+ OnPropertyChanged(nameof(AvatarUrl));
+ OnPropertyChanged(nameof(DisplayAvatarUrl));
+ OnPropertyChanged(nameof(DisplayAvatarSource));
+ }
+ }
// INotifyPropertyChanged implementation
public event PropertyChangedEventHandler? PropertyChanged;
@@ -88,36 +103,29 @@ public class User : INotifyPropertyChanged
{
get
{
- // Сначала проверяем Avatar (может содержать полный URL или data:image)
+ // Если явно указана Avatar (base64 or url)
if (!string.IsNullOrEmpty(Avatar))
{
- // Если это data:image - не возвращаем как URL (будет обработано отдельно)
if (Avatar.StartsWith("data:image"))
return null;
-
- // Если это полный URL
+
if (Avatar.StartsWith("http", StringComparison.OrdinalIgnoreCase))
return Avatar;
-
- // Если это относительный путь
+
if (!Avatar.StartsWith("data:"))
{
string cleanUrl = Avatar.TrimStart('/');
- return $"https://corp.robotail.pro/{cleanUrl}";
+ return ServiceProvider.ApiService.GetFullImageUrl(cleanUrl);
}
}
- if (string.IsNullOrEmpty(AvatarUrl)) return null;
-
- // Handle protocol-relative URLs
- if (AvatarUrl.StartsWith("//"))
- return $"https:{AvatarUrl}";
+ // Используем AvatarUrl (обычно относительный или абсолютный)
+ if (!string.IsNullOrEmpty(AvatarUrl))
+ {
+ return ServiceProvider.ApiService.GetFullImageUrl(AvatarUrl);
+ }
- if (AvatarUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
- return AvatarUrl;
-
- string cleanPath = AvatarUrl.TrimStart('/');
- return $"https://corp.robotail.pro/{cleanPath}";
+ return null;
}
}
@@ -131,7 +139,20 @@ public class User : INotifyPropertyChanged
/// Путь к файлу кэшированного аватара
///
[JsonIgnore]
- public string? CachedAvatarFilePath { get; set; }
+ private string? _cachedAvatarFilePath;
+
+ [JsonIgnore]
+ public string? CachedAvatarFilePath
+ {
+ get => _cachedAvatarFilePath;
+ set
+ {
+ if (_cachedAvatarFilePath == value) return;
+ _cachedAvatarFilePath = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(DisplayAvatarSource));
+ }
+ }
///
/// Флаг, что аватар был успешно загружен
@@ -214,7 +235,6 @@ public class User : INotifyPropertyChanged
}
// Если нет в кэше - возвращаем placeholder
- // Аватар будет загружен асинхронно через LoadUserAvatarAsync
Trace.WriteLine($"[User.DisplayAvatarSource] {FullName}: Not in cache, returning placeholder");
}
}
diff --git a/CORP-MAUI/Pages/DepartmentsPage.xaml b/CORP-MAUI/Pages/DepartmentsPage.xaml
index 1a2a4de..613554f 100644
--- a/CORP-MAUI/Pages/DepartmentsPage.xaml
+++ b/CORP-MAUI/Pages/DepartmentsPage.xaml
@@ -80,26 +80,79 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CORP-MAUI/Pages/DepartmentsPage.xaml.cs b/CORP-MAUI/Pages/DepartmentsPage.xaml.cs
index f8b5d5c..97e2823 100644
--- a/CORP-MAUI/Pages/DepartmentsPage.xaml.cs
+++ b/CORP-MAUI/Pages/DepartmentsPage.xaml.cs
@@ -15,6 +15,8 @@ public partial class DepartmentsPage : ContentPage
{
Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR START ===");
InitializeComponent();
+ // Привязываем пустой источник — будет заполнен при загрузке
+ DepartmentsCollectionView.ItemsSource = new List();
_ = LoadDepartmentsAsync();
Trace.WriteLine("=== DEPARTMENTSPAGE CONSTRUCTOR END ===");
}
@@ -33,7 +35,14 @@ public partial class DepartmentsPage : ContentPage
LoadingIndicator.IsRunning = true;
_allDepartments = await ServiceProvider.ApiService.GetAllDepartmentsAsync();
- RenderDepartments(_allDepartments.OrderBy(d => d.Name).ToList());
+
+ var ordered = _allDepartments.OrderBy(d => d.Name).ToList();
+
+ // Устанавливаем ItemsSource CollectionView
+ DepartmentsCollectionView.ItemsSource = ordered;
+
+ // Асинхронно предзагружаем аватары руководителей
+ _ = PreloadHeadsAvatarsAsync(ordered);
}
catch (Exception ex)
{
@@ -47,215 +56,27 @@ public partial class DepartmentsPage : ContentPage
}
}
- private void RenderDepartments(List 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 departments)
+ private async Task PreloadHeadsAvatarsAsync(List departments)
{
try
{
foreach (var dept in departments)
{
- if (dept.Head?.Id == null)
- continue;
-
- var card = DepartmentsFlexLayout.Children
- .OfType()
- .FirstOrDefault(b => b.ClassId == dept.Id.ToString());
-
- if (card?.Content is VerticalStackLayout vstack)
+ if (dept.Head?.Id == null) continue;
+ try
{
- var avatarStack = vstack.Children.OfType().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)
- {
- // Игнорируем ошибки загрузки отдельных аватаров
- }
- }
+ await ServiceProvider.ApiService.LoadUserAvatarAsync(dept.Head);
+ // После загрузки DisplayAvatarSource уведомит UI через INotifyPropertyChanged
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentsPage] ⚠️ Ошибка загрузки аватара для {dept.Name}: {ex.Message}");
}
}
}
catch (Exception ex)
{
- Trace.WriteLine($"[DepartmentsPage] ⚠️ Ошибка загрузки аватаров: {ex.Message}");
+ Trace.WriteLine($"[DepartmentsPage] ⚠️ PreloadHeadsAvatarsAsync error: {ex.Message}");
}
}
@@ -282,6 +103,98 @@ public partial class DepartmentsPage : ContentPage
).ToList();
}
- RenderDepartments(filtered.OrderBy(d => d.Name).ToList());
+ DepartmentsCollectionView.ItemsSource = filtered.OrderBy(d => d.Name).ToList();
+ }
+
+ ///
+ /// Попытаться извлечь Department из sender или TappedEventArgs
+ ///
+ private static Department? GetDepartmentFromTap(object? sender, TappedEventArgs? e)
+ {
+ // Сначала пробуем Parameter из TappedEventArgs
+ if (e?.Parameter is Department deptFromParam)
+ return deptFromParam;
+
+ // Затем из sender
+ if (sender is TapGestureRecognizer tr)
+ {
+ if (tr.BindingContext is Department deptFromTr) return deptFromTr;
+ if (tr.Parent is BindableObject parent && parent.BindingContext is Department deptFromParent) return deptFromParent;
+ }
+
+ if (sender is BindableObject bo && bo.BindingContext is Department deptFromBo)
+ {
+ return deptFromBo;
+ }
+
+ return null;
+ }
+
+ private async void OnDepartmentCardTapped(object sender, TappedEventArgs e)
+ {
+ try
+ {
+ var dept = GetDepartmentFromTap(sender, e);
+ if (dept == null)
+ {
+ Trace.WriteLine("[DepartmentsPage] ⚠️ Не удалось определить отдел при нажатии");
+ return;
+ }
+
+ Trace.WriteLine($"[DepartmentsPage] >>> Открытие отдела: {dept.Id} - {dept.Name}");
+
+ if (Shell.Current?.Navigation != null)
+ {
+ await Shell.Current.Navigation.PushAsync(new DepartmentDetailsPage(dept.Id));
+ }
+ else
+ {
+ await Navigation.PushAsync(new DepartmentDetailsPage(dept.Id));
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentsPage] ❌ Navigation error: {ex.Message}");
+ try
+ {
+ await Shell.Current.GoToAsync($"{nameof(DepartmentDetailsPage)}?id={(sender as BindableObject)?.BindingContext}");
+ }
+ catch
+ {
+ await DisplayAlert("Ошибка", "Не удалось открыть страницу отдела", "OK");
+ }
+ }
+ }
+
+ // Обработчик выбора в CollectionView: открывает страницу отдела и снимает выбор
+ private async void DepartmentsCollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ try
+ {
+ var selected = e.CurrentSelection?.FirstOrDefault() as Department;
+ if (selected == null) return;
+
+ Trace.WriteLine($"[DepartmentsPage] >>> SelectionChanged -> открываем отдел: {selected.Name}");
+ if (Shell.Current?.Navigation != null)
+ {
+ await Shell.Current.Navigation.PushAsync(new DepartmentDetailsPage(selected.Id));
+ }
+ else
+ {
+ await Navigation.PushAsync(new DepartmentDetailsPage(selected.Id));
+ }
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[DepartmentsPage] ❌ Ошибка в SelectionChanged: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть страницу отдела", "OK");
+ }
+ finally
+ {
+ if (sender is CollectionView cv)
+ {
+ cv.SelectedItem = null;
+ }
+ }
}
}
diff --git a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
index 0fc63cf..49b4aa8 100644
--- a/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
+++ b/CORP-MAUI/Pages/EmployeeProfilePage.xaml.cs
@@ -3,7 +3,7 @@ using CORP_MAUI.Models;
namespace CORP_MAUI.Pages;
-public partial class EmployeeProfilePage : ContentPage
+public partial class EmployeeProfilePage
{
private User? _user;
private readonly int _userId;
@@ -13,14 +13,14 @@ public partial class EmployeeProfilePage : ContentPage
try
{
Trace.WriteLine($"=== EMPLOYEEPROFILEPAGE CONSTRUCTOR START (UserID: {userId}) ===");
-
+
InitializeComponent();
-
+
_userId = userId;
-
+
// Загружаем данные
_ = LoadUserProfileAsync();
-
+
Trace.WriteLine("=== EMPLOYEEPROFILEPAGE CONSTRUCTOR END ===");
}
catch (Exception ex)
@@ -29,13 +29,14 @@ public partial class EmployeeProfilePage : ContentPage
Trace.WriteLine($"[EmployeeProfilePage] ❌ Тип ошибки: {ex.GetType().FullName}");
Trace.WriteLine($"[EmployeeProfilePage] ❌ Сообщение: {ex.Message}");
Trace.WriteLine($"[EmployeeProfilePage] ❌ StackTrace: {ex.StackTrace}");
-
+
Task.Run(async () =>
{
await Task.Delay(100);
- if (Application.Current?.Windows.Count > 0)
+ var app = Application.Current;
+ if (app != null && app.Windows.Count > 0)
{
- Page? mainPage = Application.Current.Windows[0]?.Page;
+ Page? mainPage = app.Windows[0].Page;
if (mainPage != null)
{
await mainPage.DisplayAlert("Ошибка EmployeeProfilePage",
@@ -50,9 +51,47 @@ public partial class EmployeeProfilePage : ContentPage
///
/// Конструктор для передачи готового объекта пользователя
///
- public EmployeeProfilePage(User user) : this(user.Id)
+ public EmployeeProfilePage(User user)
{
- _user = user;
+ try
+ {
+ ArgumentNullException.ThrowIfNull(user);
+
+ Trace.WriteLine($"=== EMPLOYEEPROFILEPAGE CONSTRUCTOR START (User object: {user.FullName}) ===");
+
+ InitializeComponent();
+
+ _user = user;
+ _userId = user.Id;
+
+ // Загружаем данные (будет использован переданный объект, если он полный)
+ _ = LoadUserProfileAsync();
+
+ Trace.WriteLine("=== EMPLOYEEPROFILEPAGE CONSTRUCTOR END ===");
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine("=== EMPLOYEEPROFILEPAGE CONSTRUCTOR ERROR ===");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Тип ошибки: {ex.GetType().FullName}");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ Сообщение: {ex.Message}");
+ Trace.WriteLine($"[EmployeeProfilePage] ❌ StackTrace: {ex.StackTrace}");
+
+ Task.Run(async () =>
+ {
+ await Task.Delay(100);
+ var app = Application.Current;
+ if (app != null && app.Windows.Count > 0)
+ {
+ Page? mainPage = app.Windows[0].Page;
+ if (mainPage != null)
+ {
+ await mainPage.DisplayAlert("Ошибка EmployeeProfilePage",
+ $"Ошибка инициализации:\n{ex.Message}", "OK");
+ }
+ }
+ });
+ throw;
+ }
}
private async Task LoadUserProfileAsync()
@@ -60,7 +99,7 @@ public partial class EmployeeProfilePage : ContentPage
try
{
Trace.WriteLine($"[EmployeeProfilePage] >>> LoadUserProfileAsync START (UserID: {_userId})");
-
+
LoadingIndicator.IsVisible = true;
LoadingIndicator.IsRunning = true;
@@ -80,15 +119,15 @@ public partial class EmployeeProfilePage : ContentPage
}
Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль загружен: {_user.FullName}");
-
+
// Предзагружаем аватар пользователя
Trace.WriteLine($"[EmployeeProfilePage] >>> Загрузка аватара, URL: {_user.DisplayAvatarUrl}");
await ServiceProvider.ApiService.LoadUserAvatarAsync(_user);
Trace.WriteLine($"[EmployeeProfilePage] IsAvatarLoaded: {_user.IsAvatarLoaded}, CachedPath: {_user.CachedAvatarFilePath}");
-
+
// Заполняем основную информацию
FullNameLabel.Text = _user.FullName;
- PositionLabel.Text = _user.PositionName ?? "Должность не указана";
+ PositionLabel.Text = _user.PositionName;
AvatarImage.Source = _user.DisplayAvatarSource;
Trace.WriteLine($"[EmployeeProfilePage] ✅ Аватар установлен");
OnlineIndicator.IsVisible = _user.IsOnline;
@@ -147,7 +186,7 @@ public partial class EmployeeProfilePage : ContentPage
}
Trace.WriteLine($"[EmployeeProfilePage] >>> Навыков: {_user.Skills.Count}");
}
-
+
Trace.WriteLine($"[EmployeeProfilePage] ✅ Профиль отображен успешно");
}
catch (Exception ex)
@@ -155,7 +194,7 @@ public partial class EmployeeProfilePage : ContentPage
Trace.WriteLine($"[EmployeeProfilePage] ❌ Ошибка загрузки: {ex.GetType().Name}");
Trace.WriteLine($"[EmployeeProfilePage] Message: {ex.Message}");
Trace.WriteLine($"[EmployeeProfilePage] StackTrace: {ex.StackTrace}");
-
+
await DisplayAlert("Ошибка", $"Не удалось загрузить профиль сотрудника:\n{ex.Message}", "OK");
}
finally
@@ -171,10 +210,10 @@ public partial class EmployeeProfilePage : ContentPage
return;
Trace.WriteLine($"[EmployeeProfilePage] >>> Открытие чата с: {_user.FullName}");
-
+
try
{
- await Navigation.PushAsync(new ChatPage(_user));
+ await Shell.Current.Navigation.PushAsync(new ChatPage(_user));
}
catch (Exception ex)
{
@@ -185,7 +224,6 @@ public partial class EmployeeProfilePage : ContentPage
private async void OnBackTapped(object sender, TappedEventArgs e)
{
- await Navigation.PopAsync();
+ await Shell.Current.Navigation.PopAsync();
}
}
-
diff --git a/CORP-MAUI/Pages/EmployeesPage.xaml b/CORP-MAUI/Pages/EmployeesPage.xaml
index e1c6876..8a5364c 100644
--- a/CORP-MAUI/Pages/EmployeesPage.xaml
+++ b/CORP-MAUI/Pages/EmployeesPage.xaml
@@ -89,8 +89,12 @@
+ SelectionMode="Single"
+ SelectionChanged="EmployeesCollectionView_SelectionChanged"
+ ItemsLayout="VerticalList"
+ BackgroundColor="Transparent"
+ VerticalOptions="FillAndExpand"
+ Margin="0">
@@ -109,22 +113,26 @@
+ Margin="6,4">
-
+
+ Style="{StaticResource AvatarMedium}"
+ WidthRequest="50"
+ HeightRequest="50">
+ Aspect="AspectFill"
+ WidthRequest="48"
+ HeightRequest="48"/>
+ Margin="8,0">
private async void OnChatIconTapped(object sender, TappedEventArgs e)
{
- if (e.Parameter is User selectedEmployee)
+ var selectedEmployee = GetUserFromTap(sender, e);
+ if (selectedEmployee == null)
{
- Trace.WriteLine($"[EmployeesPage] >>> Открытие чата с: {selectedEmployee.FullName}");
-
- try
- {
- await Navigation.PushAsync(new ChatPage(selectedEmployee));
- }
- catch (Exception ex)
- {
- Trace.WriteLine($"[EmployeesPage] ❌ Ошибка перехода к чату: {ex.Message}");
- await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
- }
+ Trace.WriteLine("[EmployeesPage] ⚠️ Не удалось определить выбранного сотрудника в OnChatIconTapped");
+ return;
+ }
+
+ Trace.WriteLine($"[EmployeesPage] >>> Открытие чата с: {selectedEmployee.FullName}");
+ try
+ {
+ await Shell.Current.Navigation.PushAsync(new ChatPage(selectedEmployee));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeesPage] ❌ Ошибка перехода к чату: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть чат", "OK");
}
}
@@ -206,7 +237,7 @@ public partial class EmployeesPage : ContentPage
{
try
{
- Trace.WriteLine("[EmployeesPage] >>> Начинаем предзагрузку аватаров...");
+ Trace.WriteLine("[EmployeesPage] >>> Начинаем предзагрузку ават��ров...");
await ServiceProvider.ApiService.PreloadUserAvatarsAsync(_allEmployees);
Trace.WriteLine("[EmployeesPage] ✅ Аватары предзагружены");
}
@@ -215,5 +246,33 @@ public partial class EmployeesPage : ContentPage
Trace.WriteLine($"[EmployeesPage] ⚠️ Ошибка предзагрузки аватаров: {ex.Message}");
}
}
-}
+ ///
+ /// SelectionChanged handler: откроет профиль выбранного сотрудника (вызывается при SelectionMode=Single)
+ ///
+ private async void EmployeesCollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ try
+ {
+ var selected = e.CurrentSelection?.FirstOrDefault() as User;
+ if (selected == null)
+ return;
+
+ Trace.WriteLine($"[EmployeesPage] >>> SelectionChanged -> открываем профиль: {selected.FullName}");
+ await Shell.Current.Navigation.PushAsync(new EmployeeProfilePage(selected));
+ }
+ catch (Exception ex)
+ {
+ Trace.WriteLine($"[EmployeesPage] ❌ Ошибка в SelectionChanged: {ex.Message}");
+ await DisplayAlert("Ошибка", "Не удалось открыть профиль сотрудника", "OK");
+ }
+ finally
+ {
+ // Снять выбор, чтобы пользователь мог снова выбрать тот же элемент
+ if (sender is CollectionView cv)
+ {
+ cv.SelectedItem = null;
+ }
+ }
+ }
+}
diff --git a/CORP-MAUI/Resources/Styles/AppStyles.xaml b/CORP-MAUI/Resources/Styles/AppStyles.xaml
index fabd763..4fb572d 100644
--- a/CORP-MAUI/Resources/Styles/AppStyles.xaml
+++ b/CORP-MAUI/Resources/Styles/AppStyles.xaml
@@ -184,7 +184,6 @@
-
@@ -193,7 +192,6 @@
-
diff --git a/CORP-MAUI/Services/ApiService.cs b/CORP-MAUI/Services/ApiService.cs
index 7d15b59..1718d02 100644
--- a/CORP-MAUI/Services/ApiService.cs
+++ b/CORP-MAUI/Services/ApiService.cs
@@ -783,6 +783,25 @@ public class ApiService : IDisposable
}
}
+ ///
+ /// Вернуть полный URL изображения, если передан относительный путь — добавить BaseUrl
+ ///
+ public string? GetFullImageUrl(string? url)
+ {
+ if (string.IsNullOrEmpty(url)) return null;
+
+ // Если это data-uri — не трактуем как URL
+ if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return null;
+
+ // Protocol-relative
+ if (url.StartsWith("//")) return "https:" + url;
+
+ if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return url;
+
+ // Относительный путь — приклеиваем базовый URL
+ return _config?.BaseUrl?.TrimEnd('/') + "/" + url.TrimStart('/');
+ }
+
///
/// Загрузить аватарки для списка пользователей
///
@@ -792,10 +811,7 @@ public class ApiService : IDisposable
foreach (var user in users)
{
- if (!string.IsNullOrEmpty(user.AvatarUrl) && !user.AvatarUrl.StartsWith("http"))
- {
- user.AvatarUrl = $"{_config.BaseUrl.TrimEnd('/')}/{user.AvatarUrl.TrimStart('/')}";
- }
+ user.AvatarUrl = GetFullImageUrl(user.AvatarUrl);
}
return Task.CompletedTask;
}
@@ -874,6 +890,14 @@ public class ApiService : IDisposable
}
}
+ ///
+ /// Получить текущего пользователя (alias для GetCurrentUserAsync)
+ ///
+ public async Task GetUserAsync()
+ {
+ return await GetCurrentUserAsync();
+ }
+
///
/// Получить сотрудника по ID
///
@@ -1450,6 +1474,7 @@ public class ApiService : IDisposable
}
}
+
///
/// Предзагрузить аватары для списка пользователей
///
diff --git a/CORP-MAUI/Services/WebSocketService.cs b/CORP-MAUI/Services/WebSocketService.cs
index fe6b710..3c69bf2 100644
--- a/CORP-MAUI/Services/WebSocketService.cs
+++ b/CORP-MAUI/Services/WebSocketService.cs
@@ -3,6 +3,7 @@ using System.Net.WebSockets;
using System.Text;
using CORP_MAUI.Models;
using Newtonsoft.Json;
+using System.Threading;
namespace CORP_MAUI.Services;
@@ -11,14 +12,10 @@ public class WebSocketService
private ClientWebSocket? _webSocket;
// Возможные варианты WebSocket URL (будем пробовать по порядку)
- private static readonly string[] WebSocketUrls =
+ private static readonly string[] WebSocketUrls = new string[]
{
"wss://corp.robotail.pro/ws",
- "ws://corp.robotail.pro/ws",
- "wss://corp.robotail.pro/ws/",
- "ws://corp.robotail.pro/ws/",
- "wss://corp.robotail.pro/api/ws",
- "wss://corp.robotail.pro/api/v1/ws/chat/"
+ "ws://corp.robotail.pro/ws"
};
private string? _connectedUrl;
@@ -30,7 +27,8 @@ public class WebSocketService
public event EventHandler? UserStatusChanged;
public event EventHandler? ConnectionStatusChanged;
- public bool IsConnected => _isConnected && _webSocket?.State == WebSocketState.Open;
+ // Сделаем выражение полностью булевым (защита от null)
+ public bool IsConnected => _isConnected && (_webSocket?.State == WebSocketState.Open);
public async Task ConnectAsync(string authToken)
{
@@ -139,10 +137,9 @@ public class WebSocketService
{
if (_webSocket is { State: WebSocketState.Open })
{
- if (_cancellationTokenSource != null)
- {
- await _cancellationTokenSource.CancelAsync();
- }
+ // Отмена токена — не асинхронный вызов
+ _cancellationTokenSource?.Cancel();
+
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}