Прогресс загрузки прайса 0–100% и экспорт накладных (#1)
## Summary - Реальный прогресс загрузки прайса 0–100%: по байтам при скачивании, затем по позициям при обработке и сохранении в БД - Настройка папки экспорта накладных и CSV-экспорт из контекстного меню ## Test plan - [ ] Запустить обновление прайса и убедиться, что полоска идёт от 0 до 100 с текстом «сколько / сколько» - [ ] В авторизации задать папку экспорта накладных - [ ] Экспортировать накладную в CSV через контекстное меню
This commit is contained in:
commit
999cfe4121
@ -36,6 +36,9 @@
|
|||||||
<setting name="stringToken" serializeAs="String">
|
<setting name="stringToken" serializeAs="String">
|
||||||
<value />
|
<value />
|
||||||
</setting>
|
</setting>
|
||||||
|
<setting name="InvoiceExportPath" serializeAs="String">
|
||||||
|
<value />
|
||||||
|
</setting>
|
||||||
</Электронная_Фармация.Properties.Settings>
|
</Электронная_Фармация.Properties.Settings>
|
||||||
</userSettings>
|
</userSettings>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -98,7 +98,13 @@ namespace Электронная_Фармация.Classes
|
|||||||
|
|
||||||
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
|
public bool IsAuthenticated => !string.IsNullOrEmpty(_token);
|
||||||
|
|
||||||
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string supplierId = null, string regionId = null, string q = null, int? limit = null, int? offset = null)
|
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(
|
||||||
|
string supplierId = null,
|
||||||
|
string regionId = null,
|
||||||
|
string q = null,
|
||||||
|
int? limit = null,
|
||||||
|
int? offset = null,
|
||||||
|
IProgress<HttpTransferProgress> transferProgress = null)
|
||||||
{
|
{
|
||||||
EnsureAuthenticated();
|
EnsureAuthenticated();
|
||||||
|
|
||||||
@ -130,7 +136,10 @@ namespace Электронная_Фармация.Classes
|
|||||||
path += "?" + string.Join("&", queryParams);
|
path += "?" + string.Join("&", queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса");
|
var responseBody = transferProgress == null
|
||||||
|
? await SendAuthorizedGetAsync(path, "загрузку прайса")
|
||||||
|
: await SendAuthorizedGetWithProgressAsync(path, "загрузку прайса", transferProgress);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions);
|
var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions);
|
||||||
@ -174,6 +183,14 @@ namespace Электронная_Фармация.Classes
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> SendAuthorizedGetAsync(string path, string operationName)
|
private async Task<string> SendAuthorizedGetAsync(string path, string operationName)
|
||||||
|
{
|
||||||
|
return await SendAuthorizedGetWithProgressAsync(path, operationName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> SendAuthorizedGetWithProgressAsync(
|
||||||
|
string path,
|
||||||
|
string operationName,
|
||||||
|
IProgress<HttpTransferProgress> transferProgress)
|
||||||
{
|
{
|
||||||
var fullUrl = BuildFullUrl(path);
|
var fullUrl = BuildFullUrl(path);
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
@ -185,8 +202,8 @@ namespace Электронная_Фармация.Classes
|
|||||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
|
using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl))
|
||||||
{
|
{
|
||||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||||
var response = await _httpClient.SendAsync(httpRequest);
|
var response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead);
|
||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
var responseBody = await ReadContentWithProgressAsync(response, transferProgress);
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
|
AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody);
|
||||||
@ -243,6 +260,29 @@ namespace Электронная_Фармация.Classes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadContentWithProgressAsync(
|
||||||
|
HttpResponseMessage response,
|
||||||
|
IProgress<HttpTransferProgress> 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)
|
private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
|
||||||
{
|
{
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
@ -333,4 +373,29 @@ namespace Электронная_Фармация.Classes
|
|||||||
public string InvoiceSum { get; set; }
|
public string InvoiceSum { get; set; }
|
||||||
public string RefuseSum { 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,5 +108,16 @@ namespace Электронная_Фармация.Classes
|
|||||||
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
|
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
|
||||||
Settings.Default.Save();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
194
src/ElectronicPharmacy/Classes/InvoiceExportService.cs
Normal file
194
src/ElectronicPharmacy/Classes/InvoiceExportService.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exports a local invoice (header + items) to a CSV file in the configured folder.
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -106,6 +106,7 @@
|
|||||||
<Compile Include="Classes\AppDebugLog.cs" />
|
<Compile Include="Classes\AppDebugLog.cs" />
|
||||||
<Compile Include="Classes\DataSender.cs" />
|
<Compile Include="Classes\DataSender.cs" />
|
||||||
<Compile Include="Classes\ErrorResponse.cs" />
|
<Compile Include="Classes\ErrorResponse.cs" />
|
||||||
|
<Compile Include="Classes\InvoiceExportService.cs" />
|
||||||
<Compile Include="Classes\InvoiceRequestService.cs" />
|
<Compile Include="Classes\InvoiceRequestService.cs" />
|
||||||
<Compile Include="Classes\InvoiceSyncService.cs" />
|
<Compile Include="Classes\InvoiceSyncService.cs" />
|
||||||
<Compile Include="Classes\LoginRequest.cs" />
|
<Compile Include="Classes\LoginRequest.cs" />
|
||||||
|
|||||||
@ -32,17 +32,49 @@
|
|||||||
this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView();
|
this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView();
|
||||||
this.DataSetPriceList = new System.Data.DataSet();
|
this.DataSetPriceList = new System.Data.DataSet();
|
||||||
this.btnCloseThis = new Elfisa.UI.Controls.ModernButton();
|
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.dataGridView1)).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit();
|
||||||
this.SuspendLayout();
|
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
|
// rtxtDebug
|
||||||
//
|
//
|
||||||
this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control;
|
this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control;
|
||||||
this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
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.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.TabIndex = 0;
|
||||||
this.rtxtDebug.Text = "";
|
this.rtxtDebug.Text = "";
|
||||||
//
|
//
|
||||||
@ -79,6 +111,9 @@
|
|||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.BackColor = System.Drawing.SystemColors.Control;
|
this.BackColor = System.Drawing.SystemColors.Control;
|
||||||
this.ClientSize = new System.Drawing.Size(684, 396);
|
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.btnCloseThis);
|
||||||
this.Controls.Add(this.dataGridView1);
|
this.Controls.Add(this.dataGridView1);
|
||||||
this.Controls.Add(this.rtxtDebug);
|
this.Controls.Add(this.rtxtDebug);
|
||||||
@ -93,7 +128,7 @@
|
|||||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit();
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
|
this.PerformLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -102,5 +137,8 @@
|
|||||||
private Elfisa.UI.Controls.ModernDataGridView dataGridView1;
|
private Elfisa.UI.Controls.ModernDataGridView dataGridView1;
|
||||||
private System.Data.DataSet DataSetPriceList;
|
private System.Data.DataSet DataSetPriceList;
|
||||||
private Elfisa.UI.Controls.ModernButton btnCloseThis;
|
private Elfisa.UI.Controls.ModernButton btnCloseThis;
|
||||||
|
private System.Windows.Forms.Label lblTitle;
|
||||||
|
private System.Windows.Forms.Label lblProgress;
|
||||||
|
private System.Windows.Forms.ProgressBar progressDownload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,11 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
{
|
{
|
||||||
public partial class HF_DownloadDataFromServer : Form
|
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()
|
public HF_DownloadDataFromServer()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -20,6 +25,7 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
|
private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
|
||||||
private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
|
private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
|
||||||
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
|
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
|
||||||
|
private bool _isDownloading;
|
||||||
|
|
||||||
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
|
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
@ -28,6 +34,8 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
dataGridView1.Visible = false;
|
dataGridView1.Visible = false;
|
||||||
rtxtDebug.Visible = true;
|
rtxtDebug.Visible = true;
|
||||||
rtxtDebug.ReadOnly = true;
|
rtxtDebug.ReadOnly = true;
|
||||||
|
_isDownloading = true;
|
||||||
|
SetOverallProgress(0, "Подключение к серверу...");
|
||||||
|
|
||||||
var client = new ApiClient();
|
var client = new ApiClient();
|
||||||
AppendStatus($"Сервер: {client.BaseUrl}");
|
AppendStatus($"Сервер: {client.BaseUrl}");
|
||||||
@ -36,39 +44,67 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
SetOverallProgress(1, "Авторизация...");
|
||||||
await EnsureAuthenticatedAsync(client);
|
await EnsureAuthenticatedAsync(client);
|
||||||
|
|
||||||
AppendStatus("Загружаю сводный прайс (постранично, весь)...");
|
SetOverallProgress(3, "Загрузка прайса с сервера...");
|
||||||
|
AppendStatus("Загружаю сводный прайс...");
|
||||||
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
|
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);
|
SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы...");
|
||||||
SavePriceTable(tablePrice);
|
var buildProgress = new Progress<CountProgress>(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<CountProgress>(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;
|
DialogResult = DialogResult.OK;
|
||||||
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
|
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
|
||||||
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
|
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка авторизации");
|
||||||
AppendStatus($"Ошибка авторизации: {ex.Message}");
|
AppendStatus($"Ошибка авторизации: {ex.Message}");
|
||||||
AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
|
AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
|
||||||
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка сети");
|
||||||
AppendStatus($"Ошибка сети: {ex.Message}");
|
AppendStatus($"Ошибка сети: {ex.Message}");
|
||||||
AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
|
AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
|
||||||
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка загрузки");
|
||||||
AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
|
AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
|
||||||
AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
|
AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
|
||||||
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
|
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
btnCloseThis.Enabled = true;
|
{
|
||||||
|
_isDownloading = false;
|
||||||
|
btnCloseThis.Enabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureAuthenticatedAsync(ApiClient client)
|
private async Task EnsureAuthenticatedAsync(ApiClient client)
|
||||||
@ -98,39 +134,51 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
AppendStatus("Авторизация успешно пройдена");
|
AppendStatus("Авторизация успешно пройдена");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Тянем весь прайс постранично: сервер отдаёт максимум 5000 за запрос,
|
|
||||||
// поэтому идём offset'ом, пока страница не окажется неполной.
|
|
||||||
private async Task<PriceSummaryResponse> GetFullPriceSummaryPagedAsync(ApiClient client)
|
private async Task<PriceSummaryResponse> GetFullPriceSummaryPagedAsync(ApiClient client)
|
||||||
{
|
{
|
||||||
var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
|
var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
|
||||||
? null
|
? null
|
||||||
: Settings.Default.RegionId;
|
: Settings.Default.RegionId;
|
||||||
|
|
||||||
// Тянем весь прайс одним запросом: постраничный OFFSET на тяжёлом
|
// Один крупный запрос (OFFSET на больших страницах валит сервер).
|
||||||
// сводном запросе валит сервер на глубоких страницах. Один запрос =
|
|
||||||
// один расчёт выборки. Страховка-цикл остаётся на случай, если позиций
|
|
||||||
// окажется больше pageSize.
|
|
||||||
const int pageSize = 200000;
|
const int pageSize = 200000;
|
||||||
const int maxPages = 100;
|
const int maxPages = 100;
|
||||||
|
|
||||||
var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() };
|
var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() };
|
||||||
|
var transferProgress = new Progress<HttpTransferProgress>(ReportTransferProgress);
|
||||||
|
|
||||||
for (int page = 0; page < maxPages; page++)
|
for (int page = 0; page < maxPages; page++)
|
||||||
{
|
{
|
||||||
int offset = page * pageSize;
|
int offset = page * pageSize;
|
||||||
|
SetOverallProgress(
|
||||||
|
Math.Min(DownloadPhaseEnd - 1, 3 + page),
|
||||||
|
page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}...");
|
||||||
|
|
||||||
PriceSummaryResponse chunk;
|
PriceSummaryResponse chunk;
|
||||||
try
|
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)
|
catch (UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
AppendStatus("Токен недействителен, повторная авторизация...");
|
AppendStatus("Токен недействителен, повторная авторизация...");
|
||||||
|
SetOverallProgress(2, "Повторная авторизация...");
|
||||||
Settings.Default.stringToken = string.Empty;
|
Settings.Default.stringToken = string.Empty;
|
||||||
Settings.Default.Save();
|
Settings.Default.Save();
|
||||||
client.SetToken(null);
|
client.SetToken(null);
|
||||||
await LoginAndSaveTokenAsync(client);
|
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;
|
int got = chunk?.Summary?.Count ?? 0;
|
||||||
@ -142,14 +190,33 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
|
|
||||||
if (got < pageSize)
|
if (got < pageSize)
|
||||||
{
|
{
|
||||||
break; // последняя (неполная) страница — дальше данных нет
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return combined;
|
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<CountProgress> progress = null)
|
||||||
{
|
{
|
||||||
var tablePrice = new DataTable();
|
var tablePrice = new DataTable();
|
||||||
tablePrice.Columns.Add("supplier_price_id");
|
tablePrice.Columns.Add("supplier_price_id");
|
||||||
@ -192,6 +259,8 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
return tablePrice;
|
return tablePrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int total = allSuppliersSummary.Summary.Count;
|
||||||
|
int current = 0;
|
||||||
foreach (var item in allSuppliersSummary.Summary)
|
foreach (var item in allSuppliersSummary.Summary)
|
||||||
{
|
{
|
||||||
var row = tablePrice.NewRow();
|
var row = tablePrice.NewRow();
|
||||||
@ -210,12 +279,18 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim();
|
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim();
|
||||||
row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
|
row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
|
||||||
tablePrice.Rows.Add(row);
|
tablePrice.Rows.Add(row);
|
||||||
|
|
||||||
|
current++;
|
||||||
|
if (progress != null && (current % 500 == 0 || current == total))
|
||||||
|
{
|
||||||
|
progress.Report(new CountProgress(current, total));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tablePrice;
|
return tablePrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SavePriceTable(DataTable tablePrice)
|
private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null)
|
||||||
{
|
{
|
||||||
const string commandToDelete = "delete from [PriceList]";
|
const string commandToDelete = "delete from [PriceList]";
|
||||||
const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
|
const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
|
||||||
@ -253,6 +328,7 @@ VALUES
|
|||||||
@Dosage
|
@Dosage
|
||||||
)";
|
)";
|
||||||
|
|
||||||
|
int total = tablePrice.Rows.Count;
|
||||||
using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb))
|
using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb))
|
||||||
{
|
{
|
||||||
sqliteCon.Open();
|
sqliteCon.Open();
|
||||||
@ -266,6 +342,7 @@ VALUES
|
|||||||
|
|
||||||
using (var transaction = sqliteCon.BeginTransaction())
|
using (var transaction = sqliteCon.BeginTransaction())
|
||||||
{
|
{
|
||||||
|
int saved = 0;
|
||||||
foreach (DataRow row in tablePrice.Rows)
|
foreach (DataRow row in tablePrice.Rows)
|
||||||
{
|
{
|
||||||
using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
|
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.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value);
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saved++;
|
||||||
|
if (progress != null && (saved % 250 == 0 || saved == total))
|
||||||
|
{
|
||||||
|
progress.Report(new CountProgress(saved, total));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.Commit();
|
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<int, string>(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)
|
private void AppendStatus(string message)
|
||||||
{
|
{
|
||||||
if (InvokeRequired)
|
if (InvokeRequired)
|
||||||
@ -310,13 +455,27 @@ VALUES
|
|||||||
|
|
||||||
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
|
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
Dispose();
|
if (_isDownloading)
|
||||||
Close();
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnCloseThis_Click(object sender, EventArgs e)
|
private void btnCloseThis_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class CountProgress
|
||||||
|
{
|
||||||
|
public CountProgress(int current, int total)
|
||||||
|
{
|
||||||
|
Current = current;
|
||||||
|
Total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Current { get; }
|
||||||
|
public int Total { get; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,9 @@
|
|||||||
this.label3 = new System.Windows.Forms.Label();
|
this.label3 = new System.Windows.Forms.Label();
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
this.label5 = new System.Windows.Forms.Label();
|
||||||
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
|
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.panel1.SuspendLayout();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
@ -79,9 +82,55 @@
|
|||||||
this.txtPassword.Size = new System.Drawing.Size(375, 34);
|
this.txtPassword.Size = new System.Drawing.Size(375, 34);
|
||||||
this.txtPassword.TabIndex = 3;
|
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
|
// 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.Name = "btnConfirmRegistration";
|
||||||
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
|
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
|
||||||
this.btnConfirmRegistration.TabIndex = 5;
|
this.btnConfirmRegistration.TabIndex = 5;
|
||||||
@ -90,7 +139,7 @@
|
|||||||
//
|
//
|
||||||
// btnCloseThis
|
// 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.Name = "btnCloseThis";
|
||||||
this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
|
this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
|
||||||
this.btnCloseThis.TabIndex = 6;
|
this.btnCloseThis.TabIndex = 6;
|
||||||
@ -102,7 +151,7 @@
|
|||||||
this.checkTokenGained.AutoCheck = false;
|
this.checkTokenGained.AutoCheck = false;
|
||||||
this.checkTokenGained.AutoSize = true;
|
this.checkTokenGained.AutoSize = true;
|
||||||
this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F);
|
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.Name = "checkTokenGained";
|
||||||
this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
|
this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
|
||||||
this.checkTokenGained.TabIndex = 7;
|
this.checkTokenGained.TabIndex = 7;
|
||||||
@ -133,29 +182,14 @@
|
|||||||
this.label3.Text = "АВТОРИЗАЦИЯ";
|
this.label3.Text = "АВТОРИЗАЦИЯ";
|
||||||
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
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
|
// HF_Registration
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
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.txtApiUrl);
|
||||||
this.Controls.Add(this.label5);
|
this.Controls.Add(this.label5);
|
||||||
this.Controls.Add(this.panel1);
|
this.Controls.Add(this.panel1);
|
||||||
@ -192,5 +226,8 @@
|
|||||||
private System.Windows.Forms.Label label3;
|
private System.Windows.Forms.Label label3;
|
||||||
private System.Windows.Forms.Label label5;
|
private System.Windows.Forms.Label label5;
|
||||||
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
|
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
|
||||||
|
private System.Windows.Forms.Label labelExportPath;
|
||||||
|
private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath;
|
||||||
|
private Elfisa.UI.Controls.ModernButton btnBrowseExportPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,27 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
Close();
|
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)
|
private async void btnConfirmRegistration_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
btnConfirmRegistration.Enabled = false;
|
btnConfirmRegistration.Enabled = false;
|
||||||
@ -39,6 +60,7 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
|
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
|
||||||
string apiUrl = txtApiUrl.Text.Trim();
|
string apiUrl = txtApiUrl.Text.Trim();
|
||||||
Settings.Default.ApiBaseUrl = apiUrl;
|
Settings.Default.ApiBaseUrl = apiUrl;
|
||||||
|
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
|
||||||
Settings.Default.Save();
|
Settings.Default.Save();
|
||||||
|
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
@ -56,6 +78,7 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
Settings.Default.stringPassword = strPassword;
|
Settings.Default.stringPassword = strPassword;
|
||||||
Settings.Default.stringToken = token;
|
Settings.Default.stringToken = token;
|
||||||
Settings.Default.ApiBaseUrl = client.BaseUrl;
|
Settings.Default.ApiBaseUrl = client.BaseUrl;
|
||||||
|
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
|
||||||
Settings.Default.Save();
|
Settings.Default.Save();
|
||||||
|
|
||||||
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
|
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
|
||||||
@ -104,6 +127,7 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
||||||
? AppConfig.DefaultApiBaseUrl
|
? AppConfig.DefaultApiBaseUrl
|
||||||
: Settings.Default.ApiBaseUrl;
|
: Settings.Default.ApiBaseUrl;
|
||||||
|
txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath;
|
||||||
|
|
||||||
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
|
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,5 +26,8 @@
|
|||||||
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
|
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
|
||||||
<Value Profile="(Default)">False</Value>
|
<Value Profile="(Default)">False</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
|
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
|
||||||
|
<Value Profile="(Default)" />
|
||||||
|
</Setting>
|
||||||
</Settings>
|
</Settings>
|
||||||
</SettingsFile>
|
</SettingsFile>
|
||||||
@ -406,7 +406,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт");
|
_exportInvoiceMenuItem = new ToolStripMenuItem("Экспортировать в папку");
|
||||||
_exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
|
_exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
|
||||||
|
|
||||||
_invoiceMenu = new ContextMenuStrip();
|
_invoiceMenu = new ContextMenuStrip();
|
||||||
@ -547,11 +547,27 @@ where ii.idInvoice = @invoiceId;";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty;
|
var row = dgvInvoice.SelectedRows[0];
|
||||||
ToastNotification.ShowCustom(
|
var invoiceId = row.Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
|
||||||
$"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.",
|
var invoiceNumber = row.Cells["Номер"]?.Value?.ToString() ?? string.Empty;
|
||||||
Color.SteelBlue,
|
|
||||||
Color.White);
|
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)
|
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user