Compare commits
No commits in common. "3fc825e54b18917a54d317c06ab30f17a4092e26" and "132ce1531f7b5c6427e03cb320388af3a7777ec3" have entirely different histories.
3fc825e54b
...
132ce1531f
@ -18,10 +18,6 @@ public partial class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
RequestedThemeVariant = Elfisa.Core.SettingsStore.Current.Theme == "Dark"
|
|
||||||
? global::Avalonia.Styling.ThemeVariant.Dark
|
|
||||||
: global::Avalonia.Styling.ThemeVariant.Light;
|
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
|
|||||||
@ -52,22 +52,14 @@ dotnet publish src/Elfisa.Avalonia -c Release -r win-x64 --self-contained #
|
|||||||
|
|
||||||
| Раздел | Что нужно | Сложность |
|
| Раздел | Что нужно | Сложность |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ✅ Прайс-лист | поиск + грид (/api/supplier-prices/summary) | готово (без добавления в заказ) |
|
| Прайс-лист | поиск/каталог, наценки, добавление в заказ, большой грид | высокая |
|
||||||
| ✅ Заказы | список + карточка заказа с позициями | готово |
|
| Заказы | список + карточка заказа + позиции | средняя |
|
||||||
| ✅ Накладные | реестр приходных документов | готово |
|
| Накладные | список приходов | средняя |
|
||||||
| ✅ Отчёты | 4 типа + ИИ → Word/Excel | готово |
|
| Отправить | сборка заявки + выгрузка DBF (портировать `orderexport`) | средняя |
|
||||||
| ✅ Справочник | профиль + грузополучатели | готово |
|
| Отказы | список отказных позиций | низкая |
|
||||||
| ✅ Отправить | поиск → корзина → создание заказа (POST /api/buyer/orders) | готов UI; реальная отправка не тестировалась (создаёт заказ на проде) |
|
| Справочник | контрагенты, грузополучатели, настройки | средняя |
|
||||||
| ⏳ Отказы | список отказных позиций | нужен серверный эндпоинт (RefuseItemsCount есть только в локальной БД WinForms) |
|
| Автообновление | аналог AutoUpdater под каждую ОС | средняя |
|
||||||
| ⏳ Автообновление | аналог AutoUpdater под каждую ОС | не начато |
|
|
||||||
|
|
||||||
**Прайс-лист/Заказы/Накладные** строятся на рабочих эндпоинтах
|
**Порядок рекомендую:** Заказы → Прайс-лист → Отправить → остальное.
|
||||||
(`supplier-prices/summary`, `buyer/report`). «Родные» `buyer/orders` и
|
Бизнес-логика каждого — обычный C#, переносится как есть; переписывается
|
||||||
`buyer/catalog` возвращают 500 (баг квотинга PascalCase в GORM-билдере).
|
только UI (WinForms → Avalonia XAML), что и есть основная работа.
|
||||||
Фикс для **buyer/orders** готов и застейджен (`es_api_service.exe.new`,
|
|
||||||
apply-new-exe.bat) — после деплоя Заказы/Накладные перейдут на него.
|
|
||||||
Каталог не используется (берём рабочий summary), его фикс — по желанию.
|
|
||||||
|
|
||||||
Осталось: **Отправить** (корзина + создание заказа + DBF) и **Отказы**,
|
|
||||||
плюс автообновление и сохранение сессии. Логика переносится из WinForms,
|
|
||||||
переписывается только UI (XAML).
|
|
||||||
|
|||||||
@ -123,8 +123,8 @@
|
|||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ListBox.topnav ListBoxItem">
|
<Style Selector="ListBox.topnav ListBoxItem">
|
||||||
<Setter Property="Padding" Value="6,7"/>
|
<Setter Property="Padding" Value="10,7"/>
|
||||||
<Setter Property="Margin" Value="2,0"/>
|
<Setter Property="Margin" Value="3,0"/>
|
||||||
<Setter Property="CornerRadius" Value="10"/>
|
<Setter Property="CornerRadius" Value="10"/>
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
</Style>
|
</Style>
|
||||||
@ -135,30 +135,6 @@
|
|||||||
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
|
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- ============ Вкладки-документы ============ -->
|
|
||||||
<Style Selector="ListBox.tabs">
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Padding" Value="0"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tabs ListBoxItem">
|
|
||||||
<Setter Property="Padding" Value="12,6"/>
|
|
||||||
<Setter Property="Margin" Value="3,6"/>
|
|
||||||
<Setter Property="CornerRadius" Value="8"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource Muted}"/>
|
|
||||||
<Setter Property="Cursor" Value="Hand"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tabs ListBoxItem:pointerover /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource RowAlt}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tabs ListBoxItem:selected /template/ ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AccentSoft}"/>
|
|
||||||
</Style>
|
|
||||||
<Style Selector="ListBox.tabs ListBoxItem:selected">
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource AccentPressed}"/>
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<!-- ============ Таблица ============ -->
|
<!-- ============ Таблица ============ -->
|
||||||
<Style Selector="DataGrid">
|
<Style Selector="DataGrid">
|
||||||
<Setter Property="Background" Value="{DynamicResource Surface}"/>
|
<Setter Property="Background" Value="{DynamicResource Surface}"/>
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
public partial class CartItem : ObservableObject
|
|
||||||
{
|
|
||||||
public PriceItem Source { get; }
|
|
||||||
public string Name => Source.DrugName ?? "";
|
|
||||||
public string Supplier => Source.SupplierName ?? "";
|
|
||||||
public decimal Price => Source.Price;
|
|
||||||
|
|
||||||
[ObservableProperty] private decimal _qty = 1;
|
|
||||||
public decimal LineSum => Price * Qty;
|
|
||||||
|
|
||||||
public event Action? Changed;
|
|
||||||
|
|
||||||
public CartItem(PriceItem src) => Source = src;
|
|
||||||
|
|
||||||
partial void OnQtyChanged(decimal value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(LineSum));
|
|
||||||
Changed?.Invoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Единая корзина заказа — общая для «Прайс-листа» и «Отправить».</summary>
|
|
||||||
public sealed class CartService
|
|
||||||
{
|
|
||||||
public static CartService Instance { get; } = new();
|
|
||||||
|
|
||||||
public ObservableCollection<CartItem> Items { get; } = new();
|
|
||||||
public event Action? Changed;
|
|
||||||
|
|
||||||
public decimal Total => Items.Sum(c => c.LineSum);
|
|
||||||
public int Count => Items.Count;
|
|
||||||
|
|
||||||
public void Add(PriceItem src, decimal qty = 1)
|
|
||||||
{
|
|
||||||
if (src is null || string.IsNullOrEmpty(src.SupplierPriceId)) return;
|
|
||||||
var existing = Items.FirstOrDefault(c => c.Source.SupplierPriceId == src.SupplierPriceId);
|
|
||||||
if (existing != null) existing.Qty += qty;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var ci = new CartItem(src) { Qty = qty };
|
|
||||||
ci.Changed += Raise;
|
|
||||||
Items.Add(ci);
|
|
||||||
}
|
|
||||||
Raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Remove(CartItem item)
|
|
||||||
{
|
|
||||||
if (item is null) return;
|
|
||||||
item.Changed -= Raise;
|
|
||||||
Items.Remove(item);
|
|
||||||
Raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
foreach (var i in Items) i.Changed -= Raise;
|
|
||||||
Items.Clear();
|
|
||||||
Raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Raise() => Changed?.Invoke();
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
/// <summary>Справочник покупателя: профиль + грузополучатели (адреса).</summary>
|
|
||||||
public partial class DirectoryViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
|
|
||||||
public string UserName => Session.Current.Username ?? "";
|
|
||||||
public string RoleLabel => Session.Current.Role switch
|
|
||||||
{
|
|
||||||
"buyer" => "Покупатель",
|
|
||||||
"supplier" => "Поставщик",
|
|
||||||
"manager" => "Менеджер",
|
|
||||||
"admin" => "Администратор",
|
|
||||||
_ => Session.Current.Role ?? ""
|
|
||||||
};
|
|
||||||
|
|
||||||
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private string? _status;
|
|
||||||
|
|
||||||
public DirectoryViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
if (Session.Current.IsAuthenticated) _ = LoadAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
var locs = await _api.GetBuyerLocationsAsync();
|
|
||||||
Locations.Clear();
|
|
||||||
foreach (var l in locs) Locations.Add(l);
|
|
||||||
if (Locations.Count == 0) Status = "Грузополучатели не заданы.";
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
public sealed class InvoiceRow
|
|
||||||
{
|
|
||||||
public string Date { get; init; } = "";
|
|
||||||
public string Number { get; init; } = "";
|
|
||||||
public string Supplier { get; init; } = "";
|
|
||||||
public string Location { get; init; } = "";
|
|
||||||
public int ItemsCount { get; init; }
|
|
||||||
public decimal Sum { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Накладные — реестр приходных документов. Строится из /api/buyer/report
|
|
||||||
/// (после деплоя фикса /api/buyer/orders можно перейти на «родной» источник).
|
|
||||||
/// </summary>
|
|
||||||
public partial class InvoicesViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
|
|
||||||
public ObservableCollection<InvoiceRow> Rows { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private string? _status;
|
|
||||||
[ObservableProperty] private string _totals = "";
|
|
||||||
|
|
||||||
public InvoicesViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
if (Session.Current.IsAuthenticated) _ = LoadAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
var to = DateTime.Today;
|
|
||||||
var from = to.AddMonths(-24);
|
|
||||||
var lines = await _api.GetBuyerReportAsync(from, to);
|
|
||||||
|
|
||||||
Rows.Clear();
|
|
||||||
foreach (var g in lines.GroupBy(l => l.OrderId)
|
|
||||||
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
|
||||||
{
|
|
||||||
var f = g.First();
|
|
||||||
Rows.Add(new InvoiceRow
|
|
||||||
{
|
|
||||||
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
|
||||||
Number = f.GlobalSign ?? "",
|
|
||||||
Supplier = string.Join(", ", g.Select(x => x.Supplier)
|
|
||||||
.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
|
||||||
Location = f.Location ?? "",
|
|
||||||
ItemsCount = g.Count(),
|
|
||||||
Sum = g.Sum(x => x.Sum)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Totals = $"Накладных: {Rows.Count} Сумма: {Rows.Sum(r => r.Sum):N2}";
|
|
||||||
if (Rows.Count == 0) Status = "Накладных за последние 2 года нет.";
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
/// <summary>«Загрузить» — скачивание актуального прайса с сервера в память.</summary>
|
|
||||||
public partial class LoadPriceViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private double _progress; // 0..100
|
|
||||||
[ObservableProperty] private string _progressText = "";
|
|
||||||
[ObservableProperty] private string? _result;
|
|
||||||
|
|
||||||
public string LastLoaded =>
|
|
||||||
PriceCache.LoadedAt is { } dt
|
|
||||||
? $"Последняя загрузка: {dt:dd.MM.yyyy HH:mm} — {PriceCache.Items.Count} позиций"
|
|
||||||
: "Прайс ещё не загружался в этой сессии.";
|
|
||||||
|
|
||||||
public LoadPriceViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
|
||||||
Result = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
Progress = 0;
|
|
||||||
ProgressText = "Соединение с сервером…";
|
|
||||||
|
|
||||||
var all = new List<PriceItem>();
|
|
||||||
const int page = 500;
|
|
||||||
int offset = 0, total = 0;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var resp = await _api.GetPriceSummaryAsync(null, limit: page, offset: offset);
|
|
||||||
var batch = resp.Summary ?? new();
|
|
||||||
if (resp.TotalDrugs > 0) total = resp.TotalDrugs;
|
|
||||||
all.AddRange(batch);
|
|
||||||
|
|
||||||
Progress = total > 0 ? Math.Min(100.0, all.Count * 100.0 / total) : 0;
|
|
||||||
ProgressText = total > 0
|
|
||||||
? $"Загружено {all.Count} из {total} позиций…"
|
|
||||||
: $"Загружено {all.Count} позиций…";
|
|
||||||
|
|
||||||
offset += batch.Count;
|
|
||||||
if (batch.Count < page || (total > 0 && all.Count >= total) || offset > 30000)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
PriceCache.Items = all;
|
|
||||||
PriceCache.LoadedAt = DateTime.Now;
|
|
||||||
Progress = 100;
|
|
||||||
ProgressText = "Готово.";
|
|
||||||
|
|
||||||
var suppliers = all.Select(x => x.SupplierName).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct().Count();
|
|
||||||
Result = $"Прайс загружен: {all.Count} позиций от {suppliers} поставщик(ов).";
|
|
||||||
OnPropertyChanged(nameof(LastLoaded));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Result = "Ошибка загрузки: " + ex.Message;
|
|
||||||
ProgressText = "";
|
|
||||||
}
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,8 +15,6 @@ public sealed class OrderItemRow
|
|||||||
public decimal Qty { get; init; }
|
public decimal Qty { get; init; }
|
||||||
public decimal Price { get; init; }
|
public decimal Price { get; init; }
|
||||||
public decimal Sum { get; init; }
|
public decimal Sum { get; init; }
|
||||||
public string? SupplierPriceId { get; init; }
|
|
||||||
public string? Barcode { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OrderSummary
|
public sealed class OrderSummary
|
||||||
@ -29,16 +27,12 @@ public sealed class OrderSummary
|
|||||||
public int ItemsCount { get; init; }
|
public int ItemsCount { get; init; }
|
||||||
public decimal Sum { get; init; }
|
public decimal Sum { get; init; }
|
||||||
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
|
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
|
||||||
|
|
||||||
public bool IsDraft { get; init; }
|
|
||||||
public string? DraftId { get; init; }
|
|
||||||
public string? LocationId { get; init; }
|
|
||||||
public string? Comment { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Заказы: локальные черновики (статус «Не отправлен») + серверные заказы (из /api/buyer/report).
|
/// Заказы: список + состав. Пока строится из /api/buyer/report (он работает);
|
||||||
/// Черновик можно отправить (создать реальный заказ) или удалить.
|
/// после деплоя фикса /api/buyer/orders можно перейти на «родной» эндпоинт
|
||||||
|
/// (даст ещё PlacedAt, Comment, отмену/размещение).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class OrdersViewModel : ViewModelBase
|
public partial class OrdersViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
@ -63,38 +57,11 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Busy = true;
|
Busy = true;
|
||||||
Orders.Clear();
|
|
||||||
|
|
||||||
// 1) Локальные черновики — вверху, статус «Не отправлен»
|
|
||||||
foreach (var d in DraftStore.All)
|
|
||||||
{
|
|
||||||
var items = new ObservableCollection<OrderItemRow>(
|
|
||||||
d.Items.Select(i => new OrderItemRow
|
|
||||||
{
|
|
||||||
Name = i.Name, Qty = i.Qty, Price = i.Price, Sum = i.LineSum,
|
|
||||||
SupplierPriceId = i.SupplierPriceId, Barcode = i.Barcode
|
|
||||||
}));
|
|
||||||
Orders.Add(new OrderSummary
|
|
||||||
{
|
|
||||||
Date = d.CreatedAt.ToString("dd.MM.yyyy"),
|
|
||||||
Number = d.Number,
|
|
||||||
Supplier = string.Join(", ", d.Items.Select(i => i.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
|
||||||
Location = d.LocationAddress ?? "",
|
|
||||||
Status = "Не отправлен",
|
|
||||||
ItemsCount = d.ItemsCount,
|
|
||||||
Sum = d.Total,
|
|
||||||
Items = items,
|
|
||||||
IsDraft = true,
|
|
||||||
DraftId = d.Id,
|
|
||||||
LocationId = d.LocationId,
|
|
||||||
Comment = d.Comment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Серверные заказы
|
|
||||||
var to = DateTime.Today;
|
var to = DateTime.Today;
|
||||||
var from = to.AddMonths(-24);
|
var from = to.AddMonths(-24);
|
||||||
var lines = await _api.GetBuyerReportAsync(from, to);
|
var lines = await _api.GetBuyerReportAsync(from, to);
|
||||||
|
|
||||||
|
Orders.Clear();
|
||||||
foreach (var g in lines.GroupBy(l => l.OrderId)
|
foreach (var g in lines.GroupBy(l => l.OrderId)
|
||||||
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
||||||
{
|
{
|
||||||
@ -102,14 +69,19 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
var items = new ObservableCollection<OrderItemRow>(
|
var items = new ObservableCollection<OrderItemRow>(
|
||||||
g.Select(l => new OrderItemRow
|
g.Select(l => new OrderItemRow
|
||||||
{
|
{
|
||||||
Name = l.ItemName ?? "", Code = l.ItemCode ?? "",
|
Name = l.ItemName ?? "",
|
||||||
Qty = l.Qty, Price = l.UnitPrice, Sum = l.Sum
|
Code = l.ItemCode ?? "",
|
||||||
|
Qty = l.Qty,
|
||||||
|
Price = l.UnitPrice,
|
||||||
|
Sum = l.Sum
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Orders.Add(new OrderSummary
|
Orders.Add(new OrderSummary
|
||||||
{
|
{
|
||||||
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
||||||
Number = f.GlobalSign ?? "",
|
Number = f.GlobalSign ?? "",
|
||||||
Supplier = string.Join(", ", g.Select(x => x.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
Supplier = string.Join(", ", g.Select(x => x.Supplier)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
||||||
Location = f.Location ?? "",
|
Location = f.Location ?? "",
|
||||||
Status = f.Status ?? "",
|
Status = f.Status ?? "",
|
||||||
ItemsCount = g.Count(),
|
ItemsCount = g.Count(),
|
||||||
@ -119,47 +91,9 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectedOrder = Orders.FirstOrDefault();
|
SelectedOrder = Orders.FirstOrDefault();
|
||||||
|
if (Orders.Count == 0) Status = "Заказов за последние 2 года нет.";
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
catch (Exception ex) { Status = ex.Message; }
|
||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SendDraftAsync()
|
|
||||||
{
|
|
||||||
if (SelectedOrder is not { IsDraft: true } d) return;
|
|
||||||
Status = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
var req = new BuyerOrderCreateRequest
|
|
||||||
{
|
|
||||||
GlobalSign = d.Number.StartsWith("EX-", StringComparison.OrdinalIgnoreCase) ? d.Number : null,
|
|
||||||
LocationId = d.LocationId,
|
|
||||||
Comment = d.Comment,
|
|
||||||
Items = d.Items.Select(i => new BuyerOrderItemReq
|
|
||||||
{
|
|
||||||
SupplierPriceId = i.SupplierPriceId ?? "",
|
|
||||||
Qty = (double)i.Qty,
|
|
||||||
ItemName = i.Name,
|
|
||||||
Barcode = i.Barcode
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
var resp = await _api.CreateOrderAsync(req);
|
|
||||||
if (d.DraftId is { } id) DraftStore.Remove(id);
|
|
||||||
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} отправлен ({resp.ItemsCount} поз., {resp.TotalAmount:N2}).";
|
|
||||||
await LoadAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task DeleteDraftAsync()
|
|
||||||
{
|
|
||||||
if (SelectedOrder is not { IsDraft: true } d || d.DraftId is not { } id) return;
|
|
||||||
DraftStore.Remove(id);
|
|
||||||
await LoadAsync();
|
|
||||||
Status = "Черновик удалён.";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,250 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
/// <summary>Строка предложения с редактируемым количеством «Заказ» (как колонка «Заказ» в WinForms).</summary>
|
|
||||||
public partial class OfferRow : ObservableObject
|
|
||||||
{
|
|
||||||
public PriceItem Source { get; }
|
|
||||||
public string? DrugName => Source.DrugName;
|
|
||||||
public string? SupplierName => Source.SupplierName;
|
|
||||||
public decimal Price => Source.Price;
|
|
||||||
public decimal Quantity => Source.Quantity;
|
|
||||||
|
|
||||||
[ObservableProperty] private decimal _orderQty = 1;
|
|
||||||
|
|
||||||
public OfferRow(PriceItem src) => Source = src;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Прайс-лист + сборка заказа. Каталог — как в старой программе:
|
|
||||||
/// база (название) → дозировка (МГ) → предложения поставщиков. Справа «Мой заказ».
|
|
||||||
/// «Сохранить заказ» кладёт черновик в «Заказы» (статус «Не отправлен»).
|
|
||||||
/// </summary>
|
|
||||||
public partial class PriceListViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
private readonly CartService _cart = CartService.Instance;
|
|
||||||
|
|
||||||
// Плоский список предложений + предвычисленные база/МГ (как «Базовое наименование»/«МГ» в WinForms).
|
|
||||||
private readonly List<(PriceItem Item, string BaseName, string Mg)> _indexed = new();
|
|
||||||
|
|
||||||
private const string EmptyMgDisplay = "(без МГ)";
|
|
||||||
private static readonly Regex DosageRegex = new(
|
|
||||||
@"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
|
|
||||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
||||||
|
|
||||||
// Три панели дрилдауна
|
|
||||||
public ObservableCollection<string> BaseNames { get; } = new();
|
|
||||||
public ObservableCollection<string> MgOptions { get; } = new();
|
|
||||||
public ObservableCollection<OfferRow> Offers { get; } = new();
|
|
||||||
|
|
||||||
public ObservableCollection<CartItem> Cart => _cart.Items;
|
|
||||||
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private string _search = "";
|
|
||||||
[ObservableProperty] private string? _selectedBaseName;
|
|
||||||
[ObservableProperty] private string? _selectedMg;
|
|
||||||
[ObservableProperty] private OfferRow? _selectedOffer;
|
|
||||||
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
|
|
||||||
[ObservableProperty] private string _comment = "";
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private string? _status;
|
|
||||||
[ObservableProperty] private int _total;
|
|
||||||
[ObservableProperty] private decimal _cartTotal;
|
|
||||||
|
|
||||||
public PriceListViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
_cart.Changed += Recalc;
|
|
||||||
Recalc();
|
|
||||||
if (Session.Current.IsAuthenticated)
|
|
||||||
{
|
|
||||||
_ = SearchAsync();
|
|
||||||
_ = LoadLocationsAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Recalc() => CartTotal = _cart.Total;
|
|
||||||
|
|
||||||
private async Task LoadLocationsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var locs = await _api.GetBuyerLocationsAsync();
|
|
||||||
Locations.Clear();
|
|
||||||
foreach (var l in locs) Locations.Add(l);
|
|
||||||
var savedId = SettingsStore.Current.DefaultLocationId;
|
|
||||||
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
|
|
||||||
?? Locations.FirstOrDefault(l => l.IsDefault)
|
|
||||||
?? Locations.FirstOrDefault();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SearchAsync()
|
|
||||||
{
|
|
||||||
Status = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
var resp = await _api.GetPriceSummaryAsync(Search, limit: 300);
|
|
||||||
RebuildIndex(resp.Summary ?? new());
|
|
||||||
Total = resp.TotalDrugs;
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Пересобирает индекс база/МГ и список базовых наименований.</summary>
|
|
||||||
private void RebuildIndex(List<PriceItem> items)
|
|
||||||
{
|
|
||||||
_indexed.Clear();
|
|
||||||
foreach (var it in items)
|
|
||||||
{
|
|
||||||
var mg = ExtractDosage(it.DrugName);
|
|
||||||
var baseName = BuildBaseName(it.DrugName, it.TradeName, mg);
|
|
||||||
if (string.IsNullOrWhiteSpace(baseName)) continue;
|
|
||||||
_indexed.Add((it, baseName, mg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сброс выбора и панелей
|
|
||||||
SelectedBaseName = null;
|
|
||||||
MgOptions.Clear();
|
|
||||||
Offers.Clear();
|
|
||||||
SelectedOffer = null;
|
|
||||||
|
|
||||||
var names = _indexed
|
|
||||||
.Select(x => x.BaseName)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
BaseNames.Clear();
|
|
||||||
foreach (var n in names) BaseNames.Add(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
// База выбрана → заполняем список дозировок.
|
|
||||||
partial void OnSelectedBaseNameChanged(string? value)
|
|
||||||
{
|
|
||||||
MgOptions.Clear();
|
|
||||||
Offers.Clear();
|
|
||||||
SelectedOffer = null;
|
|
||||||
SelectedMg = null;
|
|
||||||
if (string.IsNullOrEmpty(value)) return;
|
|
||||||
|
|
||||||
var mgs = _indexed
|
|
||||||
.Where(x => string.Equals(x.BaseName, value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.Select(x => string.IsNullOrEmpty(x.Mg) ? EmptyMgDisplay : x.Mg)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(m => m, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var m in mgs) MgOptions.Add(m);
|
|
||||||
|
|
||||||
// Если дозировка одна — сразу её выбираем (меньше кликов).
|
|
||||||
if (MgOptions.Count == 1) SelectedMg = MgOptions[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Дозировка выбрана → показываем предложения (фильтр база + МГ).
|
|
||||||
partial void OnSelectedMgChanged(string? value)
|
|
||||||
{
|
|
||||||
Offers.Clear();
|
|
||||||
SelectedOffer = null;
|
|
||||||
if (string.IsNullOrEmpty(SelectedBaseName) || string.IsNullOrEmpty(value)) return;
|
|
||||||
|
|
||||||
var wantMg = string.Equals(value, EmptyMgDisplay, StringComparison.Ordinal) ? "" : value;
|
|
||||||
|
|
||||||
var offers = _indexed
|
|
||||||
.Where(x => string.Equals(x.BaseName, SelectedBaseName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& string.Equals(x.Mg, wantMg, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.OrderBy(x => x.Item.Price)
|
|
||||||
.Select(x => new OfferRow(x.Item))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var o in offers) Offers.Add(o);
|
|
||||||
SelectedOffer = Offers.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExtractDosage(string? drugName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(drugName)) return string.Empty;
|
|
||||||
var m = DosageRegex.Match(drugName);
|
|
||||||
return m.Success ? m.Value.Trim() : string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildBaseName(string? drugName, string? tradeName, string dosage)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(tradeName)) return tradeName.Trim();
|
|
||||||
|
|
||||||
var name = drugName ?? string.Empty;
|
|
||||||
if (!string.IsNullOrWhiteSpace(dosage))
|
|
||||||
{
|
|
||||||
var idx = name.IndexOf(dosage, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (idx >= 0) name = name.Remove(idx, dosage.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
name = Regex.Replace(name, @"\s{2,}", " ").Trim(' ', ',', '-', '.', ';');
|
|
||||||
return string.IsNullOrWhiteSpace(name) ? (drugName ?? string.Empty).Trim() : name;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void AddToOrder() => AddSelectedOffer();
|
|
||||||
|
|
||||||
/// <summary>Добавляет выбранное предложение в заказ с его количеством (Enter в таблице / кнопка).</summary>
|
|
||||||
public void AddSelectedOffer()
|
|
||||||
{
|
|
||||||
if (SelectedOffer is null) { Status = "Выберите предложение в списке."; return; }
|
|
||||||
var q = SelectedOffer.OrderQty <= 0 ? 1 : SelectedOffer.OrderQty;
|
|
||||||
_cart.Add(SelectedOffer.Source, q);
|
|
||||||
SelectedOffer.OrderQty = 1;
|
|
||||||
Status = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SaveDraftAsync()
|
|
||||||
{
|
|
||||||
if (_cart.Count == 0) { Status = "Заказ пуст — добавьте позиции."; return; }
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
// Резервируем настоящий номер заказа EX-####### (как в WinForms)
|
|
||||||
var sign = await _api.NextGlobalSignAsync();
|
|
||||||
|
|
||||||
var draft = new DraftOrder
|
|
||||||
{
|
|
||||||
Number = string.IsNullOrWhiteSpace(sign) ? $"ЧРН-{DateTime.Now:HHmmss}" : sign!,
|
|
||||||
LocationId = SelectedLocation?.BuyerLocationId,
|
|
||||||
LocationAddress = SelectedLocation?.Address,
|
|
||||||
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
|
|
||||||
Items = _cart.Items.Select(c => new DraftItem
|
|
||||||
{
|
|
||||||
SupplierPriceId = c.Source.SupplierPriceId ?? "",
|
|
||||||
Name = c.Name,
|
|
||||||
Supplier = c.Supplier,
|
|
||||||
Barcode = c.Source.Barcode,
|
|
||||||
Price = c.Price,
|
|
||||||
Qty = c.Qty
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
DraftStore.Add(draft);
|
|
||||||
_cart.Clear();
|
|
||||||
Comment = "";
|
|
||||||
Status = $"Заказ сохранён: {draft.Number} → раздел «Заказы» (не отправлен).";
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = "Ошибка сохранения: " + ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отправить: поиск по прайсу → корзина (общая с Прайс-листом) → создание заказа.
|
|
||||||
/// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика.
|
|
||||||
/// </summary>
|
|
||||||
public partial class SendOrderViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
private readonly CartService _cart = CartService.Instance;
|
|
||||||
|
|
||||||
public ObservableCollection<PriceItem> Results { get; } = new();
|
|
||||||
public ObservableCollection<CartItem> Cart => _cart.Items;
|
|
||||||
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private string _search = "";
|
|
||||||
[ObservableProperty] private PriceItem? _selectedResult;
|
|
||||||
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
|
|
||||||
[ObservableProperty] private string _comment = "";
|
|
||||||
[ObservableProperty] private bool _busy;
|
|
||||||
[ObservableProperty] private string? _status;
|
|
||||||
[ObservableProperty] private decimal _cartTotal;
|
|
||||||
|
|
||||||
public SendOrderViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
_cart.Changed += Recalc;
|
|
||||||
Recalc();
|
|
||||||
if (Session.Current.IsAuthenticated) _ = InitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var locs = await _api.GetBuyerLocationsAsync();
|
|
||||||
Locations.Clear();
|
|
||||||
foreach (var l in locs) Locations.Add(l);
|
|
||||||
SelectedLocation = Locations.FirstOrDefault(l => l.IsDefault) ?? Locations.FirstOrDefault();
|
|
||||||
await SearchAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SearchAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true; Status = null;
|
|
||||||
var resp = await _api.GetPriceSummaryAsync(Search, limit: 200);
|
|
||||||
Results.Clear();
|
|
||||||
foreach (var it in resp.Summary ?? new()) Results.Add(it);
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void AddToCart()
|
|
||||||
{
|
|
||||||
if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; }
|
|
||||||
_cart.Add(SelectedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
|
|
||||||
|
|
||||||
private void Recalc() => CartTotal = _cart.Total;
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SendAsync()
|
|
||||||
{
|
|
||||||
Status = null;
|
|
||||||
if (_cart.Count == 0) { Status = "Корзина пуста."; return; }
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Busy = true;
|
|
||||||
var req = new BuyerOrderCreateRequest
|
|
||||||
{
|
|
||||||
LocationId = SelectedLocation?.BuyerLocationId,
|
|
||||||
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
|
|
||||||
Items = _cart.Items.Select(c => new BuyerOrderItemReq
|
|
||||||
{
|
|
||||||
SupplierPriceId = c.Source.SupplierPriceId ?? "",
|
|
||||||
Qty = (double)c.Qty,
|
|
||||||
ItemName = c.Source.DrugName,
|
|
||||||
Barcode = c.Source.Barcode
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
var resp = await _api.CreateOrderAsync(req);
|
|
||||||
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}.";
|
|
||||||
_cart.Clear();
|
|
||||||
}
|
|
||||||
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
|
|
||||||
finally { Busy = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
public partial class SettingsViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private string _apiBaseUrl = "";
|
|
||||||
public string[] Themes { get; } = { "Светлая", "Тёмная" };
|
|
||||||
[ObservableProperty] private int _themeIndex;
|
|
||||||
|
|
||||||
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
|
||||||
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
|
|
||||||
|
|
||||||
public SettingsViewModel()
|
|
||||||
{
|
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
ApiBaseUrl = SettingsStore.Current.ApiBaseUrl ?? AppConfig.DefaultApiBaseUrl;
|
|
||||||
ThemeIndex = SettingsStore.Current.Theme == "Dark" ? 1 : 0;
|
|
||||||
if (Session.Current.IsAuthenticated) _ = LoadLocationsAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadLocationsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var locs = await _api.GetBuyerLocationsAsync();
|
|
||||||
Locations.Clear();
|
|
||||||
foreach (var l in locs) Locations.Add(l);
|
|
||||||
var savedId = SettingsStore.Current.DefaultLocationId;
|
|
||||||
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
|
|
||||||
?? Locations.FirstOrDefault(l => l.IsDefault)
|
|
||||||
?? Locations.FirstOrDefault();
|
|
||||||
}
|
|
||||||
catch { /* локации не критичны для настроек */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Elfisa.Core;
|
using Elfisa.Core;
|
||||||
@ -11,7 +10,7 @@ namespace Elfisa.Avalonia.ViewModels;
|
|||||||
public class NavItem
|
public class NavItem
|
||||||
{
|
{
|
||||||
public string Title { get; }
|
public string Title { get; }
|
||||||
public string Icon { get; } // глиф Segoe MDL2 Assets
|
public string Icon { get; }
|
||||||
public Func<ViewModelBase> Factory { get; }
|
public Func<ViewModelBase> Factory { get; }
|
||||||
public NavItem(string title, string icon, Func<ViewModelBase> factory)
|
public NavItem(string title, string icon, Func<ViewModelBase> factory)
|
||||||
{
|
{
|
||||||
@ -19,29 +18,14 @@ public class NavItem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Открытая вкладка-документ.</summary>
|
|
||||||
public sealed class DocumentTab
|
|
||||||
{
|
|
||||||
public string Title { get; }
|
|
||||||
public string Key { get; }
|
|
||||||
public ViewModelBase Content { get; }
|
|
||||||
public DocumentTab(string title, string key, ViewModelBase content)
|
|
||||||
{
|
|
||||||
Title = title; Key = key; Content = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class ShellViewModel : ViewModelBase
|
public partial class ShellViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly Action _logout;
|
private readonly Action _logout;
|
||||||
private readonly ApiClient _api = new();
|
|
||||||
private bool _sync;
|
|
||||||
|
|
||||||
public ObservableCollection<NavItem> NavItems { get; }
|
public ObservableCollection<NavItem> NavItems { get; }
|
||||||
public ObservableCollection<DocumentTab> Tabs { get; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private NavItem? _selectedNav;
|
[ObservableProperty] private NavItem? _selectedNav;
|
||||||
[ObservableProperty] private DocumentTab? _activeTab;
|
[ObservableProperty] private ViewModelBase? _currentPage;
|
||||||
|
|
||||||
public string UserName => Session.Current.Username ?? "";
|
public string UserName => Session.Current.Username ?? "";
|
||||||
public string RoleLabel => Session.Current.Role switch
|
public string RoleLabel => Session.Current.Role switch
|
||||||
@ -53,84 +37,23 @@ public partial class ShellViewModel : ViewModelBase
|
|||||||
_ => Session.Current.Role ?? ""
|
_ => Session.Current.Role ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
[ObservableProperty] private string _pharmacyAddress = "";
|
|
||||||
|
|
||||||
// Segoe MDL2 Assets — те же глифы, что в WinForms-версии
|
|
||||||
private const string IcoPrice = "";
|
|
||||||
private const string IcoOrders = "";
|
|
||||||
private const string IcoInvoices = "";
|
|
||||||
private const string IcoReports = "";
|
|
||||||
private const string IcoUpload = "";
|
|
||||||
private const string IcoSend = "";
|
|
||||||
private const string IcoRefusals = "";
|
|
||||||
private const string IcoDirectory = "";
|
|
||||||
|
|
||||||
public ShellViewModel(Action logout)
|
public ShellViewModel(Action logout)
|
||||||
{
|
{
|
||||||
_logout = logout;
|
_logout = logout;
|
||||||
_api.SetToken(Session.Current.Token);
|
|
||||||
|
|
||||||
NavItems = new ObservableCollection<NavItem>
|
NavItems = new ObservableCollection<NavItem>
|
||||||
{
|
{
|
||||||
new("Прайс-лист", IcoPrice, () => new PriceListViewModel()),
|
new("Прайс-лист", "🧾", () => new PlaceholderPageViewModel("Прайс-лист", "Каталог, поиск и добавление позиций в заказ.")),
|
||||||
new("Заказы", IcoOrders, () => new OrdersViewModel()),
|
new("Заказы", "🛒", () => new OrdersViewModel()),
|
||||||
new("Накладные", IcoInvoices, () => new InvoicesViewModel()),
|
new("Накладные", "📄", () => new PlaceholderPageViewModel("Накладные", "Приход товара по заказам.")),
|
||||||
new("Отчёты", IcoReports, () => new ReportsViewModel()),
|
new("Отчёты", "📊", () => new ReportsViewModel()),
|
||||||
new("Загрузить", IcoUpload, () => new LoadPriceViewModel()),
|
new("Отправить", "📤", () => new PlaceholderPageViewModel("Отправить", "Формирование и выгрузка заявок поставщику (DBF).")),
|
||||||
new("Отправить", IcoSend, () => new SendOrderViewModel()),
|
new("Отказы", "⛔", () => new PlaceholderPageViewModel("Отказы", "Отказные позиции по заказам.")),
|
||||||
new("Отказы", IcoRefusals, () => new PlaceholderPageViewModel("Отказы",
|
new("Справочник", "📖", () => new PlaceholderPageViewModel("Справочник", "Контрагенты, грузополучатели, настройки.")),
|
||||||
"Отказные позиции по заказам. Нужен серверный эндпоинт (RefuseItemsCount сейчас только в локальной БД WinForms).")),
|
|
||||||
new("Справочник", IcoDirectory, () => new DirectoryViewModel()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
|
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
|
||||||
if (Session.Current.IsAuthenticated) _ = LoadPharmacyAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadPharmacyAsync()
|
partial void OnSelectedNavChanged(NavItem? value) => CurrentPage = value?.Factory();
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var locs = await _api.GetBuyerLocationsAsync();
|
|
||||||
var def = locs.FirstOrDefault(l => l.IsDefault) ?? locs.FirstOrDefault();
|
|
||||||
PharmacyAddress = def?.Address ?? "";
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnSelectedNavChanged(NavItem? value)
|
|
||||||
{
|
|
||||||
if (_sync || value is null) return;
|
|
||||||
OpenOrActivate(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenOrActivate(NavItem nav)
|
|
||||||
{
|
|
||||||
var existing = Tabs.FirstOrDefault(t => t.Key == nav.Title);
|
|
||||||
if (existing is not null) { ActiveTab = existing; return; }
|
|
||||||
var tab = new DocumentTab(nav.Title, nav.Title, nav.Factory());
|
|
||||||
Tabs.Add(tab);
|
|
||||||
ActiveTab = tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnActiveTabChanged(DocumentTab? value)
|
|
||||||
{
|
|
||||||
if (value is null) return;
|
|
||||||
_sync = true;
|
|
||||||
SelectedNav = NavItems.FirstOrDefault(n => n.Title == value.Key);
|
|
||||||
_sync = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void CloseTab(DocumentTab? tab)
|
|
||||||
{
|
|
||||||
if (tab is null) return;
|
|
||||||
int idx = Tabs.IndexOf(tab);
|
|
||||||
bool wasActive = ReferenceEquals(ActiveTab, tab);
|
|
||||||
Tabs.Remove(tab);
|
|
||||||
if (wasActive)
|
|
||||||
ActiveTab = Tabs.Count == 0 ? null : Tabs[Math.Min(idx, Tabs.Count - 1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand] private void Logout() => _logout();
|
[RelayCommand] private void Logout() => _logout();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
xmlns:core="using:Elfisa.Core"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.DirectoryView"
|
|
||||||
x:DataType="vm:DirectoryViewModel">
|
|
||||||
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="24" Spacing="14">
|
|
||||||
<StackPanel Spacing="2">
|
|
||||||
<TextBlock Text="Справочник" Classes="h1"/>
|
|
||||||
<TextBlock Text="Профиль и грузополучатели" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Профиль -->
|
|
||||||
<Border Classes="card" MaxWidth="720" HorizontalAlignment="Left">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="Учётная запись" Classes="h2"/>
|
|
||||||
<Grid ColumnDefinitions="160,*" RowDefinitions="Auto,Auto">
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Логин" Classes="muted"/>
|
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding UserName}" Foreground="{DynamicResource Text}"/>
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Роль" Classes="muted" Margin="0,6,0,0"/>
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding RoleLabel}" Foreground="{DynamicResource Text}" Margin="0,6,0,0"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Грузополучатели -->
|
|
||||||
<Border Classes="card" MaxWidth="720" HorizontalAlignment="Left">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="Грузополучатели (адреса)" Classes="h2"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding Locations}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="core:BuyerLocationDto">
|
|
||||||
<Border Padding="12" Margin="0,0,0,8" CornerRadius="9"
|
|
||||||
Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
|
|
||||||
<StackPanel Spacing="3">
|
|
||||||
<TextBlock Text="{Binding Address}" Foreground="{DynamicResource Text}" TextWrapping="Wrap"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<TextBlock Text="{Binding RegionName}" Classes="muted"/>
|
|
||||||
<Border Background="{DynamicResource AccentSoft}" CornerRadius="5" Padding="6,1"
|
|
||||||
IsVisible="{Binding IsDefault}">
|
|
||||||
<TextBlock Text="по умолчанию" FontSize="11" Foreground="{DynamicResource AccentPressed}"/>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="{Binding Status}" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class DirectoryView : UserControl
|
|
||||||
{
|
|
||||||
public DirectoryView() => AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
xmlns:conv="using:Avalonia.Data.Converters"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.InvoicesView"
|
|
||||||
x:DataType="vm:InvoicesViewModel">
|
|
||||||
|
|
||||||
<DockPanel Margin="24">
|
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
|
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Накладные" Classes="h1"/>
|
|
||||||
<TextBlock Text="Реестр приходных документов" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="{Binding Totals}" HorizontalAlignment="Right" VerticalAlignment="Center"
|
|
||||||
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border DockPanel.Dock="Top" Margin="0,0,0,10" Padding="10,7" CornerRadius="6"
|
|
||||||
Background="{DynamicResource AccentSoft}"
|
|
||||||
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
|
||||||
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Classes="card" Padding="0" ClipToBounds="True">
|
|
||||||
<DataGrid ItemsSource="{Binding Rows}" x:CompileBindings="False"
|
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
|
||||||
CanUserResizeColumns="True" CanUserSortColumns="False">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Дата" Width="112" Binding="{Binding Date}"/>
|
|
||||||
<DataGridTextColumn Header="Номер" Width="150" Binding="{Binding Number}"/>
|
|
||||||
<DataGridTextColumn Header="Поставщик" Width="160" Binding="{Binding Supplier}"/>
|
|
||||||
<DataGridTextColumn Header="Аптека" Width="*" Binding="{Binding Location}"/>
|
|
||||||
<DataGridTextColumn Header="Позиций" Width="90" Binding="{Binding ItemsCount}" CellStyleClasses="num"/>
|
|
||||||
<DataGridTextColumn Header="Сумма" Width="132" Binding="{Binding Sum, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</Border>
|
|
||||||
</DockPanel>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class InvoicesView : UserControl
|
|
||||||
{
|
|
||||||
public InvoicesView() => AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
xmlns:conv="using:Avalonia.Data.Converters"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.LoadPriceView"
|
|
||||||
x:DataType="vm:LoadPriceViewModel">
|
|
||||||
|
|
||||||
<Grid Margin="24" RowDefinitions="Auto,*">
|
|
||||||
<StackPanel Grid.Row="0" Spacing="2">
|
|
||||||
<TextBlock Text="Загрузить" Classes="h1"/>
|
|
||||||
<TextBlock Text="Скачать актуальный прайс с сервера" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Border Grid.Row="1" Classes="card" MaxWidth="560" Padding="30"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<StackPanel Spacing="18" HorizontalAlignment="Center">
|
|
||||||
<Border Width="60" Height="60" CornerRadius="30" Background="{DynamicResource AccentSoft}">
|
|
||||||
<TextBlock Text="📥" FontSize="26" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<TextBlock Text="{Binding LastLoaded}" Classes="muted" HorizontalAlignment="Center" TextAlignment="Center"/>
|
|
||||||
|
|
||||||
<Button Classes="primary" Content="Загрузить прайс" HorizontalAlignment="Center"
|
|
||||||
Padding="28,12" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
|
|
||||||
<StackPanel Spacing="6" IsVisible="{Binding Busy}">
|
|
||||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" Height="8" Width="360"/>
|
|
||||||
<TextBlock Text="{Binding ProgressText}" Classes="muted" HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Border Background="{DynamicResource AccentSoft}" CornerRadius="8" Padding="14,10"
|
|
||||||
IsVisible="{Binding Result, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
|
||||||
<TextBlock Text="{Binding Result}" Foreground="{DynamicResource AccentPressed}"
|
|
||||||
TextWrapping="Wrap" TextAlignment="Center"/>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class LoadPriceView : UserControl
|
|
||||||
{
|
|
||||||
public LoadPriceView() => AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
@ -43,22 +43,14 @@
|
|||||||
<TextBlock Text="{Binding SelectedOrder.Location}" Classes="muted" TextWrapping="Wrap"/>
|
<TextBlock Text="{Binding SelectedOrder.Location}" Classes="muted" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0">
|
<Border DockPanel.Dock="Bottom" Margin="0,12,0,0" Padding="0,10,0,0"
|
||||||
<Border Padding="0,10,0,0" BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Border}">
|
BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Border}">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
|
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding SelectedOrder.Sum, StringFormat='{}{0:N2}'}"
|
<TextBlock Grid.Column="1" Text="{Binding SelectedOrder.Sum, StringFormat='{}{0:N2}'}"
|
||||||
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
|
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<!-- Действия для черновика (не отправленного заказа) -->
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" IsVisible="{Binding SelectedOrder.IsDraft, FallbackValue=False}">
|
|
||||||
<Button Classes="primary" Content="Отправить заказ" Command="{Binding SendDraftCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
<Button Content="Удалить" Command="{Binding DeleteDraftCommand}"
|
|
||||||
Background="Transparent" Foreground="{DynamicResource Muted}"
|
|
||||||
BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<DataGrid ItemsSource="{Binding SelectedOrder.Items}" x:CompileBindings="False"
|
<DataGrid ItemsSource="{Binding SelectedOrder.Items}" x:CompileBindings="False"
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
|||||||
@ -1,127 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
xmlns:conv="using:Avalonia.Data.Converters"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.PriceListView"
|
|
||||||
x:DataType="vm:PriceListViewModel">
|
|
||||||
|
|
||||||
<DockPanel Margin="24">
|
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
|
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Прайс-лист" Classes="h1"/>
|
|
||||||
<TextBlock Text="Каталог позиций — собери заказ и сохрани" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border DockPanel.Dock="Top" Margin="0,0,0,10" Padding="10,7" CornerRadius="6"
|
|
||||||
Background="{DynamicResource AccentSoft}"
|
|
||||||
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
|
||||||
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" TextWrapping="Wrap" FontSize="12.5"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,16,430">
|
|
||||||
|
|
||||||
<!-- Каталог -->
|
|
||||||
<Border Grid.Column="0" Classes="card" Padding="0" ClipToBounds="True">
|
|
||||||
<DockPanel>
|
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="10" Margin="14">
|
|
||||||
<TextBox Text="{Binding Search}" Width="330" Watermark="поиск по наименованию (от 2 символов)…"/>
|
|
||||||
<Button Classes="primary" Content="Найти" Command="{Binding SearchCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Border DockPanel.Dock="Bottom" Margin="14" Padding="0">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Кол-во" Classes="label" VerticalAlignment="Center"/>
|
|
||||||
<NumericUpDown Value="{Binding SelectedOffer.OrderQty}" Minimum="0" Maximum="100000" Increment="1" FormatString="0" Width="120"
|
|
||||||
IsEnabled="{Binding SelectedOffer, Converter={x:Static conv:ObjectConverters.IsNotNull}}"/>
|
|
||||||
<Button Classes="primary" Content="Добавить в заказ ▸" Command="{Binding AddToOrderCommand}"/>
|
|
||||||
<TextBlock Text="или выдели строку и набери число на клавиатуре, Enter — добавить" Classes="muted" VerticalAlignment="Center" TextWrapping="Wrap" MaxWidth="240"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<!-- Дрилдаун как в старой программе: Название → МГ → Предложения -->
|
|
||||||
<Grid ColumnDefinitions="1.3*,10,120,10,2.2*" Margin="14,0,14,0">
|
|
||||||
|
|
||||||
<!-- Название (базовое наименование) -->
|
|
||||||
<DockPanel Grid.Column="0">
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Название" Classes="label" Margin="2,0,0,5"/>
|
|
||||||
<ListBox ItemsSource="{Binding BaseNames}" SelectedItem="{Binding SelectedBaseName}"
|
|
||||||
BorderThickness="1" BorderBrush="{DynamicResource Border}" CornerRadius="8"/>
|
|
||||||
</DockPanel>
|
|
||||||
|
|
||||||
<!-- МГ (дозировка) -->
|
|
||||||
<DockPanel Grid.Column="2">
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="МГ" Classes="label" Margin="2,0,0,5"/>
|
|
||||||
<ListBox ItemsSource="{Binding MgOptions}" SelectedItem="{Binding SelectedMg}"
|
|
||||||
BorderThickness="1" BorderBrush="{DynamicResource Border}" CornerRadius="8"/>
|
|
||||||
</DockPanel>
|
|
||||||
|
|
||||||
<!-- Предложения (фильтр: название + МГ) -->
|
|
||||||
<DockPanel Grid.Column="4">
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Предложения" Classes="label" Margin="2,0,0,5"/>
|
|
||||||
<DataGrid x:Name="OffersGrid" ItemsSource="{Binding Offers}" SelectedItem="{Binding SelectedOffer}" x:CompileBindings="False"
|
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
|
||||||
CanUserResizeColumns="True" CanUserSortColumns="False"
|
|
||||||
BorderThickness="1" BorderBrush="{DynamicResource Border}" CornerRadius="8">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Наименование" Width="*" Binding="{Binding DrugName}"/>
|
|
||||||
<DataGridTextColumn Header="Поставщик" Width="140" Binding="{Binding SupplierName}"/>
|
|
||||||
<DataGridTextColumn Header="Цена" Width="85" Binding="{Binding Price, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
|
|
||||||
<DataGridTextColumn Header="Остаток" Width="75" Binding="{Binding Quantity, StringFormat='{}{0:0}'}" CellStyleClasses="num"/>
|
|
||||||
<DataGridTextColumn Header="Заказ" Width="70" Binding="{Binding OrderQty, StringFormat='{}{0:0}'}" CellStyleClasses="num"
|
|
||||||
FontWeight="Bold" Foreground="{DynamicResource Accent}"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</DockPanel>
|
|
||||||
</Grid>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Мой заказ -->
|
|
||||||
<Border Grid.Column="2" Classes="card" Padding="16">
|
|
||||||
<DockPanel>
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Мой заказ" Classes="h2" Margin="0,0,0,10"/>
|
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0">
|
|
||||||
<StackPanel Spacing="5">
|
|
||||||
<TextBlock Text="Грузополучатель" Classes="muted"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBox Text="{Binding Comment}" Watermark="комментарий (необязательно)"/>
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" VerticalAlignment="Center" Foreground="{DynamicResource Text}"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding CartTotal, StringFormat='{}{0:N2}'}" FontWeight="Bold" FontSize="16" Foreground="{DynamicResource Accent}"/>
|
|
||||||
</Grid>
|
|
||||||
<Button Classes="primary" Content="Сохранить заказ" HorizontalAlignment="Stretch" Command="{Binding SaveDraftCommand}"/>
|
|
||||||
<TextBlock Text="Черновик появится в «Заказы» со статусом «Не отправлен»." Classes="muted" TextWrapping="Wrap"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ScrollViewer>
|
|
||||||
<ItemsControl ItemsSource="{Binding Cart}" x:CompileBindings="False">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:CartItem">
|
|
||||||
<Border Padding="10" Margin="0,0,0,8" CornerRadius="9"
|
|
||||||
Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
|
|
||||||
<DockPanel>
|
|
||||||
<Button DockPanel.Dock="Right" Content="✕" Width="30" Height="30" Padding="0"
|
|
||||||
VerticalAlignment="Top" Background="Transparent" Foreground="{DynamicResource Muted}" Cursor="Hand"
|
|
||||||
Command="{Binding $parent[ItemsControl].DataContext.RemoveFromCartCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}" TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"/>
|
|
||||||
<TextBlock Text="{Binding Supplier}" Classes="muted"/>
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
|
||||||
<NumericUpDown Grid.Column="0" Value="{Binding Qty}" Minimum="1" Maximum="100000" Increment="1" FormatString="0" Width="120"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Price, StringFormat='× {0:N2}'}" Classes="muted" VerticalAlignment="Center" Margin="10,0"/>
|
|
||||||
<TextBlock Grid.Column="2" Text="{Binding LineSum, StringFormat='{}{0:N2}'}" FontWeight="SemiBold" Foreground="{DynamicResource Text}" VerticalAlignment="Center"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</DockPanel>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class PriceListView : UserControl
|
|
||||||
{
|
|
||||||
// Строка, в которую сейчас набирают количество с клавиатуры (для многозначного ввода).
|
|
||||||
private OfferRow? _typingRow;
|
|
||||||
|
|
||||||
public PriceListView()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
var grid = this.FindControl<DataGrid>("OffersGrid");
|
|
||||||
if (grid != null) grid.KeyDown += OffersGrid_KeyDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Выделил предложение → набираешь число цифрами (как колонка «Заказ» в старой программе).
|
|
||||||
// Backspace — стереть цифру, Enter — добавить в заказ.
|
|
||||||
private void OffersGrid_KeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (sender is not DataGrid grid || grid.SelectedItem is not OfferRow row) return;
|
|
||||||
|
|
||||||
var digit = DigitFromKey(e.Key);
|
|
||||||
if (digit >= 0)
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(_typingRow, row)) { row.OrderQty = 0; _typingRow = row; }
|
|
||||||
var next = row.OrderQty * 10 + digit;
|
|
||||||
if (next <= 100000) row.OrderQty = next;
|
|
||||||
e.Handled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (e.Key)
|
|
||||||
{
|
|
||||||
case Key.Back:
|
|
||||||
row.OrderQty = Math.Floor(row.OrderQty / 10);
|
|
||||||
_typingRow = row;
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
case Key.Enter:
|
|
||||||
(DataContext as PriceListViewModel)?.AddSelectedOffer();
|
|
||||||
_typingRow = null;
|
|
||||||
e.Handled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int DigitFromKey(Key k)
|
|
||||||
{
|
|
||||||
if (k >= Key.D0 && k <= Key.D9) return k - Key.D0;
|
|
||||||
if (k >= Key.NumPad0 && k <= Key.NumPad9) return k - Key.NumPad0;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
xmlns:conv="using:Avalonia.Data.Converters"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.SendOrderView"
|
|
||||||
x:DataType="vm:SendOrderViewModel">
|
|
||||||
|
|
||||||
<DockPanel Margin="24">
|
|
||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
|
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Отправить заказ" Classes="h1"/>
|
|
||||||
<TextBlock Text="Найдите позиции в прайсе, соберите корзину и отправьте поставщику" Classes="muted"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border DockPanel.Dock="Bottom" Margin="0,10,0,0" Padding="10,7" CornerRadius="6"
|
|
||||||
Background="{DynamicResource AccentSoft}"
|
|
||||||
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
|
||||||
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" TextWrapping="Wrap" FontSize="12.5"/>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,16,440">
|
|
||||||
|
|
||||||
<!-- Прайс: поиск + результаты -->
|
|
||||||
<Border Grid.Column="0" Classes="card" Padding="0" ClipToBounds="True">
|
|
||||||
<DockPanel>
|
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="10" Margin="14">
|
|
||||||
<TextBox Text="{Binding Search}" Width="330" Watermark="поиск по наименованию…"/>
|
|
||||||
<Button Classes="primary" Content="Найти" Command="{Binding SearchCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Button DockPanel.Dock="Bottom" Classes="primary" Content="Добавить в заказ ▸"
|
|
||||||
Margin="14" HorizontalAlignment="Left"
|
|
||||||
Command="{Binding AddToCartCommand}"/>
|
|
||||||
<DataGrid ItemsSource="{Binding Results}" SelectedItem="{Binding SelectedResult}"
|
|
||||||
x:CompileBindings="False" AutoGenerateColumns="False" IsReadOnly="True"
|
|
||||||
HeadersVisibility="Column" CanUserResizeColumns="True" CanUserSortColumns="False">
|
|
||||||
<DataGrid.Columns>
|
|
||||||
<DataGridTextColumn Header="Наименование" Width="*" Binding="{Binding DrugName}"/>
|
|
||||||
<DataGridTextColumn Header="Поставщик" Width="120" Binding="{Binding SupplierName}"/>
|
|
||||||
<DataGridTextColumn Header="Цена" Width="100" Binding="{Binding Price, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
|
|
||||||
</DataGrid.Columns>
|
|
||||||
</DataGrid>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Корзина -->
|
|
||||||
<Border Grid.Column="2" Classes="card" Padding="16">
|
|
||||||
<DockPanel>
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Корзина" Classes="h2" Margin="0,0,0,10"/>
|
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0">
|
|
||||||
<StackPanel Spacing="5">
|
|
||||||
<TextBlock Text="Грузополучатель" Classes="muted"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}"
|
|
||||||
HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBox Text="{Binding Comment}" Watermark="комментарий к заказу (необязательно)"/>
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" VerticalAlignment="Center"
|
|
||||||
Foreground="{DynamicResource Text}"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding CartTotal, StringFormat='{}{0:N2}'}"
|
|
||||||
FontWeight="Bold" FontSize="16" Foreground="{DynamicResource Accent}"/>
|
|
||||||
</Grid>
|
|
||||||
<Button Classes="primary" Content="Отправить заказ" HorizontalAlignment="Stretch"
|
|
||||||
Command="{Binding SendCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<ScrollViewer>
|
|
||||||
<ItemsControl ItemsSource="{Binding Cart}" x:CompileBindings="False">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:CartItem">
|
|
||||||
<Border Padding="10" Margin="0,0,0,8" CornerRadius="9"
|
|
||||||
Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
|
|
||||||
<DockPanel>
|
|
||||||
<Button DockPanel.Dock="Right" Content="✕" Width="30" Height="30" Padding="0"
|
|
||||||
VerticalAlignment="Top" Background="Transparent"
|
|
||||||
Foreground="{DynamicResource Muted}" Cursor="Hand"
|
|
||||||
Command="{Binding $parent[ItemsControl].DataContext.RemoveFromCartCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}"
|
|
||||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"/>
|
|
||||||
<TextBlock Text="{Binding Supplier}" Classes="muted"/>
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
|
||||||
<NumericUpDown Grid.Column="0" Value="{Binding Qty}" Minimum="1" Maximum="100000"
|
|
||||||
Increment="1" FormatString="0" Width="120"/>
|
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Price, StringFormat='× {0:N2}'}"
|
|
||||||
Classes="muted" VerticalAlignment="Center" Margin="10,0"/>
|
|
||||||
<TextBlock Grid.Column="2" Text="{Binding LineSum, StringFormat='{}{0:N2}'}"
|
|
||||||
FontWeight="SemiBold" Foreground="{DynamicResource Text}"
|
|
||||||
VerticalAlignment="Center"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</ScrollViewer>
|
|
||||||
</DockPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</DockPanel>
|
|
||||||
</UserControl>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class SendOrderView : UserControl
|
|
||||||
{
|
|
||||||
public SendOrderView() => AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
|
|
||||||
x:Class="Elfisa.Avalonia.Views.SettingsWindow"
|
|
||||||
x:DataType="vm:SettingsViewModel"
|
|
||||||
Width="460" Height="360" CanResize="False"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
Background="{DynamicResource Bg}"
|
|
||||||
Title="Настройки">
|
|
||||||
|
|
||||||
<StackPanel Margin="24" Spacing="16">
|
|
||||||
<TextBlock Text="Настройки" Classes="h1"/>
|
|
||||||
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="Адрес сервера (API)" Classes="label"/>
|
|
||||||
<TextBox Text="{Binding ApiBaseUrl}" Watermark="https://24pharmdata.ru"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="Тема" Classes="label"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Themes}" SelectedIndex="{Binding ThemeIndex}" HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Text="Грузополучатель по умолчанию" Classes="label"/>
|
|
||||||
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10" Margin="0,8,0,0">
|
|
||||||
<Button Content="Отмена" Click="OnCancel" Padding="18,9"/>
|
|
||||||
<Button Classes="primary" Content="Сохранить" Click="OnSave"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Window>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Styling;
|
|
||||||
using Elfisa.Avalonia.ViewModels;
|
|
||||||
using Elfisa.Core;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
|
||||||
|
|
||||||
public partial class SettingsWindow : Window
|
|
||||||
{
|
|
||||||
public SettingsWindow() => AvaloniaXamlLoader.Load(this);
|
|
||||||
|
|
||||||
private void OnSave(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is SettingsViewModel vm)
|
|
||||||
{
|
|
||||||
SettingsStore.Current.ApiBaseUrl = string.IsNullOrWhiteSpace(vm.ApiBaseUrl) ? null : vm.ApiBaseUrl.Trim().TrimEnd('/');
|
|
||||||
SettingsStore.Current.Theme = vm.ThemeIndex == 1 ? "Dark" : "Light";
|
|
||||||
SettingsStore.Current.DefaultLocationId = vm.SelectedLocation?.BuyerLocationId;
|
|
||||||
SettingsStore.Save();
|
|
||||||
|
|
||||||
if (Application.Current is { } app)
|
|
||||||
app.RequestedThemeVariant = vm.ThemeIndex == 1 ? ThemeVariant.Dark : ThemeVariant.Light;
|
|
||||||
}
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close();
|
|
||||||
}
|
|
||||||
@ -6,15 +6,17 @@
|
|||||||
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
<!-- Верхняя панель навигации -->
|
<!-- Верхняя панель навигации (как в исходной программе) -->
|
||||||
<Border DockPanel.Dock="Top" Background="{DynamicResource Sidebar}" Height="82">
|
<Border DockPanel.Dock="Top" Background="{DynamicResource Sidebar}" Height="82">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="18,0">
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="18,0">
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,14,0">
|
<!-- Логотип -->
|
||||||
<TextBlock Text="ЭльФиСА" FontSize="18" FontWeight="Bold" Foreground="White"/>
|
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,22,0">
|
||||||
<TextBlock Text="Электронная Фармация" Foreground="{DynamicResource SidebarText}" FontSize="10"/>
|
<TextBlock Text="ЭльФиСА" FontSize="19" FontWeight="Bold" Foreground="White"/>
|
||||||
|
<TextBlock Text="Электронная Фармация" Foreground="{DynamicResource SidebarText}" FontSize="10.5"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Навигация -->
|
||||||
<ListBox Grid.Column="1" Classes="topnav" VerticalAlignment="Center" HorizontalAlignment="Left"
|
<ListBox Grid.Column="1" Classes="topnav" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
ItemsSource="{Binding NavItems}" SelectedItem="{Binding SelectedNav}">
|
ItemsSource="{Binding NavItems}" SelectedItem="{Binding SelectedNav}">
|
||||||
<ListBox.ItemsPanel>
|
<ListBox.ItemsPanel>
|
||||||
@ -24,17 +26,18 @@
|
|||||||
</ListBox.ItemsPanel>
|
</ListBox.ItemsPanel>
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:NavItem">
|
<DataTemplate x:DataType="vm:NavItem">
|
||||||
<StackPanel Orientation="Vertical" Width="66" Spacing="3">
|
<StackPanel Orientation="Vertical" Width="76" Spacing="3">
|
||||||
<TextBlock Text="{Binding Icon}" FontFamily="Segoe MDL2 Assets" FontSize="18"
|
<TextBlock Text="{Binding Icon}" FontSize="17" HorizontalAlignment="Center"/>
|
||||||
Foreground="White" HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock Text="{Binding Title}" Foreground="{DynamicResource SidebarText}"
|
<TextBlock Text="{Binding Title}" Foreground="{DynamicResource SidebarText}"
|
||||||
FontSize="11" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
FontSize="11.5" HorizontalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center" Margin="8,0,0,0">
|
<!-- Пользователь + выход -->
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center" Margin="18,0,0,0">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding UserName}" Foreground="White" FontSize="12" FontWeight="SemiBold"
|
<TextBlock Text="{Binding UserName}" Foreground="White" FontSize="12" FontWeight="SemiBold"
|
||||||
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis"/>
|
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis"/>
|
||||||
@ -48,48 +51,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Строка вкладок-документов -->
|
<!-- Контент -->
|
||||||
<Border DockPanel.Dock="Top" Background="{DynamicResource Surface}"
|
<ContentControl Content="{Binding CurrentPage}"/>
|
||||||
BorderBrush="{DynamicResource Border}" BorderThickness="0,0,0,1" MinHeight="44" Padding="10,0">
|
|
||||||
<ListBox Classes="tabs" VerticalAlignment="Center" x:CompileBindings="False"
|
|
||||||
ItemsSource="{Binding Tabs}" SelectedItem="{Binding ActiveTab}">
|
|
||||||
<ListBox.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<StackPanel Orientation="Horizontal"/>
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ListBox.ItemsPanel>
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:DocumentTab">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="{Binding Title}" VerticalAlignment="Center" FontSize="13"/>
|
|
||||||
<Button Content="✕" FontSize="11" Width="20" Height="20" Padding="0"
|
|
||||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
|
||||||
Foreground="{DynamicResource Muted}"
|
|
||||||
Command="{Binding $parent[ListBox].DataContext.CloseTabCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Нижняя строка состояния -->
|
|
||||||
<Border DockPanel.Dock="Bottom" Background="{DynamicResource Surface}"
|
|
||||||
BorderBrush="{DynamicResource Border}" BorderThickness="0,1,0,0" Height="32" Padding="16,0">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Пользователь:" Classes="muted" FontSize="12"/>
|
|
||||||
<TextBlock Text="{Binding UserName}" FontSize="12" Foreground="{DynamicResource Text}"/>
|
|
||||||
<TextBlock Text=" | Аптека:" Classes="muted" FontSize="12"/>
|
|
||||||
<TextBlock Text="{Binding PharmacyAddress}" FontSize="12" Foreground="{DynamicResource Text}" TextTrimming="CharacterEllipsis"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Grid.Column="1" Content="Настройки" Click="OnSettingsClick"
|
|
||||||
Background="Transparent" Foreground="{DynamicResource Accent}" BorderThickness="0"
|
|
||||||
Padding="10,4" FontSize="12.5" Cursor="Hand"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Контент активной вкладки -->
|
|
||||||
<ContentControl Content="{Binding ActiveTab.Content}"/>
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -1,21 +1,9 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.Interactivity;
|
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Elfisa.Avalonia.ViewModels;
|
|
||||||
|
|
||||||
namespace Elfisa.Avalonia.Views;
|
namespace Elfisa.Avalonia.Views;
|
||||||
|
|
||||||
public partial class ShellView : UserControl
|
public partial class ShellView : UserControl
|
||||||
{
|
{
|
||||||
public ShellView() => AvaloniaXamlLoader.Load(this);
|
public ShellView() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
private async void OnSettingsClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
var win = new SettingsWindow { DataContext = new SettingsViewModel() };
|
|
||||||
var owner = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
|
|
||||||
if (owner is not null) await win.ShowDialog(owner);
|
|
||||||
else win.Show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,60 +97,6 @@ public sealed class ApiClient
|
|||||||
return new AiReportResult { Content = bytes, FileName = name.Trim('"') };
|
return new AiReportResult { Content = bytes, FileName = name.Trim('"') };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Сводный прайс (то же, что грузит WinForms-десктоп). Поиск по q (от 2 символов).</summary>
|
|
||||||
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string? q = null, int limit = 200, int offset = 0)
|
|
||||||
{
|
|
||||||
EnsureAuth();
|
|
||||||
var path = $"/api/supplier-prices/summary?limit={limit}&offset={offset}";
|
|
||||||
if (!string.IsNullOrWhiteSpace(q) && q.Trim().Length >= 2)
|
|
||||||
path += "&q=" + Uri.EscapeDataString(q.Trim());
|
|
||||||
var text = await AuthorizedGetAsync(path, "загрузку прайса").ConfigureAwait(false);
|
|
||||||
return JsonSerializer.Deserialize<PriceSummaryResponse>(text, Json) ?? new PriceSummaryResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Создаёт заказ (Placed). ВНИМАНИЕ: создаёт реальный заказ у поставщика.</summary>
|
|
||||||
public async Task<BuyerOrderResponse> CreateOrderAsync(BuyerOrderCreateRequest request)
|
|
||||||
{
|
|
||||||
EnsureAuth();
|
|
||||||
if (request.Items.Count == 0) throw new ApiException("Корзина пуста.");
|
|
||||||
var body = JsonSerializer.Serialize(request, Json);
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, Url("/api/buyer/orders"))
|
|
||||||
{
|
|
||||||
Content = new StringContent(body, Encoding.UTF8, "application/json")
|
|
||||||
};
|
|
||||||
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
|
||||||
HttpResponseMessage resp;
|
|
||||||
try { resp = await _http.SendAsync(req).ConfigureAwait(false); }
|
|
||||||
catch (Exception ex) { throw new ApiException($"Не удалось отправить заказ: {ex.Message}", ex); }
|
|
||||||
|
|
||||||
var text = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
||||||
if (resp.StatusCode == HttpStatusCode.Unauthorized) { _token = null; throw new ApiException("Сессия истекла, войдите снова."); }
|
|
||||||
if (!resp.IsSuccessStatusCode) throw new ApiException(TryError(text) ?? $"Ошибка создания заказа ({(int)resp.StatusCode}).");
|
|
||||||
return JsonSerializer.Deserialize<BuyerOrderResponse>(text, Json) ?? new BuyerOrderResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Резервирует следующий номер заказа EX-####### (как в WinForms до создания заказа).</summary>
|
|
||||||
public async Task<string?> NextGlobalSignAsync()
|
|
||||||
{
|
|
||||||
EnsureAuth();
|
|
||||||
var text = await AuthorizedGetAsync("/api/buyer/global-sign/next", "резервирование номера").ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var doc = JsonDocument.Parse(text);
|
|
||||||
return doc.RootElement.TryGetProperty("global_sign", out var v) ? v.GetString() : null;
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Адреса-грузополучатели покупателя.</summary>
|
|
||||||
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
|
|
||||||
{
|
|
||||||
EnsureAuth();
|
|
||||||
var text = await AuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей").ConfigureAwait(false);
|
|
||||||
try { return JsonSerializer.Deserialize<List<BuyerLocationDto>>(text, Json) ?? new(); }
|
|
||||||
catch { return new(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> AuthorizedGetAsync(string path, string op)
|
private async Task<string> AuthorizedGetAsync(string path, string op)
|
||||||
{
|
{
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Get, Url(path));
|
using var req = new HttpRequestMessage(HttpMethod.Get, Url(path));
|
||||||
|
|||||||
@ -17,8 +17,6 @@ public static class AppConfig
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var fromSettings = SettingsStore.Current.ApiBaseUrl;
|
|
||||||
if (!string.IsNullOrWhiteSpace(fromSettings)) return fromSettings.Trim().TrimEnd('/');
|
|
||||||
var env = Environment.GetEnvironmentVariable(ApiEnv);
|
var env = Environment.GetEnvironmentVariable(ApiEnv);
|
||||||
return string.IsNullOrWhiteSpace(env) ? DefaultApiBaseUrl : env.Trim().TrimEnd('/');
|
return string.IsNullOrWhiteSpace(env) ? DefaultApiBaseUrl : env.Trim().TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Elfisa.Core;
|
|
||||||
|
|
||||||
public sealed class DraftItem
|
|
||||||
{
|
|
||||||
public string SupplierPriceId { get; set; } = "";
|
|
||||||
public string Name { get; set; } = "";
|
|
||||||
public string Supplier { get; set; } = "";
|
|
||||||
public string? Barcode { get; set; }
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
public decimal Qty { get; set; } = 1;
|
|
||||||
public decimal LineSum => Price * Qty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class DraftOrder
|
|
||||||
{
|
|
||||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
||||||
public string Number { get; set; } = "";
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
|
||||||
public string? LocationId { get; set; }
|
|
||||||
public string? LocationAddress { get; set; }
|
|
||||||
public string? Comment { get; set; }
|
|
||||||
public List<DraftItem> Items { get; set; } = new();
|
|
||||||
public decimal Total => Items.Sum(i => i.LineSum);
|
|
||||||
public int ItemsCount => Items.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Локальные черновики заказов (%APPDATA%\ElfisaAvalonia\drafts.json).</summary>
|
|
||||||
public static class DraftStore
|
|
||||||
{
|
|
||||||
private sealed class Box { public List<DraftOrder> Drafts { get; set; } = new(); }
|
|
||||||
|
|
||||||
private static readonly string Dir =
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ElfisaAvalonia");
|
|
||||||
private static readonly string File = Path.Combine(Dir, "drafts.json");
|
|
||||||
|
|
||||||
private static Box _box = Load();
|
|
||||||
|
|
||||||
public static IReadOnlyList<DraftOrder> All => _box.Drafts;
|
|
||||||
|
|
||||||
private static Box Load()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(File))
|
|
||||||
return JsonSerializer.Deserialize<Box>(System.IO.File.ReadAllText(File)) ?? new Box();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return new Box();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Save()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Dir);
|
|
||||||
System.IO.File.WriteAllText(File, JsonSerializer.Serialize(_box, new JsonSerializerOptions { WriteIndented = true }));
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DraftOrder Add(DraftOrder draft)
|
|
||||||
{
|
|
||||||
_box.Drafts.Insert(0, draft);
|
|
||||||
Save();
|
|
||||||
return draft;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Remove(string id)
|
|
||||||
{
|
|
||||||
_box.Drafts.RemoveAll(d => d.Id == id);
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -52,57 +52,3 @@ public sealed class ErrorResponse
|
|||||||
{
|
{
|
||||||
[JsonPropertyName("error")] public string? Error { get; set; }
|
[JsonPropertyName("error")] public string? Error { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PriceSummaryResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("summary")] public List<PriceItem>? Summary { get; set; }
|
|
||||||
[JsonPropertyName("total_drugs")] public int TotalDrugs { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class PriceItem
|
|
||||||
{
|
|
||||||
[JsonPropertyName("supplier_price_id")] public string? SupplierPriceId { get; set; }
|
|
||||||
[JsonPropertyName("supplier_name")] public string? SupplierName { get; set; }
|
|
||||||
[JsonPropertyName("drug_name")] public string? DrugName { get; set; }
|
|
||||||
[JsonPropertyName("cure_form")] public string? CureForm { get; set; }
|
|
||||||
[JsonPropertyName("barcode")] public string? Barcode { get; set; }
|
|
||||||
[JsonPropertyName("price")] public decimal Price { get; set; }
|
|
||||||
[JsonPropertyName("quantity")] public decimal Quantity { get; set; }
|
|
||||||
[JsonPropertyName("manufacturer")] public string? Manufacturer { get; set; }
|
|
||||||
[JsonPropertyName("trade_name")] public string? TradeName { get; set; }
|
|
||||||
[JsonPropertyName("es_code")] public long EsCode { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerLocationDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("buyer_location_id")] public string? BuyerLocationId { get; set; }
|
|
||||||
[JsonPropertyName("address")] public string? Address { get; set; }
|
|
||||||
[JsonPropertyName("region_name")] public string? RegionName { get; set; }
|
|
||||||
[JsonPropertyName("is_default")] public bool IsDefault { get; set; }
|
|
||||||
public override string ToString() => Address ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerOrderItemReq
|
|
||||||
{
|
|
||||||
[JsonPropertyName("supplier_price_id")] public string SupplierPriceId { get; set; } = "";
|
|
||||||
[JsonPropertyName("qty")] public double Qty { get; set; }
|
|
||||||
[JsonPropertyName("item_name")] public string? ItemName { get; set; }
|
|
||||||
[JsonPropertyName("barcode")] public string? Barcode { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerOrderCreateRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("location_id")] public string? LocationId { get; set; }
|
|
||||||
[JsonPropertyName("comment")] public string? Comment { get; set; }
|
|
||||||
[JsonPropertyName("global_sign")] public string? GlobalSign { get; set; }
|
|
||||||
[JsonPropertyName("items")] public List<BuyerOrderItemReq> Items { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerOrderResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("order_id")] public string? OrderId { get; set; }
|
|
||||||
[JsonPropertyName("status")] public string? Status { get; set; }
|
|
||||||
[JsonPropertyName("global_sign")] public string? GlobalSign { get; set; }
|
|
||||||
[JsonPropertyName("total_amount")] public decimal TotalAmount { get; set; }
|
|
||||||
[JsonPropertyName("items_count")] public int ItemsCount { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
namespace Elfisa.Core;
|
|
||||||
|
|
||||||
/// <summary>Загруженный прайс в памяти (наполняется разделом «Загрузить»).</summary>
|
|
||||||
public static class PriceCache
|
|
||||||
{
|
|
||||||
public static List<PriceItem> Items { get; set; } = new();
|
|
||||||
public static DateTime? LoadedAt { get; set; }
|
|
||||||
public static bool HasData => Items.Count > 0;
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Elfisa.Core;
|
|
||||||
|
|
||||||
public sealed class AppSettings
|
|
||||||
{
|
|
||||||
public string? ApiBaseUrl { get; set; }
|
|
||||||
public string Theme { get; set; } = "Light"; // Light | Dark
|
|
||||||
public string? DefaultLocationId { get; set; }
|
|
||||||
public string? Token { get; set; } // сохранённая сессия
|
|
||||||
public string? Role { get; set; }
|
|
||||||
public string? Username { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Настройки приложения в %APPDATA%\ElfisaAvalonia\settings.json.</summary>
|
|
||||||
public static class SettingsStore
|
|
||||||
{
|
|
||||||
private static readonly string Dir =
|
|
||||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ElfisaAvalonia");
|
|
||||||
private static readonly string File = Path.Combine(Dir, "settings.json");
|
|
||||||
|
|
||||||
public static AppSettings Current { get; private set; } = Load();
|
|
||||||
|
|
||||||
public static AppSettings Load()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(File))
|
|
||||||
return JsonSerializer.Deserialize<AppSettings>(System.IO.File.ReadAllText(File)) ?? new AppSettings();
|
|
||||||
}
|
|
||||||
catch { /* повреждённый файл — начинаем с чистых настроек */ }
|
|
||||||
return new AppSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Save()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Dir);
|
|
||||||
System.IO.File.WriteAllText(File, JsonSerializer.Serialize(Current, new JsonSerializerOptions { WriteIndented = true }));
|
|
||||||
}
|
|
||||||
catch { /* нет прав/диска — молча пропускаем */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user