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 + 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}"; + } + } +}