diff --git a/dist/Рлектронная-Фармация/Elfisa.UI.dll b/dist/Рлектронная-Фармация/Elfisa.UI.dll
index e17bc77..77d99fd 100644
Binary files a/dist/Рлектронная-Фармация/Elfisa.UI.dll and b/dist/Рлектронная-Фармация/Elfisa.UI.dll differ
diff --git a/dist/Рлектронная-Фармация/Elfisa.UI.pdb b/dist/Рлектронная-Фармация/Elfisa.UI.pdb
index a900d5b..444b95b 100644
Binary files a/dist/Рлектронная-Фармация/Elfisa.UI.pdb and b/dist/Рлектронная-Фармация/Elfisa.UI.pdb differ
diff --git a/dist/Рлектронная-Фармация/Электронная Фармация.exe b/dist/Рлектронная-Фармация/Электронная Фармация.exe
index bbda518..a11e7f8 100644
Binary files a/dist/Рлектронная-Фармация/Электронная Фармация.exe and b/dist/Рлектронная-Фармация/Электронная Фармация.exe differ
diff --git a/dist/Рлектронная-Фармация/Электронная Фармация.exe.config b/dist/Рлектронная-Фармация/Электронная Фармация.exe.config
index 54fe3b4..2a260d9 100644
--- a/dist/Рлектронная-Фармация/Электронная Фармация.exe.config
+++ b/dist/Рлектронная-Фармация/Электронная Фармация.exe.config
@@ -29,6 +29,9 @@
0
+
+ 0
+
@@ -38,6 +41,21 @@
+
+
+
+
+ https://git.24pharmdata.ru
+
+
+ pharmdata/elfisa-pharmacy
+
+
+
+
+
+ True
+
Электронная_Фармация.Properties.Settings>
diff --git a/dist/Рлектронная-Фармация/Электронная Фармация.pdb b/dist/Рлектронная-Фармация/Электронная Фармация.pdb
index a442355..a1e9ae0 100644
Binary files a/dist/Рлектронная-Фармация/Электронная Фармация.pdb and b/dist/Рлектронная-Фармация/Электронная Фармация.pdb differ
diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs
index e6c263d..4482fc0 100644
--- a/src/ElectronicPharmacy/Classes/ApiClient.cs
+++ b/src/ElectronicPharmacy/Classes/ApiClient.cs
@@ -374,6 +374,26 @@ namespace Электронная_Фармация.Classes
}
}
+ ///
+ /// Отчёт покупателя: позиции его заказов за период. Сервер скоупит по JWT покупателя.
+ ///
+ public async Task> GetBuyerReportAsync(DateTime from, DateTime to)
+ {
+ EnsureAuthenticated();
+ var path = $"/api/buyer/report?date_from={from:yyyy-MM-dd}&date_to={to:yyyy-MM-dd}";
+ var responseBody = await SendAuthorizedGetAsync(path, "формирование отчёта");
+ try
+ {
+ var response = JsonSerializer.Deserialize(responseBody, JsonOptions);
+ return response?.Lines ?? new List();
+ }
+ catch (Exception ex)
+ {
+ AppDebugLog.Error("ApiClient", "Не удалось разобрать отчёт", ex);
+ throw new HttpRequestException($"Некорректный ответ сервера при формировании отчёта: {ex.Message}", ex);
+ }
+ }
+
public async Task> GetBuyerLocationsAsync()
{
EnsureAuthenticated();
@@ -758,6 +778,48 @@ namespace Электронная_Фармация.Classes
public List Orders { get; set; }
}
+ public sealed class BuyerReportResponse
+ {
+ [JsonPropertyName("lines")]
+ public List Lines { get; set; }
+ }
+
+ public sealed class BuyerReportLineDto
+ {
+ [JsonPropertyName("order_id")]
+ public string OrderID { get; set; }
+
+ [JsonPropertyName("global_sign")]
+ public string GlobalSign { get; set; }
+
+ [JsonPropertyName("order_date")]
+ public DateTime? OrderDate { get; set; }
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; }
+
+ [JsonPropertyName("supplier")]
+ public string Supplier { get; set; }
+
+ [JsonPropertyName("location")]
+ public string Location { get; set; }
+
+ [JsonPropertyName("item_name")]
+ public string ItemName { get; set; }
+
+ [JsonPropertyName("item_code")]
+ public string ItemCode { get; set; }
+
+ [JsonPropertyName("qty")]
+ public decimal Qty { get; set; }
+
+ [JsonPropertyName("unit_price")]
+ public decimal UnitPrice { get; set; }
+
+ [JsonPropertyName("sum")]
+ public decimal Sum { get; set; }
+ }
+
public sealed class BuyerLocationDto
{
[JsonPropertyName("buyer_location_id")]
diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
index a5da65e..f63d868 100644
--- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj
+++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
@@ -248,6 +248,9 @@
UserControl
+
+ UserControl
+
UserControl
diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs
index 89c3382..5e92beb 100644
--- a/src/ElectronicPharmacy/Form1.UiShell.cs
+++ b/src/ElectronicPharmacy/Form1.UiShell.cs
@@ -61,6 +61,7 @@ namespace Электронная_Фармация
new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" },
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" },
new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" },
+ new SidebarItem { Key = "reports", Text = "Отчёты", IconGlyph = "" },
new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" },
new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" },
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" },
@@ -321,6 +322,9 @@ namespace Электронная_Фармация
case "invoices":
OpenInvoicesTab();
break;
+ case "reports":
+ OpenReportsTab();
+ break;
case "upload":
tsBtnDownloadData_Click(this, EventArgs.Empty);
_appHeader.SetActive("upload");
@@ -499,6 +503,12 @@ namespace Электронная_Фармация
OpenDocumentTab("Отказы", "refusals", refusals, allowDuplicate: false);
}
+ private void OpenReportsTab()
+ {
+ var reports = new UCReports();
+ OpenDocumentTab("Отчёты", "reports", reports, allowDuplicate: false);
+ }
+
partial void ShowShellLoading(string message)
{
_loadingOverlay?.Show(message);
diff --git a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
index 174ef7d..08fd159 100644
--- a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
+++ b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
@@ -32,6 +32,6 @@ using System.Runtime.InteropServices;
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.25.0")]
-[assembly: AssemblyFileVersion("1.0.25.0")]
+[assembly: AssemblyVersion("1.0.26.0")]
+[assembly: AssemblyFileVersion("1.0.26.0")]
diff --git a/src/ElectronicPharmacy/UserControls/UCReports.cs b/src/ElectronicPharmacy/UserControls/UCReports.cs
new file mode 100644
index 0000000..20d01a1
--- /dev/null
+++ b/src/ElectronicPharmacy/UserControls/UCReports.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using Электронная_Фармация.Classes;
+using Электронная_Фармация.Properties;
+
+namespace Электронная_Фармация.UserControls
+{
+ ///
+ /// Раздел «Отчёты» покупателя. Данные тянутся с сервера (/api/buyer/report),
+ /// который скоупит их по JWT покупателя — клиент видит ТОЛЬКО свои заказы.
+ /// Все 4 отчёта агрегируются на клиенте из одного набора строк.
+ ///
+ public class UCReports : UserControl
+ {
+ private readonly DateTimePicker _dtFrom;
+ private readonly DateTimePicker _dtTo;
+ private readonly ComboBox _cboReport;
+ private readonly Button _btnBuild;
+ private readonly Label _lblTotals;
+ private readonly DataGridView _grid;
+ private List _lines = new List();
+
+ public UCReports()
+ {
+ Dock = DockStyle.Fill;
+
+ var top = new Panel { Dock = DockStyle.Top, Height = 46 };
+
+ var lblFrom = new Label { Text = "С", AutoSize = true, Left = 10, Top = 15 };
+ _dtFrom = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 110, Left = 30, Top = 11, Value = DateTime.Today.AddMonths(-1) };
+ var lblTo = new Label { Text = "по", AutoSize = true, Left = 146, Top = 15 };
+ _dtTo = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 110, Left = 172, Top = 11, Value = DateTime.Today };
+ _cboReport = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 190, Left = 292, Top = 11 };
+ _cboReport.Items.AddRange(new object[] { "Мои заказы", "По поставщикам", "По товарам", "По аптекам" });
+ _cboReport.SelectedIndex = 0;
+ _btnBuild = new Button { Text = "Сформировать", Left = 492, Top = 10, Width = 120, Height = 26 };
+ _lblTotals = new Label { AutoSize = true, Left = 624, Top = 15, Text = "" };
+
+ top.Controls.AddRange(new Control[] { lblFrom, _dtFrom, lblTo, _dtTo, _cboReport, _btnBuild, _lblTotals });
+
+ _grid = new DataGridView
+ {
+ Dock = DockStyle.Fill,
+ ReadOnly = true,
+ AllowUserToAddRows = false,
+ AllowUserToDeleteRows = false,
+ AllowUserToResizeRows = false,
+ RowHeadersVisible = false,
+ SelectionMode = DataGridViewSelectionMode.FullRowSelect,
+ AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
+ BorderStyle = BorderStyle.None
+ };
+
+ Controls.Add(_grid);
+ Controls.Add(top);
+
+ _btnBuild.Click += async (_, __) => await LoadAsync();
+ _cboReport.SelectedIndexChanged += (_, __) => Render();
+ }
+
+ private async Task LoadAsync()
+ {
+ try
+ {
+ _btnBuild.Enabled = false;
+ var token = Settings.Default.stringToken;
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ ToastNotification.ShowError("Не найден токен авторизации. Войдите в систему.");
+ return;
+ }
+
+ var client = new ApiClient();
+ client.SetToken(token);
+ _lines = await client.GetBuyerReportAsync(_dtFrom.Value.Date, _dtTo.Value.Date)
+ ?? new List();
+ Render();
+
+ if (_lines.Count == 0)
+ {
+ ToastNotification.ShowCustom("За выбранный период заказов нет.", Color.DarkOrange, Color.White);
+ }
+ }
+ catch (Exception ex)
+ {
+ AppDebugLog.Error("UCReports", "Ошибка формирования отчёта", ex);
+ ToastNotification.ShowError("Ошибка отчёта: " + ex.Message);
+ }
+ finally
+ {
+ _btnBuild.Enabled = true;
+ }
+ }
+
+ private void Render()
+ {
+ var dt = new DataTable();
+
+ switch (_cboReport.SelectedIndex)
+ {
+ case 0: // Мои заказы (по заказу)
+ dt.Columns.Add("Дата", typeof(string));
+ dt.Columns.Add("Номер", typeof(string));
+ dt.Columns.Add("Поставщик", typeof(string));
+ dt.Columns.Add("Аптека", typeof(string));
+ dt.Columns.Add("Позиций", typeof(int));
+ dt.Columns.Add("Сумма", typeof(decimal));
+ dt.Columns.Add("Статус", typeof(string));
+ foreach (var g in _lines.GroupBy(l => l.OrderID)
+ .OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
+ {
+ var f = g.First();
+ var suppliers = string.Join(", ", g.Select(x => x.Supplier)
+ .Where(s => !string.IsNullOrWhiteSpace(s)).Distinct());
+ dt.Rows.Add(
+ f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
+ f.GlobalSign,
+ suppliers,
+ f.Location,
+ g.Count(),
+ g.Sum(x => x.Sum),
+ f.Status);
+ }
+ break;
+
+ case 1: // По поставщикам
+ dt.Columns.Add("Поставщик", typeof(string));
+ dt.Columns.Add("Заказов", typeof(int));
+ dt.Columns.Add("Позиций", typeof(int));
+ dt.Columns.Add("Сумма", typeof(decimal));
+ foreach (var g in _lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Supplier) ? "(не указан)" : l.Supplier)
+ .OrderByDescending(x => x.Sum(y => y.Sum)))
+ {
+ dt.Rows.Add(g.Key, g.Select(x => x.OrderID).Distinct().Count(), g.Count(), g.Sum(x => x.Sum));
+ }
+ break;
+
+ case 2: // По товарам
+ dt.Columns.Add("Товар", typeof(string));
+ dt.Columns.Add("Код", typeof(string));
+ dt.Columns.Add("Кол-во", typeof(decimal));
+ dt.Columns.Add("Сумма", typeof(decimal));
+ foreach (var g in _lines.GroupBy(l => ((l.ItemName ?? "").Trim() + "|" + (l.ItemCode ?? "")))
+ .OrderByDescending(x => x.Sum(y => y.Sum)))
+ {
+ var f = g.First();
+ dt.Rows.Add(f.ItemName, f.ItemCode, g.Sum(x => x.Qty), g.Sum(x => x.Sum));
+ }
+ break;
+
+ case 3: // По аптекам (грузополучателям)
+ dt.Columns.Add("Аптека", typeof(string));
+ dt.Columns.Add("Заказов", typeof(int));
+ dt.Columns.Add("Позиций", typeof(int));
+ dt.Columns.Add("Сумма", typeof(decimal));
+ foreach (var g in _lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Location) ? "(не указана)" : l.Location)
+ .OrderByDescending(x => x.Sum(y => y.Sum)))
+ {
+ dt.Rows.Add(g.Key, g.Select(x => x.OrderID).Distinct().Count(), g.Count(), g.Sum(x => x.Sum));
+ }
+ break;
+ }
+
+ _grid.DataSource = dt;
+ foreach (DataGridViewColumn c in _grid.Columns)
+ {
+ if (c.Name == "Сумма") c.DefaultCellStyle.Format = "N2";
+ if (c.Name == "Кол-во") c.DefaultCellStyle.Format = "N3";
+ if (c.Name == "Позиций" || c.Name == "Заказов") c.Width = 80;
+ }
+
+ int orders = _lines.Select(l => l.OrderID).Distinct().Count();
+ decimal totalSum = _lines.Sum(l => l.Sum);
+ _lblTotals.Text = $"Заказов: {orders} Позиций: {_lines.Count} Сумма: {totalSum:N2}";
+ }
+ }
+}