Release v1.0.26: раздел «Отчёты» для покупателя
Новый раздел «Отчёты» в десктопе (buyer-scoped, данные с сервера): - Мои заказы за период, По поставщикам, По товарам, По аптекам - фильтр по датам, агрегация клиентски из одного набора строк - сервер: GET /api/buyer/report скоупит строки по JWT покупателя (клиент видит ТОЛЬКО свои заказы) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
557f8667ad
commit
a72f2aee81
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -29,6 +29,9 @@
|
||||
<setting name="IntDefaultPercentMarkup" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="ClientMarkupPercent" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="stringLogin" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
@ -38,6 +41,21 @@
|
||||
<setting name="stringToken" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="InvoiceExportPath" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="UpdateCheckBaseUrl" serializeAs="String">
|
||||
<value>https://git.24pharmdata.ru</value>
|
||||
</setting>
|
||||
<setting name="UpdateRepo" serializeAs="String">
|
||||
<value>pharmdata/elfisa-pharmacy</value>
|
||||
</setting>
|
||||
<setting name="UpdateApiToken" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="UpgradeSettings" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
</Электронная_Фармация.Properties.Settings>
|
||||
</userSettings>
|
||||
<runtime>
|
||||
|
||||
Binary file not shown.
@ -374,6 +374,26 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отчёт покупателя: позиции его заказов за период. Сервер скоупит по JWT покупателя.
|
||||
/// </summary>
|
||||
public async Task<List<BuyerReportLineDto>> 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<BuyerReportResponse>(responseBody, JsonOptions);
|
||||
return response?.Lines ?? new List<BuyerReportLineDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать отчёт", ex);
|
||||
throw new HttpRequestException($"Некорректный ответ сервера при формировании отчёта: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
@ -758,6 +778,48 @@ namespace Электронная_Фармация.Classes
|
||||
public List<BuyerOrderListItemDto> Orders { get; set; }
|
||||
}
|
||||
|
||||
public sealed class BuyerReportResponse
|
||||
{
|
||||
[JsonPropertyName("lines")]
|
||||
public List<BuyerReportLineDto> 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")]
|
||||
|
||||
@ -248,6 +248,9 @@
|
||||
<Compile Include="UserControls\UCRefusals.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UserControls\UCReports.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UserControls\UCPriceList.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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")]
|
||||
|
||||
|
||||
182
src/ElectronicPharmacy/UserControls/UCReports.cs
Normal file
182
src/ElectronicPharmacy/UserControls/UCReports.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Раздел «Отчёты» покупателя. Данные тянутся с сервера (/api/buyer/report),
|
||||
/// который скоупит их по JWT покупателя — клиент видит ТОЛЬКО свои заказы.
|
||||
/// Все 4 отчёта агрегируются на клиенте из одного набора строк.
|
||||
/// </summary>
|
||||
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<BuyerReportLineDto> _lines = new List<BuyerReportLineDto>();
|
||||
|
||||
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<BuyerReportLineDto>();
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user