diff --git a/src/ElectronicPharmacy/App.config b/src/ElectronicPharmacy/App.config
index 6eae6c4..761ce75 100644
--- a/src/ElectronicPharmacy/App.config
+++ b/src/ElectronicPharmacy/App.config
@@ -36,6 +36,9 @@
+
+
+
Электронная_Фармация.Properties.Settings>
\ No newline at end of file
diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs
index 567852f..1706e0b 100644
--- a/src/ElectronicPharmacy/Classes/ApiClient.cs
+++ b/src/ElectronicPharmacy/Classes/ApiClient.cs
@@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
- public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null)
+ public async Task GetPriceSummaryAsync(
+ string supplierId = null,
+ string regionId = null,
+ string q = null,
+ int? limit = null,
+ int? offset = null,
+ IProgress transferProgress = null)
{
EnsureAuthenticated();
@@ -130,7 +136,10 @@ namespace Электронная_Фармация.Classes
path += "?" + string.Join("&", queryParams);
}
- var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса");
+ var responseBody = transferProgress == null
+ ? await SendAuthorizedGetAsync(path, "загрузку прайса")
+ : await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress);
+
try
{
var summary = JsonSerializer.Deserialize(responseBody, JsonOptions);
@@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes
}
private async Task SendAuthorizedGetAsync(string path, string operationName)
+ {
+ return await SendAuthorizedGetWithProgressAsync(path, operationName, null);
+ }
+
+ private async Task SendAuthorizedGetWithProgressAsync(
+ string path,
+ string operationName,
+ IProgress transferProgress)
{
var fullUrl = BuildFullUrl(path);
var stopwatch = Stopwatch.StartNew();
@@ -185,8 +202,8 @@ namespace Электронная_Фармация.Classes
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
{
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
- var response = await _httpClient.SendAsync(httpRequest);
- var responseBody = await response.Content.ReadAsStringAsync();
+ var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
+ var responseBody = await ReadContentWithProgressAsync(response, transferProgress);
stopwatch.Stop();
AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
@@ -243,6 +260,29 @@ namespace Электронная_Фармация.Classes
}
}
+ private static async Task ReadContentWithProgressAsync(
+ HttpResponseMessage response,
+ IProgress transferProgress)
+ {
+ var totalBytes = response.Content.Headers.ContentLength;
+ using (var stream = await response.Content.ReadAsStreamAsync())
+ using (var memory = new System.IO.MemoryStream())
+ {
+ var buffer = new byte[81920];
+ long received = 0;
+ int read;
+ while ((read = await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
+ {
+ memory.Write(buffer, 0, read);
+ received += read;
+ transferProgress?.Report(new HttpTransferProgress(received, totalBytes));
+ }
+
+ transferProgress?.Report(new HttpTransferProgress(received, totalBytes ?? received));
+ return Encoding.UTF8.GetString(memory.ToArray());
+ }
+ }
+
private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
{
if (response.IsSuccessStatusCode)
@@ -333,4 +373,29 @@ namespace Электронная_Фармация.Classes
public string InvoiceSum { get; set; }
public string RefuseSum { get; set; }
}
+
+ public sealed class HttpTransferProgress
+ {
+ public HttpTransferProgress(long bytesReceived, long? totalBytes)
+ {
+ BytesReceived = bytesReceived;
+ TotalBytes = totalBytes;
+ }
+
+ public long BytesReceived { get; }
+ public long? TotalBytes { get; }
+
+ public int Percent
+ {
+ get
+ {
+ if (!TotalBytes.HasValue || TotalBytes.Value <= 0)
+ {
+ return 0;
+ }
+
+ return (int)Math.Max(0, Math.Min(100, (100.0 * BytesReceived) / TotalBytes.Value));
+ }
+ }
+ }
}
diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs
index f606bcd..459c3dc 100644
--- a/src/ElectronicPharmacy/Classes/AppConfig.cs
+++ b/src/ElectronicPharmacy/Classes/AppConfig.cs
@@ -108,5 +108,16 @@ namespace Электронная_Фармация.Classes
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
Settings.Default.Save();
}
+
+ public static string InvoiceExportPath
+ {
+ get { return Settings.Default.InvoiceExportPath ?? string.Empty; }
+ }
+
+ public static void SetInvoiceExportPath(string path)
+ {
+ Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim();
+ Settings.Default.Save();
+ }
}
}
diff --git a/src/ElectronicPharmacy/Classes/InvoiceExportService.cs b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs
new file mode 100644
index 0000000..39025fe
--- /dev/null
+++ b/src/ElectronicPharmacy/Classes/InvoiceExportService.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Windows.Forms;
+using System.Data.SQLite;
+
+namespace Электронная_Фармация.Classes
+{
+ ///
+ /// Exports a local invoice (header + items) to a CSV file in the configured folder.
+ ///
+ public sealed class InvoiceExportService
+ {
+ public string ExportInvoice(string invoiceId, IWin32Window owner = null)
+ {
+ if (string.IsNullOrWhiteSpace(invoiceId))
+ {
+ throw new ArgumentException("Не указана накладная для экспорта.", nameof(invoiceId));
+ }
+
+ var folder = ResolveExportFolder(owner);
+ if (string.IsNullOrWhiteSpace(folder))
+ {
+ throw new InvalidOperationException("Не указана папка для экспорта накладных.");
+ }
+
+ if (!Directory.Exists(folder))
+ {
+ Directory.CreateDirectory(folder);
+ }
+
+ InvoiceHeader header;
+ using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
+ {
+ con.Open();
+ header = LoadHeader(con, invoiceId);
+ var itemsSql = @"
+select
+ ifnull([GoodCode], '') as [GoodCode],
+ ifnull([Good], '') as [Good],
+ ifnull([ProducerName], '') as [ProducerName],
+ ifnull([GoodsGount], 0) as [Qty],
+ ifnull([PriceSupplierWithNDS], ifnull([PriceSupplierWithoutNDS], 0)) as [Price],
+ ifnull([SumWithNDS], ifnull([SumWithoutNDS], 0)) as [Sum],
+ ifnull([BestBefore], '') as [BestBefore],
+ ifnull([Serial], '') as [Serial]
+from [InvoiceItem]
+where [idInvoice] = @invoiceId
+order by [Good];";
+
+ var fileName = BuildFileName(header);
+ var filePath = Path.Combine(folder, fileName);
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Номер накладной;Дата;Поставщик;Грузополучатель;Номер заказа;Сумма");
+ sb.AppendLine(string.Join(";",
+ Csv(header.Number),
+ Csv(header.Date),
+ Csv(header.Supplier),
+ Csv(header.Consignee),
+ Csv(header.OrderNumber),
+ Csv(header.Sum)));
+ sb.AppendLine();
+ sb.AppendLine("Код товара;Товар;Производитель;Количество;Цена;Сумма;Срок годности;Серия");
+
+ using (var cmd = new SQLiteCommand(itemsSql, con))
+ {
+ cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ sb.AppendLine(string.Join(";",
+ Csv(reader["GoodCode"]),
+ Csv(reader["Good"]),
+ Csv(reader["ProducerName"]),
+ Csv(reader["Qty"]),
+ Csv(reader["Price"]),
+ Csv(reader["Sum"]),
+ Csv(reader["BestBefore"]),
+ Csv(reader["Serial"])));
+ }
+ }
+ }
+
+ // UTF-8 BOM — Excel корректно открывает кириллицу.
+ File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
+ AppDebugLog.Info("InvoiceExport", $"Накладная {header.Number} экспортирована: {filePath}");
+ return filePath;
+ }
+ }
+
+ public static string ResolveExportFolder(IWin32Window owner = null)
+ {
+ var folder = AppConfig.InvoiceExportPath;
+ if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
+ {
+ return folder;
+ }
+
+ using (var dialog = new FolderBrowserDialog())
+ {
+ dialog.Description = "Выберите папку для экспорта накладных";
+ dialog.ShowNewFolderButton = true;
+ if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
+ {
+ dialog.SelectedPath = folder;
+ }
+
+ if (dialog.ShowDialog(owner) != DialogResult.OK || string.IsNullOrWhiteSpace(dialog.SelectedPath))
+ {
+ return string.Empty;
+ }
+
+ AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
+ return dialog.SelectedPath;
+ }
+ }
+
+ private static InvoiceHeader LoadHeader(SQLiteConnection con, string invoiceId)
+ {
+ using (var cmd = new SQLiteCommand(@"
+select
+ ifnull([InvoiceNumber], '') as [Number],
+ ifnull([InvoiceDate], '') as [Date],
+ ifnull([SupplierName], '') as [Supplier],
+ ifnull([ConsigneesName], '') as [Consignee],
+ ifnull([SourceOrderNumber], '') as [OrderNumber],
+ ifnull([InvoiceSum], ifnull([SumWithNDS], 0)) as [Sum]
+from [Invoice]
+where [idInvoice] = @invoiceId
+limit 1;", con))
+ {
+ cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (!reader.Read())
+ {
+ throw new InvalidOperationException("Накладная не найдена.");
+ }
+
+ return new InvoiceHeader
+ {
+ Number = reader["Number"]?.ToString() ?? string.Empty,
+ Date = reader["Date"]?.ToString() ?? string.Empty,
+ Supplier = reader["Supplier"]?.ToString() ?? string.Empty,
+ Consignee = reader["Consignee"]?.ToString() ?? string.Empty,
+ OrderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty,
+ Sum = reader["Sum"]?.ToString() ?? "0"
+ };
+ }
+ }
+ }
+
+ private static string BuildFileName(InvoiceHeader header)
+ {
+ var number = SanitizeFileName(string.IsNullOrWhiteSpace(header.Number) ? "invoice" : header.Number);
+ var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
+ return $"Накладная_{number}_{stamp}.csv";
+ }
+
+ private static string SanitizeFileName(string value)
+ {
+ foreach (var c in Path.GetInvalidFileNameChars())
+ {
+ value = value.Replace(c, '_');
+ }
+
+ return value.Trim();
+ }
+
+ private static string Csv(object value)
+ {
+ var text = value?.ToString() ?? string.Empty;
+ if (text.IndexOfAny(new[] { ';', '"', '\r', '\n' }) >= 0)
+ {
+ return "\"" + text.Replace("\"", "\"\"") + "\"";
+ }
+
+ return text;
+ }
+
+ private sealed class InvoiceHeader
+ {
+ public string Number { get; set; }
+ public string Date { get; set; }
+ public string Supplier { get; set; }
+ public string Consignee { get; set; }
+ public string OrderNumber { get; set; }
+ public string Sum { get; set; }
+ }
+ }
+}
diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
index aace520..fd4daf9 100644
--- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj
+++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
@@ -106,6 +106,7 @@
+
diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs
index e68f899..e9c9758 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.Designer.cs
@@ -32,17 +32,49 @@
this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView();
this.DataSetPriceList = new System.Data.DataSet();
this.btnCloseThis = new Elfisa.UI.Controls.ModernButton();
+ this.lblTitle = new System.Windows.Forms.Label();
+ this.lblProgress = new System.Windows.Forms.Label();
+ this.progressDownload = new System.Windows.Forms.ProgressBar();
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit();
this.SuspendLayout();
//
+ // lblTitle
+ //
+ this.lblTitle.AutoSize = true;
+ this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
+ this.lblTitle.Location = new System.Drawing.Point(22, 14);
+ this.lblTitle.Name = "lblTitle";
+ this.lblTitle.Size = new System.Drawing.Size(220, 28);
+ this.lblTitle.TabIndex = 5;
+ this.lblTitle.Text = "Загрузка прайс-листа";
+ //
+ // progressDownload
+ //
+ this.progressDownload.Location = new System.Drawing.Point(25, 52);
+ this.progressDownload.Name = "progressDownload";
+ this.progressDownload.Size = new System.Drawing.Size(630, 22);
+ this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Continuous;
+ this.progressDownload.Maximum = 100;
+ this.progressDownload.TabIndex = 6;
+ //
+ // lblProgress
+ //
+ this.lblProgress.AutoEllipsis = true;
+ this.lblProgress.Font = new System.Drawing.Font("Segoe UI", 10F);
+ this.lblProgress.Location = new System.Drawing.Point(25, 82);
+ this.lblProgress.Name = "lblProgress";
+ this.lblProgress.Size = new System.Drawing.Size(630, 24);
+ this.lblProgress.TabIndex = 7;
+ this.lblProgress.Text = "Подготовка...";
+ //
// rtxtDebug
//
this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control;
this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.None;
- this.rtxtDebug.Location = new System.Drawing.Point(25, 12);
+ this.rtxtDebug.Location = new System.Drawing.Point(25, 116);
this.rtxtDebug.Name = "rtxtDebug";
- this.rtxtDebug.Size = new System.Drawing.Size(630, 319);
+ this.rtxtDebug.Size = new System.Drawing.Size(630, 215);
this.rtxtDebug.TabIndex = 0;
this.rtxtDebug.Text = "";
//
@@ -79,6 +111,9 @@
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.Control;
this.ClientSize = new System.Drawing.Size(684, 396);
+ this.Controls.Add(this.lblProgress);
+ this.Controls.Add(this.progressDownload);
+ this.Controls.Add(this.lblTitle);
this.Controls.Add(this.btnCloseThis);
this.Controls.Add(this.dataGridView1);
this.Controls.Add(this.rtxtDebug);
@@ -93,7 +128,7 @@
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit();
this.ResumeLayout(false);
-
+ this.PerformLayout();
}
#endregion
@@ -102,5 +137,8 @@
private Elfisa.UI.Controls.ModernDataGridView dataGridView1;
private System.Data.DataSet DataSetPriceList;
private Elfisa.UI.Controls.ModernButton btnCloseThis;
+ private System.Windows.Forms.Label lblTitle;
+ private System.Windows.Forms.Label lblProgress;
+ private System.Windows.Forms.ProgressBar progressDownload;
}
-}
\ No newline at end of file
+}
diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
index 6f0a8e0..3d8b23b 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs
@@ -12,6 +12,11 @@ namespace Электронная_Фармация.HelpForms
{
public partial class HF_DownloadDataFromServer : Form
{
+ // Доли общего прогресса 0..100.
+ private const int DownloadPhaseEnd = 45;
+ private const int BuildPhaseEnd = 55;
+ private const int SavePhaseEnd = 100;
+
public HF_DownloadDataFromServer()
{
InitializeComponent();
@@ -20,6 +25,7 @@ namespace Электронная_Фармация.HelpForms
private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
+ private bool _isDownloading;
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
{
@@ -28,6 +34,8 @@ namespace Электронная_Фармация.HelpForms
dataGridView1.Visible = false;
rtxtDebug.Visible = true;
rtxtDebug.ReadOnly = true;
+ _isDownloading = true;
+ SetOverallProgress(0, "Подключение к серверу...");
var client = new ApiClient();
AppendStatus($"Сервер: {client.BaseUrl}");
@@ -36,39 +44,67 @@ namespace Электронная_Фармация.HelpForms
try
{
+ SetOverallProgress(1, "Авторизация...");
await EnsureAuthenticatedAsync(client);
- AppendStatus("Загружаю сводный прайс (постранично, весь)...");
+ SetOverallProgress(3, "Загрузка прайса с сервера...");
+ AppendStatus("Загружаю сводный прайс...");
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
- AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций");
+ int totalItems = allSuppliersSummary.Summary?.Count ?? 0;
+ AppendStatus($"Получено {totalItems} позиций");
+ SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)");
- var tablePrice = BuildPriceTable(allSuppliersSummary);
- SavePriceTable(tablePrice);
+ SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы...");
+ var buildProgress = new Progress(p =>
+ {
+ int phasePercent = MapPhasePercent(p.Current, p.Total, DownloadPhaseEnd, BuildPhaseEnd);
+ SetOverallProgress(
+ phasePercent,
+ $"Обработка: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)");
+ });
+ var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, buildProgress));
+ SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу...");
+ var saveProgress = new Progress(p =>
+ {
+ int phasePercent = MapPhasePercent(p.Current, p.Total, BuildPhaseEnd, SavePhaseEnd);
+ SetOverallProgress(
+ phasePercent,
+ $"Сохранение: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)");
+ });
+ await Task.Run(() => SavePriceTable(tablePrice, saveProgress));
+
+ SetOverallProgress(100, $"Готово: {tablePrice.Rows.Count} / {tablePrice.Rows.Count} (100%)");
DialogResult = DialogResult.OK;
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
}
catch (UnauthorizedAccessException ex)
{
+ SetOverallProgress(0, "Ошибка авторизации");
AppendStatus($"Ошибка авторизации: {ex.Message}");
AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
}
catch (HttpRequestException ex)
{
+ SetOverallProgress(0, "Ошибка сети");
AppendStatus($"Ошибка сети: {ex.Message}");
AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
}
catch (Exception ex)
{
+ SetOverallProgress(0, "Ошибка загрузки");
AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
}
-
- btnCloseThis.Enabled = true;
+ finally
+ {
+ _isDownloading = false;
+ btnCloseThis.Enabled = true;
+ }
}
private async Task EnsureAuthenticatedAsync(ApiClient client)
@@ -98,39 +134,51 @@ namespace Электронная_Фармация.HelpForms
AppendStatus("Авторизация успешно пройдена");
}
- // Тянем весь прайс постранично: сервер отдаёт максимум 5000 за запрос,
- // поэтому идём offset'ом, пока страница не окажется неполной.
private async Task GetFullPriceSummaryPagedAsync(ApiClient client)
{
var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
? null
: Settings.Default.RegionId;
- // Тянем весь прайс одним запросом: постраничный OFFSET на тяжёлом
- // сводном запросе валит сервер на глубоких страницах. Один запрос =
- // один расчёт выборки. Страховка-цикл остаётся на случай, если позиций
- // окажется больше pageSize.
+ // Один крупный запрос (OFFSET на больших страницах валит сервер).
const int pageSize = 200000;
const int maxPages = 100;
var combined = new PriceSummaryResponse { Summary = new List() };
+ var transferProgress = new Progress(ReportTransferProgress);
for (int page = 0; page < maxPages; page++)
{
int offset = page * pageSize;
+ SetOverallProgress(
+ Math.Min(DownloadPhaseEnd - 1, 3 + page),
+ page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}...");
+
PriceSummaryResponse chunk;
try
{
- chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset);
+ chunk = await client.GetPriceSummaryAsync(
+ supplierId: null,
+ regionId: regionId,
+ limit: pageSize,
+ offset: offset,
+ transferProgress: transferProgress);
}
catch (UnauthorizedAccessException)
{
AppendStatus("Токен недействителен, повторная авторизация...");
+ SetOverallProgress(2, "Повторная авторизация...");
Settings.Default.stringToken = string.Empty;
Settings.Default.Save();
client.SetToken(null);
await LoginAndSaveTokenAsync(client);
- chunk = await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId, limit: pageSize, offset: offset);
+ SetOverallProgress(3, "Повторная загрузка прайса...");
+ chunk = await client.GetPriceSummaryAsync(
+ supplierId: null,
+ regionId: regionId,
+ limit: pageSize,
+ offset: offset,
+ transferProgress: transferProgress);
}
int got = chunk?.Summary?.Count ?? 0;
@@ -142,14 +190,33 @@ namespace Электронная_Фармация.HelpForms
if (got < pageSize)
{
- break; // последняя (неполная) страница — дальше данных нет
+ break;
}
}
return combined;
}
- private static DataTable BuildPriceTable(PriceSummaryResponse allSuppliersSummary)
+ private void ReportTransferProgress(HttpTransferProgress transfer)
+ {
+ if (transfer.TotalBytes.HasValue && transfer.TotalBytes.Value > 0)
+ {
+ int phasePercent = MapPhasePercent(transfer.BytesReceived, transfer.TotalBytes.Value, 3, DownloadPhaseEnd);
+ SetOverallProgress(
+ phasePercent,
+ $"Скачивание: {FormatBytes(transfer.BytesReceived)} / {FormatBytes(transfer.TotalBytes.Value)} ({transfer.Percent}%)");
+ }
+ else
+ {
+ SetOverallProgress(
+ Math.Min(DownloadPhaseEnd - 1, 5),
+ $"Скачивание: получено {FormatBytes(transfer.BytesReceived)}...");
+ }
+ }
+
+ private static DataTable BuildPriceTable(
+ PriceSummaryResponse allSuppliersSummary,
+ IProgress progress = null)
{
var tablePrice = new DataTable();
tablePrice.Columns.Add("supplier_price_id");
@@ -192,6 +259,8 @@ namespace Электронная_Фармация.HelpForms
return tablePrice;
}
+ int total = allSuppliersSummary.Summary.Count;
+ int current = 0;
foreach (var item in allSuppliersSummary.Summary)
{
var row = tablePrice.NewRow();
@@ -210,12 +279,18 @@ namespace Электронная_Фармация.HelpForms
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim();
row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
tablePrice.Rows.Add(row);
+
+ current++;
+ if (progress != null && (current % 500 == 0 || current == total))
+ {
+ progress.Report(new CountProgress(current, total));
+ }
}
return tablePrice;
}
- private void SavePriceTable(DataTable tablePrice)
+ private void SavePriceTable(DataTable tablePrice, IProgress progress = null)
{
const string commandToDelete = "delete from [PriceList]";
const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
@@ -253,6 +328,7 @@ VALUES
@Dosage
)";
+ int total = tablePrice.Rows.Count;
using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb))
{
sqliteCon.Open();
@@ -266,6 +342,7 @@ VALUES
using (var transaction = sqliteCon.BeginTransaction())
{
+ int saved = 0;
foreach (DataRow row in tablePrice.Rows)
{
using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
@@ -286,6 +363,12 @@ VALUES
cmd.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value);
cmd.ExecuteNonQuery();
}
+
+ saved++;
+ if (progress != null && (saved % 250 == 0 || saved == total))
+ {
+ progress.Report(new CountProgress(saved, total));
+ }
}
transaction.Commit();
@@ -293,6 +376,68 @@ VALUES
}
}
+ private static int MapPhasePercent(long current, long total, int phaseStart, int phaseEnd)
+ {
+ if (total <= 0)
+ {
+ return phaseStart;
+ }
+
+ double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total));
+ return phaseStart + (int)((phaseEnd - phaseStart) * ratio);
+ }
+
+ private static int PercentOf(long current, long total)
+ {
+ if (total <= 0)
+ {
+ return 0;
+ }
+
+ return (int)Math.Max(0, Math.Min(100, (100.0 * current) / total));
+ }
+
+ private static string FormatBytes(long bytes)
+ {
+ if (bytes < 1024)
+ {
+ return $"{bytes} Б";
+ }
+
+ if (bytes < 1024 * 1024)
+ {
+ return $"{bytes / 1024.0:0.0} КБ";
+ }
+
+ return $"{bytes / (1024.0 * 1024.0):0.00} МБ";
+ }
+
+ private void SetOverallProgress(int percent, string text)
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(SetOverallProgress), percent, text);
+ return;
+ }
+
+ if (progressDownload.Style != ProgressBarStyle.Continuous)
+ {
+ progressDownload.Style = ProgressBarStyle.Continuous;
+ }
+
+ progressDownload.Minimum = 0;
+ progressDownload.Maximum = 100;
+ progressDownload.Value = Math.Max(0, Math.Min(100, percent));
+ lblProgress.Text = $"{Math.Max(0, Math.Min(100, percent))}% — {text}";
+ lblProgress.Refresh();
+ progressDownload.Refresh();
+ }
+
private void AppendStatus(string message)
{
if (InvokeRequired)
@@ -310,13 +455,27 @@ VALUES
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
{
- Dispose();
- Close();
+ if (_isDownloading)
+ {
+ e.Cancel = true;
+ }
}
private void btnCloseThis_Click(object sender, EventArgs e)
{
Close();
}
+
+ private sealed class CountProgress
+ {
+ public CountProgress(int current, int total)
+ {
+ Current = current;
+ Total = total;
+ }
+
+ public int Current { get; }
+ public int Total { get; }
+ }
}
}
diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs
index b9a93d7..3447438 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.Designer.cs
@@ -40,6 +40,9 @@
this.label3 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
+ this.labelExportPath = new System.Windows.Forms.Label();
+ this.txtInvoiceExportPath = new Elfisa.UI.Controls.ModernTextBox();
+ this.btnBrowseExportPath = new Elfisa.UI.Controls.ModernButton();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
@@ -79,9 +82,55 @@
this.txtPassword.Size = new System.Drawing.Size(375, 34);
this.txtPassword.TabIndex = 3;
//
+ // label5
+ //
+ this.label5.AutoSize = true;
+ this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
+ this.label5.Location = new System.Drawing.Point(23, 225);
+ this.label5.Name = "label5";
+ this.label5.Size = new System.Drawing.Size(103, 28);
+ this.label5.TabIndex = 11;
+ this.label5.Text = "URL API";
+ //
+ // txtApiUrl
+ //
+ this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
+ this.txtApiUrl.Location = new System.Drawing.Point(132, 225);
+ this.txtApiUrl.Name = "txtApiUrl";
+ this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
+ this.txtApiUrl.TabIndex = 12;
+ //
+ // labelExportPath
+ //
+ this.labelExportPath.AutoSize = true;
+ this.labelExportPath.Font = new System.Drawing.Font("Segoe UI", 12F);
+ this.labelExportPath.Location = new System.Drawing.Point(23, 272);
+ this.labelExportPath.Name = "labelExportPath";
+ this.labelExportPath.Size = new System.Drawing.Size(280, 28);
+ this.labelExportPath.TabIndex = 13;
+ this.labelExportPath.Text = "Папка экспорта накладных";
+ //
+ // txtInvoiceExportPath
+ //
+ this.txtInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 10F);
+ this.txtInvoiceExportPath.Location = new System.Drawing.Point(28, 304);
+ this.txtInvoiceExportPath.Name = "txtInvoiceExportPath";
+ this.txtInvoiceExportPath.Size = new System.Drawing.Size(320, 30);
+ this.txtInvoiceExportPath.TabIndex = 14;
+ //
+ // btnBrowseExportPath
+ //
+ this.btnBrowseExportPath.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
+ this.btnBrowseExportPath.Location = new System.Drawing.Point(354, 304);
+ this.btnBrowseExportPath.Name = "btnBrowseExportPath";
+ this.btnBrowseExportPath.Size = new System.Drawing.Size(49, 30);
+ this.btnBrowseExportPath.TabIndex = 15;
+ this.btnBrowseExportPath.Text = "...";
+ this.btnBrowseExportPath.Click += new System.EventHandler(this.btnBrowseExportPath_Click);
+ //
// btnConfirmRegistration
//
- this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 300);
+ this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 356);
this.btnConfirmRegistration.Name = "btnConfirmRegistration";
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
this.btnConfirmRegistration.TabIndex = 5;
@@ -90,7 +139,7 @@
//
// btnCloseThis
//
- this.btnCloseThis.Location = new System.Drawing.Point(28, 300);
+ this.btnCloseThis.Location = new System.Drawing.Point(28, 356);
this.btnCloseThis.Name = "btnCloseThis";
this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
this.btnCloseThis.TabIndex = 6;
@@ -102,7 +151,7 @@
this.checkTokenGained.AutoCheck = false;
this.checkTokenGained.AutoSize = true;
this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F);
- this.checkTokenGained.Location = new System.Drawing.Point(28, 260);
+ this.checkTokenGained.Location = new System.Drawing.Point(28, 420);
this.checkTokenGained.Name = "checkTokenGained";
this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
this.checkTokenGained.TabIndex = 7;
@@ -133,29 +182,14 @@
this.label3.Text = "АВТОРИЗАЦИЯ";
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
- // label5
- //
- this.label5.AutoSize = true;
- this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
- this.label5.Location = new System.Drawing.Point(23, 225);
- this.label5.Name = "label5";
- this.label5.Size = new System.Drawing.Size(103, 28);
- this.label5.TabIndex = 11;
- this.label5.Text = "URL API";
- //
- // txtApiUrl
- //
- this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
- this.txtApiUrl.Location = new System.Drawing.Point(132, 225);
- this.txtApiUrl.Name = "txtApiUrl";
- this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
- this.txtApiUrl.TabIndex = 12;
- //
// HF_Registration
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(436, 360);
+ this.ClientSize = new System.Drawing.Size(436, 420);
+ this.Controls.Add(this.btnBrowseExportPath);
+ this.Controls.Add(this.txtInvoiceExportPath);
+ this.Controls.Add(this.labelExportPath);
this.Controls.Add(this.txtApiUrl);
this.Controls.Add(this.label5);
this.Controls.Add(this.panel1);
@@ -192,5 +226,8 @@
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label5;
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
+ private System.Windows.Forms.Label labelExportPath;
+ private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath;
+ private Elfisa.UI.Controls.ModernButton btnBrowseExportPath;
}
}
diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
index 8069c1f..74cc942 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs
@@ -25,6 +25,27 @@ namespace Электронная_Фармация.HelpForms
Close();
}
+ private void btnBrowseExportPath_Click(object sender, EventArgs e)
+ {
+ using (var dialog = new FolderBrowserDialog())
+ {
+ dialog.Description = "Выберите папку для экспорта накладных";
+ dialog.ShowNewFolderButton = true;
+ if (!string.IsNullOrWhiteSpace(txtInvoiceExportPath.Text)
+ && System.IO.Directory.Exists(txtInvoiceExportPath.Text.Trim()))
+ {
+ dialog.SelectedPath = txtInvoiceExportPath.Text.Trim();
+ }
+
+ if (dialog.ShowDialog(this) == DialogResult.OK)
+ {
+ txtInvoiceExportPath.Text = dialog.SelectedPath;
+ AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
+ ToastNotification.ShowSuccess("Папка экспорта накладных сохранена");
+ }
+ }
+ }
+
private async void btnConfirmRegistration_Click(object sender, EventArgs e)
{
btnConfirmRegistration.Enabled = false;
@@ -39,6 +60,7 @@ namespace Электронная_Фармация.HelpForms
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
string apiUrl = txtApiUrl.Text.Trim();
Settings.Default.ApiBaseUrl = apiUrl;
+ AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
Settings.Default.Save();
LoginRequest loginRequest = new LoginRequest();
@@ -56,6 +78,7 @@ namespace Электронная_Фармация.HelpForms
Settings.Default.stringPassword = strPassword;
Settings.Default.stringToken = token;
Settings.Default.ApiBaseUrl = client.BaseUrl;
+ AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
Settings.Default.Save();
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
@@ -104,6 +127,7 @@ namespace Электронная_Фармация.HelpForms
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
? AppConfig.DefaultApiBaseUrl
: Settings.Default.ApiBaseUrl;
+ txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath;
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
}
diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
index c582df8..61c34aa 100644
--- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs
+++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
@@ -119,5 +119,17 @@ namespace Электронная_Фармация.Properties {
}
}
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("")]
+ public string InvoiceExportPath {
+ get {
+ return ((string)(this["InvoiceExportPath"]));
+ }
+ set {
+ this["InvoiceExportPath"] = value;
+ }
+ }
+
}
}
diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings
index b2c624d..b4863ca 100644
--- a/src/ElectronicPharmacy/Properties/Settings.settings
+++ b/src/ElectronicPharmacy/Properties/Settings.settings
@@ -26,5 +26,8 @@
False
+
+
+
\ No newline at end of file
diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
index 05fb61a..ba51fdc 100644
--- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs
+++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs
@@ -406,7 +406,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
return;
}
- _exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт");
+ _exportInvoiceMenuItem = new ToolStripMenuItem("Экспортировать в папку");
_exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
_invoiceMenu = new ContextMenuStrip();
@@ -547,11 +547,27 @@ where ii.idInvoice = @invoiceId;";
return;
}
- var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty;
- ToastNotification.ShowCustom(
- $"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.",
- Color.SteelBlue,
- Color.White);
+ var row = dgvInvoice.SelectedRows[0];
+ var invoiceId = row.Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
+ var invoiceNumber = row.Cells["Номер"]?.Value?.ToString() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(invoiceId))
+ {
+ ToastNotification.ShowCustom("Не удалось определить накладную для экспорта.", Color.DarkOrange, Color.White);
+ return;
+ }
+
+ try
+ {
+ var exportService = new InvoiceExportService();
+ var filePath = exportService.ExportInvoice(invoiceId, FindForm());
+ ToastNotification.ShowSuccess($"Накладная {invoiceNumber} сохранена:\n{filePath}");
+ }
+ catch (Exception ex)
+ {
+ AppDebugLog.Error("Invoices", $"Ошибка экспорта накладной {invoiceNumber}", ex);
+ ToastNotification.ShowError($"Не удалось экспортировать накладную: {ex.Message}");
+ }
}
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)