Improve order UX and harden API debugging.
Prompt for Location ID after login, move cart delete there, refine price-list Backspace navigation, and log every API failure with stack and response body. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
74390c4918
commit
a6601567f2
@ -118,27 +118,45 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
|
||||
var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса");
|
||||
try
|
||||
{
|
||||
var summary = JsonSerializer.Deserialize<PriceSummaryResponse>(responseBody, JsonOptions);
|
||||
var count = summary?.Summary?.Count ?? 0;
|
||||
AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций");
|
||||
return summary;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ прайса", ex);
|
||||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайса: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<InvoiceApiItem>> GetInvoicesAsync()
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
|
||||
var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных");
|
||||
try
|
||||
{
|
||||
var items = JsonSerializer.Deserialize<List<InvoiceApiItem>>(responseBody, JsonOptions) ?? new List<InvoiceApiItem>();
|
||||
AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт.");
|
||||
return items;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ накладных", ex);
|
||||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке накладных: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAuthenticated()
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
{
|
||||
throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
|
||||
var ex = new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных");
|
||||
AppDebugLog.Error("ApiClient", "Запрос без токена авторизации", ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,11 +187,19 @@ namespace Электронная_Фармация.Classes
|
||||
{
|
||||
_token = null;
|
||||
var error = TryDeserializeError(responseBody);
|
||||
throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}");
|
||||
var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}";
|
||||
AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName);
|
||||
var unauthorized = new UnauthorizedAccessException(message);
|
||||
AppDebugLog.Error("ApiClient", message, unauthorized);
|
||||
throw unauthorized;
|
||||
}
|
||||
|
||||
var httpError = TryDeserializeError(responseBody);
|
||||
throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}");
|
||||
var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}";
|
||||
AppDebugLog.ApiHttpError("GET", fullUrl, (int)response.StatusCode, responseBody, operationName);
|
||||
var httpEx = new HttpRequestException(httpMessage);
|
||||
AppDebugLog.Error("ApiClient", httpMessage, httpEx);
|
||||
throw httpEx;
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
|
||||
@ -188,7 +214,15 @@ namespace Электронная_Фармация.Classes
|
||||
AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||||
throw WrapNetworkError(ex, operationName);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is UnauthorizedAccessException) && !(ex is HttpRequestException))
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||||
@ -199,26 +233,48 @@ namespace Электронная_Фармация.Classes
|
||||
private string ParseLoginResponse(HttpResponseMessage response, string responseBody)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var loginResponse = JsonSerializer.Deserialize<LoginResponse>(responseBody, JsonOptions);
|
||||
if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token))
|
||||
{
|
||||
throw new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации");
|
||||
var emptyTokenEx = new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации");
|
||||
AppDebugLog.Error("ApiClient", "Пустой токен в ответе логина", emptyTokenEx);
|
||||
throw emptyTokenEx;
|
||||
}
|
||||
|
||||
_token = loginResponse.Token;
|
||||
AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}");
|
||||
return _token;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ логина", ex);
|
||||
throw new HttpRequestException($"Некорректный ответ сервера при авторизации: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
var error = TryDeserializeError(responseBody);
|
||||
throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}");
|
||||
var message = $"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}";
|
||||
AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login");
|
||||
var unauthorized = new UnauthorizedAccessException(message);
|
||||
AppDebugLog.Error("ApiClient", message, unauthorized);
|
||||
throw unauthorized;
|
||||
}
|
||||
|
||||
var httpError = TryDeserializeError(responseBody);
|
||||
throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}");
|
||||
var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}";
|
||||
AppDebugLog.ApiHttpError("POST", BuildFullUrl("/auth/login"), (int)response.StatusCode, responseBody, "login");
|
||||
var httpEx = new HttpRequestException(httpMessage);
|
||||
AppDebugLog.Error("ApiClient", httpMessage, httpEx);
|
||||
throw httpEx;
|
||||
}
|
||||
|
||||
private string BuildFullUrl(string path)
|
||||
|
||||
@ -102,5 +102,11 @@ namespace Электронная_Фармация.Classes
|
||||
{
|
||||
get { return Settings.Default.LocationId ?? string.Empty; }
|
||||
}
|
||||
|
||||
public static void SetLocationId(string locationId)
|
||||
{
|
||||
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
|
||||
Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ namespace Электронная_Фармация.Classes
|
||||
public static class AppDebugLog
|
||||
{
|
||||
private static readonly object Sync = new object();
|
||||
private const int MaxBodyLength = 4000;
|
||||
private const int MaxBodyLength = 8000;
|
||||
private const string DisableEnvVar = "ELF_API_DEBUG";
|
||||
private const string DisableMarkerFile = "api-debug.off";
|
||||
|
||||
@ -88,10 +88,36 @@ namespace Электронная_Фармация.Classes
|
||||
|
||||
public static void ApiFailure(string method, string url, long elapsedMs, Exception exception)
|
||||
{
|
||||
var message = $"{method} {url} failed after {elapsedMs} ms: {exception.Message}";
|
||||
var message = $"{method} {url} failed after {elapsedMs} ms: {exception?.Message}";
|
||||
Write("FAIL", "Api", message, exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Логирует ошибку HTTP-ответа API (4xx/5xx) с телом ответа.
|
||||
/// </summary>
|
||||
public static void ApiHttpError(
|
||||
string method,
|
||||
string url,
|
||||
int statusCode,
|
||||
string responseBody,
|
||||
string context = null)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.Append(method).Append(" ").Append(url);
|
||||
message.Append(" -> HTTP ").Append(statusCode);
|
||||
if (!string.IsNullOrWhiteSpace(context))
|
||||
{
|
||||
message.Append(" | ").Append(context);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(responseBody))
|
||||
{
|
||||
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
|
||||
}
|
||||
|
||||
Write("HTTP-ERR", "Api", message.ToString(), null);
|
||||
}
|
||||
|
||||
public static string MaskToken(string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
@ -148,6 +174,12 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
|
||||
sb.Append(current.GetType().Name).Append(": ").Append(current.Message);
|
||||
if (!string.IsNullOrWhiteSpace(current.StackTrace) && depth == 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append(current.StackTrace);
|
||||
}
|
||||
|
||||
current = current.InnerException;
|
||||
depth++;
|
||||
}
|
||||
@ -172,7 +204,8 @@ namespace Электронная_Фармация.Classes
|
||||
|
||||
if (exception != null)
|
||||
{
|
||||
line.Append(" | ").Append(FormatException(exception));
|
||||
line.AppendLine();
|
||||
line.Append(FormatException(exception));
|
||||
}
|
||||
|
||||
lock (Sync)
|
||||
|
||||
@ -10,6 +10,8 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Электронная_Фармация.HelpForms;
|
||||
using Электронная_Фармация.Properties;
|
||||
|
||||
namespace Электронная_Фармация.Classes
|
||||
@ -28,20 +30,36 @@ namespace Электронная_Фармация.Classes
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
AppDebugLog.Error("DataSender", "Отправка заказов: токен отсутствует");
|
||||
ToastNotification.ShowError("Не найден токен авторизации.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
||||
{
|
||||
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|
||||
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
||||
{
|
||||
AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан");
|
||||
ToastNotification.ShowCustom(
|
||||
"Не указан location_id. Заполните его в окне авторизации/настроек.",
|
||||
"Не указан location_id. Заполните его после авторизации.",
|
||||
Color.DarkOrange,
|
||||
Color.White);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var orders = FetchDataFromSql(null);
|
||||
List<BuyerOrderRequest> orders;
|
||||
try
|
||||
{
|
||||
orders = FetchDataFromSql(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("DataSender", "Ошибка подготовки заказов к отправке", ex);
|
||||
ToastNotification.ShowError($"Ошибка подготовки заказов: {ex.Message}");
|
||||
return;
|
||||
}
|
||||
if (orders.Count == 0)
|
||||
{
|
||||
ToastNotification.ShowCustom("Нет новых заказов для отправки.", Color.DarkOrange, Color.White);
|
||||
@ -87,20 +105,36 @@ namespace Электронная_Фармация.Classes
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: токен отсутствует");
|
||||
ToastNotification.ShowError("Не найден токен авторизации.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
||||
{
|
||||
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|
||||
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
||||
{
|
||||
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: location_id не указан");
|
||||
ToastNotification.ShowCustom(
|
||||
"Не указан location_id. Заполните его в окне авторизации/настроек.",
|
||||
"Не указан location_id. Заполните его после авторизации.",
|
||||
Color.DarkOrange,
|
||||
Color.White);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var orders = FetchDataFromSql(localOrderId);
|
||||
List<BuyerOrderRequest> orders;
|
||||
try
|
||||
{
|
||||
orders = FetchDataFromSql(localOrderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("DataSender", $"Ошибка подготовки заказа {localOrderId}", ex);
|
||||
ToastNotification.ShowError($"Ошибка подготовки заказа: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
if (orders.Count == 0)
|
||||
{
|
||||
ToastNotification.ShowCustom("Заказ не найден или уже отправлен.", Color.DarkOrange, Color.White);
|
||||
@ -218,7 +252,15 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}");
|
||||
AppDebugLog.ApiHttpError(
|
||||
"POST",
|
||||
fullUrl,
|
||||
(int)response.StatusCode,
|
||||
responseString,
|
||||
$"order={order.LocalOrderId}");
|
||||
AppDebugLog.Error(
|
||||
"DataSender",
|
||||
$"Ошибка отправки заказа {order.LocalOrderId}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
ToastNotification.ShowError(
|
||||
$"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
|
||||
return false;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,13 +16,26 @@ namespace Электронная_Фармация.Classes
|
||||
var token = Settings.Default.stringToken;
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
throw new System.InvalidOperationException("Необходима авторизация перед загрузкой накладных.");
|
||||
var ex = new InvalidOperationException("Необходима авторизация перед загрузкой накладных.");
|
||||
AppDebugLog.Error("InvoiceSync", "Синхронизация накладных без токена", ex);
|
||||
throw ex;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = new ApiClient();
|
||||
client.SetToken(token);
|
||||
AppDebugLog.Info("InvoiceSync", $"Старт синхронизации накладных. Сервер={client.BaseUrl}");
|
||||
var items = await client.GetInvoicesAsync();
|
||||
return SaveInvoices(items);
|
||||
var saved = SaveInvoices(items);
|
||||
AppDebugLog.Info("InvoiceSync", $"Накладные сохранены локально: {saved} шт.");
|
||||
return saved;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("InvoiceSync", "Ошибка синхронизации накладных с API", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static int SaveInvoices(IReadOnlyList<InvoiceApiItem> items)
|
||||
|
||||
@ -175,6 +175,12 @@
|
||||
<Compile Include="HelpForms\HF_DownloadDataFromServer.Designer.cs">
|
||||
<DependentUpon>HF_DownloadDataFromServer.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="HelpForms\HF_LocationId.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="HelpForms\HF_LocationId.Designer.cs">
|
||||
<DependentUpon>HF_LocationId.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="HelpForms\HF_Registration.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
|
||||
@ -500,6 +500,7 @@ namespace Электронная_Фармация
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Form1", "Не удалось отправить данные (меню Обмен)", ex);
|
||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||
//MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
//MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
@ -604,6 +605,7 @@ namespace Электронная_Фармация
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Form1", "Не удалось отправить данные (кнопка ОТПРАВИТЬ)", ex);
|
||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||
}
|
||||
finally
|
||||
|
||||
131
src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs
generated
Normal file
131
src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs
generated
Normal file
@ -0,0 +1,131 @@
|
||||
namespace Электронная_Фармация.HelpForms
|
||||
{
|
||||
partial class HF_LocationId
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.panelHeader = new System.Windows.Forms.Panel();
|
||||
this.lblTitle = new System.Windows.Forms.Label();
|
||||
this.lblHint = new System.Windows.Forms.Label();
|
||||
this.lblLocationId = new System.Windows.Forms.Label();
|
||||
this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox();
|
||||
this.btnCancel = new Elfisa.UI.Controls.ModernButton();
|
||||
this.btnSave = new Elfisa.UI.Controls.ModernButton();
|
||||
this.panelHeader.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// panelHeader
|
||||
//
|
||||
this.panelHeader.Controls.Add(this.lblTitle);
|
||||
this.panelHeader.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.panelHeader.Location = new System.Drawing.Point(0, 0);
|
||||
this.panelHeader.Name = "panelHeader";
|
||||
this.panelHeader.Size = new System.Drawing.Size(460, 56);
|
||||
this.panelHeader.TabIndex = 0;
|
||||
//
|
||||
// lblTitle
|
||||
//
|
||||
this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 13.8F, System.Drawing.FontStyle.Bold);
|
||||
this.lblTitle.ForeColor = System.Drawing.Color.White;
|
||||
this.lblTitle.Location = new System.Drawing.Point(12, 10);
|
||||
this.lblTitle.Name = "lblTitle";
|
||||
this.lblTitle.Size = new System.Drawing.Size(430, 36);
|
||||
this.lblTitle.TabIndex = 0;
|
||||
this.lblTitle.Text = "Location ID";
|
||||
this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||
//
|
||||
// lblHint
|
||||
//
|
||||
this.lblHint.Font = new System.Drawing.Font("Segoe UI", 10F);
|
||||
this.lblHint.Location = new System.Drawing.Point(20, 70);
|
||||
this.lblHint.Name = "lblHint";
|
||||
this.lblHint.Size = new System.Drawing.Size(420, 48);
|
||||
this.lblHint.TabIndex = 1;
|
||||
this.lblHint.Text = "Укажите UUID торговой точки. Он будет использоваться при отправке заказов на сервер.";
|
||||
//
|
||||
// lblLocationId
|
||||
//
|
||||
this.lblLocationId.AutoSize = true;
|
||||
this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.lblLocationId.Location = new System.Drawing.Point(20, 128);
|
||||
this.lblLocationId.Name = "lblLocationId";
|
||||
this.lblLocationId.Size = new System.Drawing.Size(108, 28);
|
||||
this.lblLocationId.TabIndex = 2;
|
||||
this.lblLocationId.Text = "Location ID";
|
||||
//
|
||||
// txtLocationId
|
||||
//
|
||||
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.txtLocationId.Location = new System.Drawing.Point(24, 160);
|
||||
this.txtLocationId.Name = "txtLocationId";
|
||||
this.txtLocationId.Size = new System.Drawing.Size(412, 34);
|
||||
this.txtLocationId.TabIndex = 3;
|
||||
//
|
||||
// btnCancel
|
||||
//
|
||||
this.btnCancel.Location = new System.Drawing.Point(24, 220);
|
||||
this.btnCancel.Name = "btnCancel";
|
||||
this.btnCancel.Size = new System.Drawing.Size(125, 40);
|
||||
this.btnCancel.TabIndex = 4;
|
||||
this.btnCancel.Text = "Отмена";
|
||||
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
|
||||
//
|
||||
// btnSave
|
||||
//
|
||||
this.btnSave.Location = new System.Drawing.Point(221, 220);
|
||||
this.btnSave.Name = "btnSave";
|
||||
this.btnSave.Size = new System.Drawing.Size(215, 40);
|
||||
this.btnSave.TabIndex = 5;
|
||||
this.btnSave.Text = "Сохранить";
|
||||
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
|
||||
//
|
||||
// HF_LocationId
|
||||
//
|
||||
this.AcceptButton = this.btnSave;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.CancelButton = this.btnCancel;
|
||||
this.ClientSize = new System.Drawing.Size(460, 280);
|
||||
this.Controls.Add(this.btnSave);
|
||||
this.Controls.Add(this.btnCancel);
|
||||
this.Controls.Add(this.txtLocationId);
|
||||
this.Controls.Add(this.lblLocationId);
|
||||
this.Controls.Add(this.lblHint);
|
||||
this.Controls.Add(this.panelHeader);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "HF_LocationId";
|
||||
this.ShowIcon = false;
|
||||
this.ShowInTaskbar = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Load += new System.EventHandler(this.HF_LocationId_Load);
|
||||
this.panelHeader.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel panelHeader;
|
||||
private System.Windows.Forms.Label lblTitle;
|
||||
private System.Windows.Forms.Label lblHint;
|
||||
private System.Windows.Forms.Label lblLocationId;
|
||||
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
|
||||
private Elfisa.UI.Controls.ModernButton btnCancel;
|
||||
private Elfisa.UI.Controls.ModernButton btnSave;
|
||||
}
|
||||
}
|
||||
62
src/ElectronicPharmacy/HelpForms/HF_LocationId.cs
Normal file
62
src/ElectronicPharmacy/HelpForms/HF_LocationId.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
using Elfisa.UI.Helpers;
|
||||
using Elfisa.UI.Theming;
|
||||
using Электронная_Фармация.Classes;
|
||||
using Электронная_Фармация.Properties;
|
||||
|
||||
namespace Электронная_Фармация.HelpForms
|
||||
{
|
||||
public partial class HF_LocationId : Form
|
||||
{
|
||||
public HF_LocationId()
|
||||
{
|
||||
InitializeComponent();
|
||||
Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Показывает диалог Location ID. Возвращает true, если значение сохранено.
|
||||
/// </summary>
|
||||
public static bool PromptAndSave(IWin32Window owner)
|
||||
{
|
||||
using (var form = new HF_LocationId())
|
||||
{
|
||||
UiThemeHelper.ApplyToControlTree(form);
|
||||
return form.ShowDialog(owner) == DialogResult.OK;
|
||||
}
|
||||
}
|
||||
|
||||
private void HF_LocationId_Load(object sender, EventArgs e)
|
||||
{
|
||||
UiThemeHelper.ApplyToControlTree(this);
|
||||
WindowChromeHelper.ApplyDialogChrome(this, panelHeader, lblTitle);
|
||||
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
|
||||
txtLocationId.SelectAll();
|
||||
txtLocationId.Focus();
|
||||
}
|
||||
|
||||
private void btnSave_Click(object sender, EventArgs e)
|
||||
{
|
||||
var locationId = (txtLocationId.Text ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(locationId))
|
||||
{
|
||||
UiDialogs.ShowError("Укажите Location ID торговой точки.", "Location ID", this);
|
||||
txtLocationId.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
AppConfig.SetLocationId(locationId);
|
||||
AppDebugLog.Info("Auth", $"LocationId сохранён: {locationId}");
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void btnCancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,8 +38,6 @@
|
||||
this.checkTokenGained = new System.Windows.Forms.CheckBox();
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.label3 = new System.Windows.Forms.Label();
|
||||
this.label4 = new System.Windows.Forms.Label();
|
||||
this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox();
|
||||
this.label5 = new System.Windows.Forms.Label();
|
||||
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
|
||||
this.panel1.SuspendLayout();
|
||||
@ -83,16 +81,16 @@
|
||||
//
|
||||
// btnConfirmRegistration
|
||||
//
|
||||
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 401);
|
||||
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 300);
|
||||
this.btnConfirmRegistration.Name = "btnConfirmRegistration";
|
||||
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
|
||||
this.btnConfirmRegistration.TabIndex = 5;
|
||||
this.btnConfirmRegistration.Text = "Зарегистрировать";
|
||||
this.btnConfirmRegistration.Text = "Войти";
|
||||
this.btnConfirmRegistration.Click += new System.EventHandler(this.btnConfirmRegistration_Click);
|
||||
//
|
||||
// btnCloseThis
|
||||
//
|
||||
this.btnCloseThis.Location = new System.Drawing.Point(28, 401);
|
||||
this.btnCloseThis.Location = new System.Drawing.Point(28, 300);
|
||||
this.btnCloseThis.Name = "btnCloseThis";
|
||||
this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
|
||||
this.btnCloseThis.TabIndex = 6;
|
||||
@ -104,7 +102,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, 342);
|
||||
this.checkTokenGained.Location = new System.Drawing.Point(28, 260);
|
||||
this.checkTokenGained.Name = "checkTokenGained";
|
||||
this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
|
||||
this.checkTokenGained.TabIndex = 7;
|
||||
@ -135,29 +133,11 @@
|
||||
this.label3.Text = "АВТОРИЗАЦИЯ";
|
||||
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// label4
|
||||
//
|
||||
this.label4.AutoSize = true;
|
||||
this.label4.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.label4.Location = new System.Drawing.Point(23, 225);
|
||||
this.label4.Name = "label4";
|
||||
this.label4.Size = new System.Drawing.Size(108, 28);
|
||||
this.label4.TabIndex = 9;
|
||||
this.label4.Text = "Location ID";
|
||||
//
|
||||
// txtLocationId
|
||||
//
|
||||
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.txtLocationId.Location = new System.Drawing.Point(28, 256);
|
||||
this.txtLocationId.Name = "txtLocationId";
|
||||
this.txtLocationId.Size = new System.Drawing.Size(375, 34);
|
||||
this.txtLocationId.TabIndex = 10;
|
||||
//
|
||||
// label5
|
||||
//
|
||||
this.label5.AutoSize = true;
|
||||
this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.label5.Location = new System.Drawing.Point(23, 296);
|
||||
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;
|
||||
@ -166,7 +146,7 @@
|
||||
// txtApiUrl
|
||||
//
|
||||
this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||
this.txtApiUrl.Location = new System.Drawing.Point(132, 296);
|
||||
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;
|
||||
@ -175,11 +155,9 @@
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(436, 456);
|
||||
this.ClientSize = new System.Drawing.Size(436, 360);
|
||||
this.Controls.Add(this.txtApiUrl);
|
||||
this.Controls.Add(this.label5);
|
||||
this.Controls.Add(this.txtLocationId);
|
||||
this.Controls.Add(this.label4);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Controls.Add(this.checkTokenGained);
|
||||
this.Controls.Add(this.btnCloseThis);
|
||||
@ -199,7 +177,6 @@
|
||||
this.panel1.ResumeLayout(false);
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -213,8 +190,6 @@
|
||||
private System.Windows.Forms.CheckBox checkTokenGained;
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Label label3;
|
||||
private System.Windows.Forms.Label label4;
|
||||
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
|
||||
private System.Windows.Forms.Label label5;
|
||||
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
|
||||
}
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Электронная_Фармация.Properties;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Электронная_Фармация.Classes;
|
||||
using System.Net.Http;
|
||||
using System.Windows.Forms;
|
||||
using Elfisa.UI.Helpers;
|
||||
using Elfisa.UI.Theming;
|
||||
using Электронная_Фармация.Classes;
|
||||
using Электронная_Фармация.Properties;
|
||||
|
||||
namespace Электронная_Фармация.HelpForms
|
||||
{
|
||||
@ -25,21 +16,20 @@ namespace Электронная_Фармация.HelpForms
|
||||
Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
|
||||
}
|
||||
|
||||
string login= Settings.Default.stringLogin.ToString();
|
||||
string password= Settings.Default.stringPassword.ToString();
|
||||
string doWeHaveAToken= Settings.Default.stringToken.ToString();
|
||||
|
||||
string login = Settings.Default.stringLogin.ToString();
|
||||
string password = Settings.Default.stringPassword.ToString();
|
||||
string doWeHaveAToken = Settings.Default.stringToken.ToString();
|
||||
|
||||
private void btnCloseThis_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void btnConfirmRegistration_Click(object sender, EventArgs e)
|
||||
{
|
||||
btnConfirmRegistration.Enabled = false;
|
||||
btnCloseThis.Enabled = false;
|
||||
this.UseWaitCursor = true;
|
||||
UseWaitCursor = true;
|
||||
string strLogin = txtLogin.Text;
|
||||
string strPassword = txtPassword.Text;
|
||||
|
||||
@ -49,7 +39,6 @@ namespace Электронная_Фармация.HelpForms
|
||||
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
|
||||
string apiUrl = txtApiUrl.Text.Trim();
|
||||
Settings.Default.ApiBaseUrl = apiUrl;
|
||||
Settings.Default.LocationId = txtLocationId.Text.Trim();
|
||||
Settings.Default.Save();
|
||||
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
@ -66,18 +55,24 @@ namespace Электронная_Фармация.HelpForms
|
||||
Settings.Default.stringLogin = strLogin;
|
||||
Settings.Default.stringPassword = strPassword;
|
||||
Settings.Default.stringToken = token;
|
||||
Settings.Default.LocationId = txtLocationId.Text.Trim();
|
||||
Settings.Default.ApiBaseUrl = client.BaseUrl;
|
||||
Settings.Default.Save();
|
||||
|
||||
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
|
||||
ToastNotification.ShowSuccess("Авторизация успешно выполнена");
|
||||
//MessageBox.Show("Данные были сохранены", "Регистрация программы успешно выполнена", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
|
||||
// Токен автоматически сохраняется в клиенте
|
||||
// и будет использоваться для всех последующих запросов
|
||||
// После входа обязательно запрашиваем Location ID для отправки заказов.
|
||||
if (!HF_LocationId.PromptAndSave(this))
|
||||
{
|
||||
ToastNotification.ShowCustom(
|
||||
"Location ID не указан. Отправка заказов будет недоступна, пока его не заполните.",
|
||||
System.Drawing.Color.DarkOrange,
|
||||
System.Drawing.Color.White);
|
||||
}
|
||||
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
@ -94,10 +89,10 @@ namespace Электронная_Фармация.HelpForms
|
||||
{
|
||||
UiDialogs.ShowError("Есть незаполненные данные. Регистрация программы не пройдена", "Ошибка ввода", this);
|
||||
}
|
||||
|
||||
btnConfirmRegistration.Enabled = true;
|
||||
btnCloseThis.Enabled = true;
|
||||
this.UseWaitCursor = false;
|
||||
|
||||
UseWaitCursor = false;
|
||||
}
|
||||
|
||||
private void HF_Registration_Load(object sender, EventArgs e)
|
||||
@ -106,19 +101,11 @@ namespace Электронная_Фармация.HelpForms
|
||||
WindowChromeHelper.ApplyDialogChrome(this, panel1, label3);
|
||||
txtLogin.Text = login;
|
||||
txtPassword.Text = password;
|
||||
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
|
||||
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
||||
? AppConfig.DefaultApiBaseUrl
|
||||
: Settings.Default.ApiBaseUrl;
|
||||
|
||||
if (doWeHaveAToken.Length > 0)
|
||||
{
|
||||
checkTokenGained.Checked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkTokenGained.Checked = false;
|
||||
}
|
||||
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,9 +39,8 @@ namespace Электронная_Фармация.HelpForms
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Upload", "Не удалось отправить данные на сервер", ex);
|
||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||
//MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
//MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,8 +355,17 @@ namespace Электронная_Фармация.UserControls
|
||||
}
|
||||
|
||||
var orderId = DGVOrders.SelectedRows[0].Cells["ИД заказа"].Value.ToString();
|
||||
try
|
||||
{
|
||||
var sender = new DataSender();
|
||||
await sender.SendSingleOrderAsync(orderId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Orders", $"Не удалось отправить заказ {orderId}", ex);
|
||||
ToastNotification.ShowError($"Не удалось отправить заказ: {ex.Message}");
|
||||
}
|
||||
|
||||
ApplyOrderFilters();
|
||||
}
|
||||
|
||||
|
||||
@ -237,8 +237,7 @@
|
||||
//
|
||||
// UCInvoices
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.Controls.Add(this.btnClearConsignees);
|
||||
this.Controls.Add(this.btnClearSuppliers);
|
||||
this.Controls.Add(this.PContentInvoice);
|
||||
|
||||
@ -18,6 +18,8 @@ namespace Электронная_Фармация.UserControls
|
||||
private string _initialSourceOrderId;
|
||||
private ContextMenuStrip _invoiceMenu;
|
||||
private ToolStripMenuItem _exportInvoiceMenuItem;
|
||||
private Panel panelToolbar;
|
||||
private bool _toolbarReady;
|
||||
|
||||
public UCInvoices()
|
||||
: this(null)
|
||||
@ -210,18 +212,170 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
|
||||
UiThemeHelper.ApplyToControlTree(this);
|
||||
Font = new Font("Segoe UI", 9.75f, FontStyle.Regular);
|
||||
InitializeInvoiceContextMenu();
|
||||
EnsureInvoiceToolbar();
|
||||
|
||||
button1.Visible = true;
|
||||
button1.Text = "Обновить с сервера";
|
||||
button1.Width = 180;
|
||||
ModernButtonStyles.ApplyAuto(button1);
|
||||
button1.Click += async (_, __) => await SyncFromServerAsync();
|
||||
button1.Click -= Button1_SyncClick;
|
||||
button1.Click += Button1_SyncClick;
|
||||
|
||||
ModernButtonStyles.ApplyClear(btnClearSuppliers);
|
||||
ModernButtonStyles.ApplyClear(btnClearConsignees);
|
||||
btnClearSuppliers.Text = "×";
|
||||
btnClearConsignees.Text = "×";
|
||||
|
||||
ApplyToolbarLayout();
|
||||
|
||||
loadConsignees();
|
||||
loadSuppliers();
|
||||
getDatesFromCalendar();
|
||||
}
|
||||
|
||||
private async void Button1_SyncClick(object sender, EventArgs e)
|
||||
{
|
||||
await SyncFromServerAsync();
|
||||
}
|
||||
|
||||
private void EnsureInvoiceToolbar()
|
||||
{
|
||||
if (_toolbarReady)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
panelToolbar = new Panel
|
||||
{
|
||||
Name = "panelToolbar",
|
||||
Dock = DockStyle.Top,
|
||||
Height = 96,
|
||||
Padding = new Padding(12, 10, 12, 14),
|
||||
Tag = "toolbar"
|
||||
};
|
||||
|
||||
var toolbarControls = new Control[]
|
||||
{
|
||||
button1,
|
||||
dtpDateFrom,
|
||||
label1,
|
||||
dtpDateTo,
|
||||
txtInvoiceNumber,
|
||||
comboSuppliers,
|
||||
btnClearSuppliers,
|
||||
comboConsignees,
|
||||
btnClearConsignees
|
||||
};
|
||||
|
||||
foreach (var control in toolbarControls)
|
||||
{
|
||||
Controls.Remove(control);
|
||||
panelToolbar.Controls.Add(control);
|
||||
}
|
||||
|
||||
Controls.Add(panelToolbar);
|
||||
panelToolbar.BringToFront();
|
||||
|
||||
PContentInvoice.Dock = DockStyle.Fill;
|
||||
PContentInvoice.BringToFront();
|
||||
panelToolbar.BringToFront();
|
||||
|
||||
panelToolbar.Resize += (_, __) => ApplyToolbarLayout();
|
||||
Resize += (_, __) => ApplyToolbarLayout();
|
||||
_toolbarReady = true;
|
||||
}
|
||||
|
||||
private void ApplyToolbarLayout()
|
||||
{
|
||||
if (panelToolbar == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int pad = 12;
|
||||
const int gap = 8;
|
||||
const int clearGap = 4;
|
||||
const int controlHeight = 32;
|
||||
const int buttonHeight = 34;
|
||||
const int rowGap = 10;
|
||||
const int refreshWidth = 180;
|
||||
const int dateWidth = 112;
|
||||
const int clearSize = 32;
|
||||
const int invoicePreferred = 200;
|
||||
const int invoiceMin = 140;
|
||||
const int supplierPreferred = 170;
|
||||
const int consigneePreferred = 190;
|
||||
const int supplierMin = 130;
|
||||
const int consigneeMin = 150;
|
||||
|
||||
var panelWidth = Math.Max(panelToolbar.ClientSize.Width, 360);
|
||||
int y = 10;
|
||||
int x = pad;
|
||||
|
||||
button1.SetBounds(x, y, refreshWidth, buttonHeight);
|
||||
x = button1.Right + gap;
|
||||
|
||||
dtpDateFrom.SetBounds(x, y + 1, dateWidth, controlHeight);
|
||||
x = dtpDateFrom.Right + 6;
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new Point(x, y + 5);
|
||||
x = label1.Right + 6;
|
||||
dtpDateTo.SetBounds(x, y + 1, dateWidth, controlHeight);
|
||||
x = dtpDateTo.Right + gap;
|
||||
|
||||
int invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad - x));
|
||||
if (invoiceWidth < invoiceMin || x + invoiceMin > panelWidth - pad)
|
||||
{
|
||||
y += buttonHeight + rowGap;
|
||||
x = pad;
|
||||
invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad * 2));
|
||||
}
|
||||
|
||||
txtInvoiceNumber.SetBounds(x, y + 1, invoiceWidth, controlHeight);
|
||||
|
||||
y += buttonHeight + rowGap;
|
||||
x = pad;
|
||||
|
||||
int filtersAvailable = panelWidth - pad * 2 - clearSize * 2 - clearGap * 2 - gap;
|
||||
int supplierWidth;
|
||||
int consigneeWidth;
|
||||
if (filtersAvailable >= supplierPreferred + consigneePreferred)
|
||||
{
|
||||
supplierWidth = supplierPreferred;
|
||||
consigneeWidth = consigneePreferred;
|
||||
}
|
||||
else
|
||||
{
|
||||
float scale = filtersAvailable / (float)(supplierPreferred + consigneePreferred);
|
||||
supplierWidth = Math.Max(supplierMin, (int)(supplierPreferred * scale));
|
||||
consigneeWidth = Math.Max(consigneeMin, filtersAvailable - supplierWidth);
|
||||
}
|
||||
|
||||
if (pad + supplierWidth + clearGap + clearSize + gap + consigneeWidth + clearGap + clearSize > panelWidth)
|
||||
{
|
||||
// Wrap consignee to next row on very narrow widths.
|
||||
supplierWidth = Math.Max(supplierMin, panelWidth - pad * 2 - clearGap - clearSize);
|
||||
comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight);
|
||||
btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight);
|
||||
|
||||
y += buttonHeight + rowGap;
|
||||
x = pad;
|
||||
consigneeWidth = Math.Max(consigneeMin, panelWidth - pad * 2 - clearGap - clearSize);
|
||||
comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight);
|
||||
btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight);
|
||||
btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight);
|
||||
x = btnClearSuppliers.Right + gap;
|
||||
comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight);
|
||||
btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight);
|
||||
}
|
||||
|
||||
panelToolbar.Height = y + buttonHeight + pad + 4;
|
||||
panelToolbar.Padding = new Padding(pad, 10, pad, 14);
|
||||
}
|
||||
|
||||
private async Task SyncFromServerAsync()
|
||||
{
|
||||
button1.Enabled = false;
|
||||
@ -235,6 +389,7 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Invoices", "Не удалось загрузить накладные с API", ex);
|
||||
ToastNotification.ShowError($"Не удалось загрузить накладные: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
this.dgvOrder = new Elfisa.UI.Controls.ModernDataGridView();
|
||||
this.lblSumOfOrder = new System.Windows.Forms.Label();
|
||||
this.btnEraseOrder = new Elfisa.UI.Controls.ModernButton();
|
||||
this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton();
|
||||
this.btnSendOrder = new Elfisa.UI.Controls.ModernButton();
|
||||
this.button1 = new Elfisa.UI.Controls.ModernButton();
|
||||
this.lblMinimalSumOfOrder = new System.Windows.Forms.Label();
|
||||
@ -93,6 +94,18 @@
|
||||
this.btnEraseOrder.Text = "Очистить корзину";
|
||||
this.btnEraseOrder.Click += new System.EventHandler(this.btnEraseOrder_Click);
|
||||
//
|
||||
// btnDeleteGood
|
||||
//
|
||||
this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
|
||||
this.btnDeleteGood.Location = new System.Drawing.Point(538, 36);
|
||||
this.btnDeleteGood.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||
this.btnDeleteGood.Name = "btnDeleteGood";
|
||||
this.btnDeleteGood.Size = new System.Drawing.Size(260, 38);
|
||||
this.btnDeleteGood.TabIndex = 17;
|
||||
this.btnDeleteGood.Text = "Удалить товар";
|
||||
this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click);
|
||||
//
|
||||
// btnSendOrder
|
||||
//
|
||||
this.btnSendOrder.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
@ -184,6 +197,7 @@
|
||||
this.Controls.Add(this.btnSaveAndSendOrder);
|
||||
this.Controls.Add(this.lblMinimalSumOfOrder);
|
||||
this.Controls.Add(this.lblSumOfOrder);
|
||||
this.Controls.Add(this.btnDeleteGood);
|
||||
this.Controls.Add(this.btnEraseOrder);
|
||||
this.Controls.Add(this.btnSendOrder);
|
||||
this.Controls.Add(this.button1);
|
||||
@ -205,6 +219,7 @@
|
||||
private Elfisa.UI.Controls.ModernDataGridView dgvOrder;
|
||||
private System.Windows.Forms.Label lblSumOfOrder;
|
||||
private Elfisa.UI.Controls.ModernButton btnEraseOrder;
|
||||
private Elfisa.UI.Controls.ModernButton btnDeleteGood;
|
||||
private Elfisa.UI.Controls.ModernButton btnSendOrder;
|
||||
private Elfisa.UI.Controls.ModernButton button1;
|
||||
private System.Windows.Forms.Label lblMinimalSumOfOrder;
|
||||
|
||||
@ -248,6 +248,78 @@ and ConsigneesName = '{consigneeName}'
|
||||
}
|
||||
}
|
||||
|
||||
private void btnDeleteGood_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (dgvOrder.SelectedRows.Count == 0 || dgvOrder.CurrentRow == null)
|
||||
{
|
||||
ToastNotification.ShowCustom("Выберите наименование в корзине для удаления.", Color.DarkOrange, Color.White);
|
||||
return;
|
||||
}
|
||||
|
||||
var row = dgvOrder.SelectedRows[0];
|
||||
string idPriceListItem = null;
|
||||
|
||||
if (dgvOrder.Columns.Contains("id_PriceList_Item"))
|
||||
{
|
||||
idPriceListItem = row.Cells["id_PriceList_Item"]?.Value?.ToString();
|
||||
}
|
||||
else if (row.Cells.Count > 11)
|
||||
{
|
||||
idPriceListItem = row.Cells[11]?.Value?.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(idPriceListItem))
|
||||
{
|
||||
ToastNotification.ShowCustom("Не удалось определить позицию для удаления.", Color.DarkOrange, Color.White);
|
||||
return;
|
||||
}
|
||||
|
||||
var drugName = dgvOrder.Columns.Contains("Наименование")
|
||||
? row.Cells["Наименование"]?.Value?.ToString()
|
||||
: row.Cells.Count > 3
|
||||
? row.Cells[3]?.Value?.ToString()
|
||||
: null;
|
||||
|
||||
DeleteCartItem(idPriceListItem, drugName);
|
||||
}
|
||||
|
||||
private void DeleteCartItem(string idPriceListItem, string drugName)
|
||||
{
|
||||
var safeId = idPriceListItem.Replace("'", "''");
|
||||
string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{safeId}'";
|
||||
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}'";
|
||||
|
||||
using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB))
|
||||
{
|
||||
try
|
||||
{
|
||||
sqlCon.Open();
|
||||
using (var cmdClear = new SQLiteCommand(qClearPriceList, sqlCon))
|
||||
using (var cmdDelete = new SQLiteCommand(qDeleteFromTempOrder, sqlCon))
|
||||
{
|
||||
cmdClear.ExecuteNonQuery();
|
||||
cmdDelete.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(drugName))
|
||||
{
|
||||
ToastNotification.ShowSuccess($"Удалено: {drugName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ToastNotification.ShowError($"Ошибка удаления товара: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
sqlCon.Close();
|
||||
loadDataAboutOrderV2(_supplierName);
|
||||
ParentForm?.loadPriceListV2();
|
||||
ParentForm?.loadTempOrderItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EraseOrder(string Supplier)
|
||||
{
|
||||
using(SQLiteConnection sqlConForDeleteTempOrder = new SQLiteConnection(connectionStringToLocalDB))
|
||||
|
||||
@ -36,7 +36,6 @@
|
||||
this.TLPPriceList = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.dgvPriceList = new Elfisa.UI.Controls.ModernDataGridView();
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton();
|
||||
this.checkUsePercentageAsDefault = new System.Windows.Forms.CheckBox();
|
||||
this.lblPriceForGood = new System.Windows.Forms.Label();
|
||||
this.numericPercent = new System.Windows.Forms.NumericUpDown();
|
||||
@ -149,7 +148,6 @@
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Controls.Add(this.btnDeleteGood);
|
||||
this.panel1.Controls.Add(this.checkUsePercentageAsDefault);
|
||||
this.panel1.Controls.Add(this.lblPriceForGood);
|
||||
this.panel1.Controls.Add(this.numericPercent);
|
||||
@ -163,17 +161,6 @@
|
||||
this.panel1.Size = new System.Drawing.Size(1039, 54);
|
||||
this.panel1.TabIndex = 1;
|
||||
//
|
||||
// btnDeleteGood
|
||||
//
|
||||
this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||
this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
|
||||
this.btnDeleteGood.Location = new System.Drawing.Point(8, 62);
|
||||
this.btnDeleteGood.Name = "btnDeleteGood";
|
||||
this.btnDeleteGood.Size = new System.Drawing.Size(360, 34);
|
||||
this.btnDeleteGood.TabIndex = 13;
|
||||
this.btnDeleteGood.Text = "Удалить товар (BACKSPACE)";
|
||||
this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click);
|
||||
//
|
||||
// checkUsePercentageAsDefault
|
||||
//
|
||||
this.checkUsePercentageAsDefault.AutoSize = true;
|
||||
@ -514,6 +501,5 @@
|
||||
private Elfisa.UI.Controls.ModernButton btnClearSupplierFilter;
|
||||
private System.Windows.Forms.CheckBox checkUsePercentageAsDefault;
|
||||
private System.Windows.Forms.Timer timerForResetSearch;
|
||||
private Elfisa.UI.Controls.ModernButton btnDeleteGood;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,6 @@ namespace Электронная_Фармация.UserControls
|
||||
UiThemeHelper.ApplyToControlTree(this);
|
||||
Tag = "chrome-child";
|
||||
|
||||
btnDeleteGood.Height = 34;
|
||||
AlignSearchRow();
|
||||
|
||||
panel1.SizeChanged += (_, __) => LayoutMarkupRow();
|
||||
@ -870,6 +869,61 @@ namespace Электронная_Фармация.UserControls
|
||||
ShowBrowseView();
|
||||
}
|
||||
|
||||
private void HandleBackspaceInPriceList(int rowIndex, string idPriceListItem)
|
||||
{
|
||||
if (_browseMode == PriceBrowseMode.Offers)
|
||||
{
|
||||
if (rowIndex >= 0
|
||||
&& rowIndex < dgvPriceList.Rows.Count
|
||||
&& dgvPriceList.Rows[rowIndex].Cells.Count > 9)
|
||||
{
|
||||
var qty = dgvPriceList.Rows[rowIndex].Cells[9].Value?.ToString() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(qty))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(idPriceListItem))
|
||||
{
|
||||
deleteGood(rowIndex, idPriceListItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
dgvPriceList.Rows[rowIndex].Cells[9].Value = string.Empty;
|
||||
dgvPriceList.Rows[rowIndex].Cells[10].Value = string.Empty;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ResetBrowseToNames();
|
||||
if (dgvBrowseNames != null && dgvBrowseNames.Visible)
|
||||
{
|
||||
dgvBrowseNames.Focus();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (GoodsFilter.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
|
||||
txtSearchGoodNameInPriceList.Text = GoodsFilter;
|
||||
ResetBrowseToNames();
|
||||
|
||||
if (GoodsFilter.Length == 0)
|
||||
{
|
||||
timerForResetSearch.Enabled = false;
|
||||
timerForResetSearch.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
timerForResetSearch.Enabled = true;
|
||||
timerForResetSearch.Start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void loadInfoAboutDefaultMarkup()
|
||||
{
|
||||
@ -1411,28 +1465,14 @@ and tempOrder.GoodName = PriceList.GoodName
|
||||
}
|
||||
}
|
||||
|
||||
if (_browseMode != PriceBrowseMode.Offers)
|
||||
if (e.KeyChar == (Char)Keys.Back)
|
||||
{
|
||||
if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0)
|
||||
{
|
||||
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
|
||||
txtSearchGoodNameInPriceList.Text = GoodsFilter;
|
||||
ResetBrowseToNames();
|
||||
|
||||
if (GoodsFilter.Length == 0)
|
||||
{
|
||||
timerForResetSearch.Enabled = false;
|
||||
timerForResetSearch.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
timerForResetSearch.Enabled = true;
|
||||
timerForResetSearch.Start();
|
||||
}
|
||||
|
||||
HandleBackspaceInPriceList(RowIndex, idPriceListItem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_browseMode != PriceBrowseMode.Offers)
|
||||
{
|
||||
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z')
|
||||
|| (e.KeyChar >= 'a' && e.KeyChar <= 'z')
|
||||
|| (e.KeyChar >= 'А' && e.KeyChar <= 'Я')
|
||||
@ -1443,7 +1483,7 @@ and tempOrder.GoodName = PriceList.GoodName
|
||||
GoodsFilter += e.KeyChar.ToString();
|
||||
txtSearchGoodNameInPriceList.Text = GoodsFilter;
|
||||
ResetBrowseToNames();
|
||||
if (dgvPriceList.Rows.Count == 0)
|
||||
if (dgvBrowseNames != null && dgvBrowseNames.Rows.Count == 0)
|
||||
{
|
||||
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
|
||||
GoodsFilter = string.Empty;
|
||||
@ -1455,36 +1495,6 @@ and tempOrder.GoodName = PriceList.GoodName
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0)
|
||||
{
|
||||
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
|
||||
txtSearchGoodNameInPriceList.Text = GoodsFilter;
|
||||
ResetBrowseToNames();
|
||||
|
||||
if (GoodsFilter.Length == 0)
|
||||
{
|
||||
timerForResetSearch.Enabled = false;
|
||||
timerForResetSearch.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
timerForResetSearch.Enabled = true;
|
||||
timerForResetSearch.Start();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (timerForResetSearch.Enabled == false)
|
||||
{
|
||||
if (RowIndex >= 0 && !string.IsNullOrEmpty(idPriceListItem))
|
||||
{
|
||||
deleteGood(RowIndex, idPriceListItem);
|
||||
timerForResetSearch.Enabled = true;
|
||||
timerForResetSearch.Start();
|
||||
}
|
||||
}
|
||||
|
||||
if ((e.KeyChar >= 'A' && e.KeyChar <= 'Z') || (e.KeyChar >= 'a' && e.KeyChar <= 'z') || (e.KeyChar >= 'А' && e.KeyChar <= 'Я') || (e.KeyChar >= 'а' && e.KeyChar <= 'я') || (e.KeyChar == (Char)Keys.Subtract))
|
||||
{
|
||||
if (e.KeyChar == (Char)Keys.Enter)
|
||||
@ -1573,26 +1583,6 @@ and tempOrder.GoodName = PriceList.GoodName
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (e.KeyChar == (Char)Keys.Back)
|
||||
{
|
||||
if (RowIndex < 0) return;
|
||||
|
||||
string countToBuy = dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString();
|
||||
if (countToBuy.Length > 1)
|
||||
{
|
||||
string result = countToBuy.Remove(countToBuy.Length - 1);
|
||||
|
||||
dgvPriceList.Rows[RowIndex].Cells[9].Value = result;
|
||||
|
||||
if (!TryReadDecimal(dgvPriceList.Rows[RowIndex].Cells[5].Value, out var priceForOne) ||
|
||||
!TryReadInt32(result, out var count))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dgvPriceList.Rows[RowIndex].Cells[10].Value = (count * priceForOne).ToString();
|
||||
}
|
||||
}
|
||||
else if (e.KeyChar == (Char)Keys.Delete)
|
||||
{
|
||||
if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return;
|
||||
@ -2341,14 +2331,6 @@ and SumOrderedItems = '{SumOrder}'";
|
||||
timerForResetSearch.Stop();
|
||||
}
|
||||
|
||||
private void btnDeleteGood_Click(object sender, EventArgs e)
|
||||
{
|
||||
int RowIndex = dgvPriceList.SelectedRows[0].Index;
|
||||
string idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value.ToString();
|
||||
deleteGood(RowIndex, idPriceListItem);
|
||||
}
|
||||
|
||||
|
||||
private void btnMoreInfoAboutGood_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (dgvPriceList.CurrentRow == null)
|
||||
|
||||
@ -109,6 +109,7 @@ namespace Электронная_Фармация.UserControls
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("Refusals", "Не удалось загрузить отказы с API", ex);
|
||||
ToastNotification.ShowError($"Не удалось загрузить отказы: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
|
||||
Loading…
Reference in New Issue
Block a user