diff --git a/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs b/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs
new file mode 100644
index 0000000..10438f6
--- /dev/null
+++ b/GSS2/ViewModels/AmineContent/BrandCalibrationViewModel.cs
@@ -0,0 +1,366 @@
+using System.Collections.ObjectModel;
+
+using Microsoft.Extensions.Logging;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+using GSS2.Core.Analysis.AmineContent.Database;
+
+using GSS2.UI.Core.ViewModels;
+using GSS2.UI.Core.Services;
+
+namespace GSS2.ViewModels.AmineContent;
+
+public partial class BrandCalibrationViewModel : ViewModelBase
+{
+ ///
+ /// Внутренняя модель элементов меню
+ ///
+ public partial class MenuItemViewModel : ViewModelBase
+ {
+ [ObservableProperty] public partial int Id { get; set; }
+ [ObservableProperty] public partial string BrandName { get; set; }
+ [ObservableProperty] public partial string MixtureName { get; set; }
+
+ public MenuItemViewModel(int id, string brandName, string mixtureName)
+ {
+ Id = id;
+ BrandName = brandName;
+ MixtureName = mixtureName;
+ }
+ }
+
+ ///
+ /// Логгер
+ ///
+ private readonly ILogger _logger;
+ ///
+ /// КОнтекст базы данных
+ ///
+ private readonly Context _context;
+ ///
+ /// ViewLocator для получения других View по их моделям
+ ///
+ private readonly DependencyInjectionViewLocator _viewLocator;
+ ///
+ /// Сервис навигации между экранами
+ ///
+ private readonly NavigationService _navigation;
+
+ ///
+ /// Меню открыто
+ ///
+ [ObservableProperty] public partial bool MenuOpened { get; set; } = false;
+ ///
+ /// Элементы меню
+ ///
+ [ObservableProperty] public partial ObservableCollection MenuItems { get; set; }
+ ///
+ /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
+ ///
+ [ObservableProperty] public partial int MenuSelectedIndex { get; set; }
+ ///
+ /// выбранного элемента
+ ///
+ [ObservableProperty] public partial int SelectedId { get; set; }
+ ///
+ /// выбранного элемента
+ ///
+ [ObservableProperty] public partial string SelectedUuid { get; set; }
+ ///
+ /// выбранного элемента
+ ///
+ [ObservableProperty] public partial string SelectedBrandName { get; set; }
+ ///
+ /// выбранного элемента
+ ///
+ [ObservableProperty] public partial string SelectedMixtureName { get; set; }
+ ///
+ /// выбранного элемента
+ ///
+ [ObservableProperty] public partial double SelectedMixtureNormalRate { get; set; }
+ ///
+ /// Можно ли производить перемещение между экранами
+ ///
+ [ObservableProperty] public partial bool CanScroll { get; set; }
+
+ public BrandCalibrationViewModel(
+ ILogger logger,
+ Context context,
+ DependencyInjectionViewLocator viewLocator,
+ NavigationService navigation
+ )
+ {
+ // Устанавливаем readonly поля
+ _logger = logger;
+ _context = context;
+ _viewLocator = viewLocator;
+ _navigation = navigation;
+ _logger.LogInformation("Инициализация");
+
+ // Заполняем свойства пустыми значениями
+ MenuOpened = new();
+ MenuItems = new();
+ MenuSelectedIndex = -1;
+ CanScroll = true;
+ SelectedId = 0;
+ SelectedUuid = Guid.NewGuid().ToString();
+ SelectedBrandName = "";
+ SelectedMixtureName = "";
+ SelectedMixtureNormalRate = -1;
+
+ // Получаем записи из базы данных
+ MenuItems = new ObservableCollection(_context.BrandRecords.Select(r => new MenuItemViewModel(r.Id, r.BrandName, r.MixtureName)));
+ if (MenuItems.Count() > 0) // Если записи есть,
+ MenuSelectedIndex = 0; // Выбираем первую запись
+ else // Если записей нет,
+ MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
+
+ _logger.LogInformation("Инициализировано");
+ }
+
+ ///
+ /// Вызывается при изменении
+ ///
+ partial void OnMenuSelectedIndexChanged(int value)
+ {
+ // Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
+ if (value >= MenuItems.Count())
+ _logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(MenuSelectedIndex), MenuSelectedIndex, nameof(MenuItems), MenuItems.Count());
+ // Если значение больше или равно 0, то значит выбран один из элементов меню
+ if (value >= 0 && value < MenuItems.Count())
+ {
+ _logger.LogTrace("Выбран элемент из списка: {}", value);
+ // Находим запись в базе данных
+ var selectedRecord = _context.BrandRecords.Find(MenuItems[value].Id);
+ // Если запись найдена, то заполняем поля из базы данных
+ if (selectedRecord is not null)
+ {
+ SelectedId = selectedRecord.Id;
+ SelectedUuid = selectedRecord.Uuid;
+ SelectedBrandName = selectedRecord.BrandName;
+ SelectedMixtureName = selectedRecord.MixtureName;
+ SelectedMixtureNormalRate = selectedRecord.MixtureNormalRate;
+ return;
+ }
+ // Если значение не найдено в базе данных
+ else
+ {
+ // Логгируем как ошибку
+ _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", MenuItems[value].Id, nameof(_context.BrandRecords));
+ var nonExistingIndex = MenuSelectedIndex; // Сохраняем несуществующий индекс
+ // Устанавливаем, что запись не выбрана, этот метод вызовется повторно и заполнит поля пустыми значениями
+ MenuSelectedIndex = -1;
+ // Удаляем несуществующий элемент из списка
+ MenuItems.RemoveAt(nonExistingIndex);
+ return;
+ }
+ }
+
+ // Если элемент не выбран, заполняем поля пустыми значениями
+ _logger.LogTrace("Выбран новый элемент");
+ SelectedId = 0;
+ SelectedUuid = Guid.NewGuid().ToString();
+ SelectedBrandName = "";
+ SelectedMixtureName = "";
+ SelectedMixtureNormalRate = -1;
+ }
+
+ ///
+ /// Вызывается при изменении
+ ///
+ partial void OnMenuOpenedChanged(bool value)
+ {
+ CanScroll = !value;
+ }
+
+
+ // Команды управления меню
+ ///
+ /// Открыть меню
+ ///
+ [RelayCommand] private void OpenMenu() => MenuOpened = true;
+ ///
+ /// Закрыть меню
+ ///
+ [RelayCommand] private void CloseMenu() => MenuOpened = false;
+ ///
+ /// Переключить видимость меню
+ ///
+ [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
+
+ ///
+ /// Создать новую запись
+ ///
+ [RelayCommand]
+ private void CreateRecord()
+ {
+ _logger.LogInformation("Создание новой записи");
+ // Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
+ // при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
+ // потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
+ // поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
+ if (MenuSelectedIndex != -1)
+ MenuSelectedIndex = -1;
+ else
+ OnMenuSelectedIndexChanged(-1);
+ }
+
+ ///
+ /// Удалить выбранную запись из базы данных или очистить значения, если запись не выбрана
+ ///
+ [RelayCommand]
+ private void DeleteRecord()
+ {
+ try
+ {
+ _logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
+ // Если запись не выбрана, то просто создаём новую запись
+ if (MenuSelectedIndex < 0)
+ {
+ _logger.LogTrace("Запись не выбрана, создаём новую");
+ CreateRecord();
+ return;
+ }
+
+ // Создаём модель для экрана подтверждения
+ var confirmViewModel = new DeleteConfirmViewModel(_navigation)
+ {
+ Question = "Вы уверены, что хотите удалить запись?"
+ };
+
+ // Создаём коллбек который вызовется при экрана подтверждения
+ void OnConfirmCloses(object? sender, bool result)
+ {
+ // Если пользователь отменил удаление, то выходим из коллбека
+ if (!result)
+ {
+ _logger.LogTrace("Пользователь отменил удаление");
+ return;
+ }
+
+ // Если запись не выбрана, то просто создаём новую запись
+ if (MenuSelectedIndex < 0)
+ {
+ _logger.LogTrace("Запись не выбрана, создаём новую");
+ CreateRecord();
+ return;
+ }
+
+ // Сохраняем индекс удалённый записи и находим её в базе данных
+ var deletedMenuIndex = MenuSelectedIndex;
+ var deletedRecordId = SelectedId;
+ var deletedRecord = _context.BrandRecords.Find(deletedRecordId);
+ // Создаём новую запись
+ CreateRecord();
+
+ // Удаляем запись из списка
+ MenuItems.RemoveAt(deletedMenuIndex);
+ // Если удалённая запись не найдена в базе данных, то логгируем как ошибку
+ if (deletedRecord is null)
+ {
+ _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords));
+ return;
+ }
+
+ // Удаляем запись из базы данных и сохраняем изменения в базе данных
+ _context.BrandRecords.Remove(deletedRecord);
+ _context.SaveChanges();
+ _logger.LogTrace("Запись удалена");
+
+ confirmViewModel.Closes -= OnConfirmCloses; // Отписываемся от события закрытия экрана подтверждения
+ }
+
+ // Создаём View экрана подтверждения
+ var confirmView = _viewLocator.Build(confirmViewModel);
+ // Если View не найден, то логгируем как ошибку
+ if (confirmView is null)
+ {
+ _logger.LogError("Не удалось найти View для экрана подтверждения");
+ return;
+ }
+
+ _logger.LogTrace("Задаём вопрос");
+ // Подписываемся на событие закрытия экрана подтверждения
+ confirmViewModel.Closes += OnConfirmCloses;
+ // Преходим на экран подтверждения, при закрытии он сам вернёт экран обратно на текущий
+ _navigation.NavigateTo(confirmView);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Исключение при удалении записи");
+ }
+ }
+
+ ///
+ /// Сохранить текущие значения в новую запись или перезаписать значения в выдранной записи
+ ///
+ [RelayCommand]
+ private void SaveRecord()
+ {
+ try
+ {
+ _logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName);
+ // Если запись не выбрана
+ if (MenuSelectedIndex < 0)
+ {
+ _logger.LogTrace("Создаём новую запись");
+ // Создаём новую запись
+ var newRecord = new BrandRecord(
+ uuid: SelectedUuid,
+ brandName: SelectedBrandName,
+ mixtureName: SelectedMixtureName,
+ mixtureNormalRate: SelectedMixtureNormalRate
+ );
+
+ // При добавляем запись в базу данных
+ var attachedRecord = _context.BrandRecords.Add(newRecord);
+
+ // Сохраняем изменения в базе данных
+ _context.SaveChanges();
+ _logger.LogTrace("Запись сохранена");
+
+ // Добавляем новый элемент в меню
+ MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.BrandName, attachedRecord.Entity.MixtureName));
+ // Выбираем последний элемент из меню
+ MenuSelectedIndex = MenuItems.Count() - 1;
+ }
+ // Если запись выбрана
+ else
+ {
+ // Находим запись в базе данных
+ var attachedRecord = _context.BrandRecords.Find(SelectedId);
+ // Если запись не найдена, то логгируем как ошибку
+ if (attachedRecord is null)
+ {
+ _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.BrandRecords));
+ return;
+ }
+
+ _logger.LogTrace("Записываем изменения в базу данных");
+ // Если в данных есть изменения, то записываем их
+ if (attachedRecord.BrandName != SelectedBrandName)
+ attachedRecord.BrandName = SelectedBrandName;
+ if (attachedRecord.MixtureName != SelectedMixtureName)
+ attachedRecord.MixtureName = SelectedMixtureName;
+ if (attachedRecord.MixtureNormalRate != SelectedMixtureNormalRate)
+ attachedRecord.MixtureNormalRate = SelectedMixtureNormalRate;
+
+ // Записываем изменения в меню
+ _logger.LogTrace("Записываем изменения в меню");
+ MenuItems[MenuSelectedIndex].BrandName = SelectedBrandName;
+ MenuItems[MenuSelectedIndex].MixtureName = SelectedMixtureName;
+
+ // Сохраняем изменения в базе данных
+ _context.SaveChanges();
+ _logger.LogTrace("Запись сохранена");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Исключение при сохранении записи");
+ }
+ }
+
+}
diff --git a/GSS2/Views/AmineContent/BrandCalibrationView.axaml b/GSS2/Views/AmineContent/BrandCalibrationView.axaml
new file mode 100644
index 0000000..0b2b582
--- /dev/null
+++ b/GSS2/Views/AmineContent/BrandCalibrationView.axaml
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+ 5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GSS2/Views/AmineContent/BrandCalibrationView.axaml.cs b/GSS2/Views/AmineContent/BrandCalibrationView.axaml.cs
new file mode 100644
index 0000000..d9dff9d
--- /dev/null
+++ b/GSS2/Views/AmineContent/BrandCalibrationView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace GSS2.Views.AmineContent;
+
+public partial class BrandCalibrationView : UserControl
+{
+ public BrandCalibrationView()
+ {
+ InitializeComponent();
+ }
+}