diff --git a/dist/Электронная-Фармация/Elfisa.UI.dll b/dist/Электронная-Фармация/Elfisa.UI.dll index cb04318..e17bc77 100644 Binary files a/dist/Электронная-Фармация/Elfisa.UI.dll and b/dist/Электронная-Фармация/Elfisa.UI.dll differ diff --git a/dist/Электронная-Фармация/Elfisa.UI.pdb b/dist/Электронная-Фармация/Elfisa.UI.pdb index af1bdf3..a900d5b 100644 Binary files a/dist/Электронная-Фармация/Elfisa.UI.pdb and b/dist/Электронная-Фармация/Elfisa.UI.pdb differ diff --git a/dist/Электронная-Фармация/Электронная Фармация.exe b/dist/Электронная-Фармация/Электронная Фармация.exe index 77e7afe..bbda518 100644 Binary files a/dist/Электронная-Фармация/Электронная Фармация.exe and b/dist/Электронная-Фармация/Электронная Фармация.exe differ diff --git a/dist/Электронная-Фармация/Электронная Фармация.pdb b/dist/Электронная-Фармация/Электронная Фармация.pdb index 6616477..a442355 100644 Binary files a/dist/Электронная-Фармация/Электронная Фармация.pdb and b/dist/Электронная-Фармация/Электронная Фармация.pdb differ diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index f121e08..f955928 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -1,38 +1,38 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using System.Net.Http.Json; using System.Text.Json; -using System.Text.Json.Serialization; namespace Электронная_Фармация.Classes { public class ApiClient { + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = null + }; + private readonly HttpClient _httpClient; private readonly string _baseUrl; private string _token; public ApiClient(string baseUrl = null) { - _baseUrl = (baseUrl ?? AppConfig.ApiBaseUrl).TrimEnd('/'); + _baseUrl = AppConfig.NormalizeApiBaseUrl(baseUrl ?? AppConfig.ApiBaseUrl); _httpClient = new HttpClient { - BaseAddress = new Uri(_baseUrl), - Timeout = TimeSpan.FromSeconds(30) + Timeout = TimeSpan.FromSeconds(60) }; + + AppDebugLog.Info("ApiClient", $"Создан клиент. BaseUrl={_baseUrl}, Timeout={_httpClient.Timeout.TotalSeconds}s"); } - /// - /// Выполняет авторизацию и сохраняет токен - /// - /// Имя пользователя - /// Пароль - /// JWT токен - /// При ошибке авторизации + public string BaseUrl => _baseUrl; + public async Task LoginAsync(string username, string password) { var request = new LoginRequest @@ -41,54 +41,65 @@ namespace Электронная_Фармация.Classes Password = password }; + var path = "/auth/login"; + var fullUrl = BuildFullUrl(path); + var requestJson = JsonSerializer.Serialize(request, JsonOptions); + var stopwatch = Stopwatch.StartNew(); + + AppDebugLog.ApiRequest("POST", fullUrl, requestJson, $"username={username}"); + try { - var response = await _httpClient.PostAsJsonAsync("/auth/login", request); + using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl)) + { + httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(httpRequest); + var responseBody = await response.Content.ReadAsStringAsync(); + stopwatch.Stop(); - if (response.IsSuccessStatusCode) - { - var loginResponse = await response.Content.ReadFromJsonAsync(); - _token = loginResponse.Token; - return _token; - } - else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - var error = await response.Content.ReadFromJsonAsync(); - throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}"); - } - else - { - var error = await response.Content.ReadFromJsonAsync(); - throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {error?.Error ?? "Неизвестная ошибка"}"); + AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); + return ParseLoginResponse(response, responseBody); } } - catch (TaskCanceledException) + catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) { - throw new HttpRequestException("Превышено время ожидания ответа сервера"); + stopwatch.Stop(); + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw WrapNetworkError(ex, "авторизацию"); + } + catch (TaskCanceledException ex) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw WrapNetworkError(ex, "авторизацию"); + } + catch (Exception ex) + { + stopwatch.Stop(); + if (ex is UnauthorizedAccessException || ex is HttpRequestException) + { + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw; + } + + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw new HttpRequestException($"Ошибка при авторизации ({_baseUrl}): {ex.Message}", ex); } } - /// - /// Устанавливает токен для последующих запросов - /// public void SetToken(string token) { - _token = token; + _token = string.IsNullOrWhiteSpace(token) ? null : token; + AppDebugLog.Info("ApiClient", $"Токен установлен: {AppDebugLog.MaskToken(_token)}"); } public string Token => _token; - /// - /// Проверяет, авторизован ли клиент - /// public bool IsAuthenticated => !string.IsNullOrEmpty(_token); public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null) { - if (!IsAuthenticated) - { - throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); - } + EnsureAuthenticated(); var queryParams = new List(); if (!string.IsNullOrEmpty(supplierId)) @@ -100,64 +111,147 @@ namespace Электронная_Фармация.Classes queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}"); } - var url = "/api/supplier-prices/summary"; + var path = "/api/supplier-prices/summary"; if (queryParams.Count > 0) { - url += "?" + string.Join("&", queryParams); + path += "?" + string.Join("&", queryParams); } - _httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token); - - try - { - var response = await _httpClient.GetAsync(url); - - if (response.IsSuccessStatusCode) - { - var summary = await response.Content.ReadFromJsonAsync(); - return summary; - } - else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - _token = null; // Токен истек или невалиден - var error = await response.Content.ReadFromJsonAsync(); - throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"); - } - else - { - var error = await response.Content.ReadFromJsonAsync(); - throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {error?.Error ?? "Неизвестная ошибка"}"); - } - - - - } - catch (TaskCanceledException) - { - throw new HttpRequestException("Превышено время ожидания ответа сервера (таймаут 30 секунд)"); - } + var responseBody = await SendAuthorizedGetAsync(path, "загрузку прайса"); + var summary = JsonSerializer.Deserialize(responseBody, JsonOptions); + var count = summary?.Summary?.Count ?? 0; + AppDebugLog.Info("ApiClient", $"Прайс получен: {count} позиций"); + return summary; } public async Task> GetInvoicesAsync() + { + EnsureAuthenticated(); + + var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных"); + var items = JsonSerializer.Deserialize>(responseBody, JsonOptions) ?? new List(); + AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт."); + return items; + } + + private void EnsureAuthenticated() { if (!IsAuthenticated) { - throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом накладных"); + throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); } + } - _httpClient.DefaultRequestHeaders.Authorization = - new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _token); + private async Task SendAuthorizedGetAsync(string path, string operationName) + { + var fullUrl = BuildFullUrl(path); + var stopwatch = Stopwatch.StartNew(); - var response = await _httpClient.GetAsync("/api/buyer/invoices"); - var body = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) + AppDebugLog.ApiRequest("GET", fullUrl, extra: $"token={AppDebugLog.MaskToken(_token)}"); + + try { - throw new HttpRequestException( - $"Ошибка загрузки накладных: {(int)response.StatusCode} {response.ReasonPhrase}\n{body}"); + using (var httpRequest = new HttpRequestMessage(HttpMethod.Get, fullUrl)) + { + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token); + var response = await _httpClient.SendAsync(httpRequest); + var responseBody = await response.Content.ReadAsStringAsync(); + stopwatch.Stop(); + + AppDebugLog.ApiResponse("GET", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseBody); + + if (response.IsSuccessStatusCode) + { + return responseBody; + } + + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + _token = null; + var error = TryDeserializeError(responseBody); + throw new UnauthorizedAccessException($"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}"); + } + + var httpError = TryDeserializeError(responseBody); + throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"); + } + } + catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP")) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw WrapNetworkError(ex, operationName); + } + catch (TaskCanceledException ex) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw WrapNetworkError(ex, operationName); + } + catch (Exception ex) when (!(ex is UnauthorizedAccessException) && !(ex is HttpRequestException)) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("GET", fullUrl, stopwatch.ElapsedMilliseconds, ex); + throw; + } + } + + private string ParseLoginResponse(HttpResponseMessage response, string responseBody) + { + if (response.IsSuccessStatusCode) + { + var loginResponse = JsonSerializer.Deserialize(responseBody, JsonOptions); + if (loginResponse == null || string.IsNullOrWhiteSpace(loginResponse.Token)) + { + throw new HttpRequestException($"Сервер {_baseUrl} вернул пустой токен авторизации"); + } + + _token = loginResponse.Token; + AppDebugLog.Info("ApiClient", $"Авторизация успешна. token={AppDebugLog.MaskToken(_token)}"); + return _token; } - return JsonSerializer.Deserialize>(body) ?? new List(); + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + var error = TryDeserializeError(responseBody); + throw new UnauthorizedAccessException($"Ошибка авторизации: {error?.Error ?? "Неверные учетные данные"}"); + } + + var httpError = TryDeserializeError(responseBody); + throw new HttpRequestException($"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}"); + } + + private string BuildFullUrl(string path) + { + return new Uri(new Uri(_baseUrl + "/"), path.TrimStart('/')).ToString(); + } + + private static ErrorResponse TryDeserializeError(string body) + { + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(body, JsonOptions); + } + catch + { + return null; + } + } + + private HttpRequestException WrapNetworkError(Exception ex, string operation) + { + var details = ex.InnerException != null && ex.InnerException.Message != ex.Message + ? $" ({ex.InnerException.Message})" + : string.Empty; + + return new HttpRequestException( + $"Не удалось выполнить {operation}. Сервер: {_baseUrl}{details}. {ex.Message}", + ex); } } diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index d2cc970..505bc28 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -9,9 +9,32 @@ namespace Электронная_Фармация.Classes /// public static class AppConfig { - public const string DefaultApiBaseUrl = "http://195.34.241.84:9988"; + public const string DefaultApiBaseUrl = "https://24pharmdata.ru"; + private const string LegacyApiBaseUrl = "http://195.34.241.84:9988"; private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL"; + /// + /// Replaces known dead API URLs in saved user settings. + /// + public static void MigrateLegacySettings() + { + var current = Settings.Default.ApiBaseUrl; + if (string.IsNullOrWhiteSpace(current)) + { + return; + } + + var normalized = NormalizeApiBaseUrl(current); + if (string.Equals(normalized, LegacyApiBaseUrl, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalized, "http://195.34.241.84:9988", StringComparison.OrdinalIgnoreCase)) + { + Settings.Default.ApiBaseUrl = DefaultApiBaseUrl; + Settings.Default.stringToken = string.Empty; + Settings.Default.Save(); + AppDebugLog.Info("AppConfig", $"ApiBaseUrl мигрирован: {LegacyApiBaseUrl} -> {DefaultApiBaseUrl}"); + } + } + public static string ApiBaseUrl { get @@ -19,19 +42,36 @@ namespace Электронная_Фармация.Classes var fromSettings = Settings.Default.ApiBaseUrl; if (!string.IsNullOrWhiteSpace(fromSettings)) { - return fromSettings.Trim().TrimEnd('/'); + return NormalizeApiBaseUrl(fromSettings); } var fromEnv = Environment.GetEnvironmentVariable(ApiBaseUrlEnvVar); if (!string.IsNullOrWhiteSpace(fromEnv)) { - return fromEnv.Trim().TrimEnd('/'); + return NormalizeApiBaseUrl(fromEnv); } return DefaultApiBaseUrl; } } + public static string NormalizeApiBaseUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return DefaultApiBaseUrl; + } + + url = url.Trim().TrimEnd('/'); + if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && + !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + url = "http://" + url; + } + + return url; + } + public static string SqliteDbPath { get diff --git a/src/ElectronicPharmacy/Classes/AppDebugLog.cs b/src/ElectronicPharmacy/Classes/AppDebugLog.cs new file mode 100644 index 0000000..3974b87 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/AppDebugLog.cs @@ -0,0 +1,202 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Электронная_Фармация.Classes +{ + /// + /// File logger for API/auth/network debugging. + /// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable. + /// + public static class AppDebugLog + { + private static readonly object Sync = new object(); + private const int MaxBodyLength = 4000; + private const string DisableEnvVar = "ELF_API_DEBUG"; + private const string DisableMarkerFile = "api-debug.off"; + + public static bool IsEnabled + { + get + { + var env = Environment.GetEnvironmentVariable(DisableEnvVar); + if (string.Equals(env, "0", StringComparison.OrdinalIgnoreCase) || + string.Equals(env, "false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var markerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DisableMarkerFile); + return !File.Exists(markerPath); + } + } + + public static string LogDirectory => + Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + + public static string CurrentLogFilePath => + Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log"); + + public static void Info(string category, string message) + { + Write("INFO", category, message, null); + } + + public static void Warning(string category, string message) + { + Write("WARN", category, message, null); + } + + public static void Error(string category, string message, Exception exception = null) + { + Write("ERROR", category, message, exception); + } + + public static void ApiRequest(string method, string url, string requestBody = null, string extra = null) + { + var message = new StringBuilder(); + message.Append(method).Append(" ").Append(url); + if (!string.IsNullOrWhiteSpace(extra)) + { + message.Append(" | ").Append(extra); + } + + if (!string.IsNullOrWhiteSpace(requestBody)) + { + message.Append(" | body=").Append(Truncate(SanitizeJson(requestBody))); + } + + Write("REQ", "Api", message.ToString(), null); + } + + public static void ApiResponse(string method, string url, int statusCode, long elapsedMs, string responseBody = null) + { + var message = new StringBuilder(); + message.Append(method).Append(" ").Append(url); + message.Append(" -> HTTP ").Append(statusCode); + message.Append(" (").Append(elapsedMs).Append(" ms)"); + + if (!string.IsNullOrWhiteSpace(responseBody)) + { + message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody))); + } + + Write(statusCode >= 400 ? "RESP-ERR" : "RESP", "Api", message.ToString(), null); + } + + public static void ApiFailure(string method, string url, long elapsedMs, Exception exception) + { + var message = $"{method} {url} failed after {elapsedMs} ms: {exception.Message}"; + Write("FAIL", "Api", message, exception); + } + + public static string MaskToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return "(empty)"; + } + + if (token.Length <= 8) + { + return "***"; + } + + return token.Substring(0, 4) + "..." + token.Substring(token.Length - 4); + } + + public static string SanitizeJson(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return string.Empty; + } + + var sanitized = Regex.Replace( + json, + @"""password""\s*:\s*""[^""]*""", + @"""password"":""***""", + RegexOptions.IgnoreCase); + + sanitized = Regex.Replace( + sanitized, + @"""token""\s*:\s*""([^""]*)""", + m => @"""token"":""" + MaskToken(m.Groups[1].Value) + @"""", + RegexOptions.IgnoreCase); + + return sanitized; + } + + public static string FormatException(Exception exception) + { + if (exception == null) + { + return string.Empty; + } + + var sb = new StringBuilder(); + var current = exception; + var depth = 0; + while (current != null && depth < 8) + { + if (depth > 0) + { + sb.AppendLine(); + sb.Append(" -> "); + } + + sb.Append(current.GetType().Name).Append(": ").Append(current.Message); + current = current.InnerException; + depth++; + } + + return sb.ToString(); + } + + private static void Write(string level, string category, string message, Exception exception) + { + if (!IsEnabled) + { + return; + } + + try + { + var line = new StringBuilder(); + line.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); + line.Append(" [").Append(level).Append("] "); + line.Append("[").Append(category).Append("] "); + line.Append(message); + + if (exception != null) + { + line.Append(" | ").Append(FormatException(exception)); + } + + lock (Sync) + { + Directory.CreateDirectory(LogDirectory); + File.AppendAllText(CurrentLogFilePath, line + Environment.NewLine, Encoding.UTF8); + } + + Debug.WriteLine(line.ToString()); + } + catch + { + // Logging must never break the app. + } + } + + private static string Truncate(string text) + { + if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLength) + { + return text ?? string.Empty; + } + + return text.Substring(0, MaxBodyLength) + "...[truncated]"; + } + } +} diff --git a/src/ElectronicPharmacy/Classes/DataSender.cs b/src/ElectronicPharmacy/Classes/DataSender.cs index 9e13ae8..fb11e8d 100644 --- a/src/ElectronicPharmacy/Classes/DataSender.cs +++ b/src/ElectronicPharmacy/Classes/DataSender.cs @@ -197,6 +197,10 @@ WHERE Orders.OrderState = 'НОВЫЙ' DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; string json = JsonSerializer.Serialize(order, options); + var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), BuyerOrdersEndpoint.TrimStart('/')).ToString(); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + AppDebugLog.ApiRequest("POST", fullUrl, json, $"order={order.LocalOrderId}"); using (var client = new HttpClient()) { @@ -204,18 +208,33 @@ WHERE Orders.OrderState = 'НОВЫЙ' client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token); var content = new StringContent(json, Encoding.UTF8, "application/json"); - HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content); - string responseString = await response.Content.ReadAsStringAsync(); - - if (!response.IsSuccessStatusCode) + try { - ToastNotification.ShowError( - $"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}"); + HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content); + string responseString = await response.Content.ReadAsStringAsync(); + stopwatch.Stop(); + + AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseString); + + if (!response.IsSuccessStatusCode) + { + AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}"); + ToastNotification.ShowError( + $"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}"); + return false; + } + + MarkOrderSent(order.LocalOrderId); + AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен"); + return true; + } + catch (Exception ex) + { + stopwatch.Stop(); + AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex); + ToastNotification.ShowError($"Ошибка сети при отправке заказа {order.LocalOrderId}: {ex.Message}"); return false; } - - MarkOrderSent(order.LocalOrderId); - return true; } } diff --git a/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs b/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs new file mode 100644 index 0000000..e4a37f7 --- /dev/null +++ b/src/ElectronicPharmacy/Classes/InvoiceRequestService.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.Globalization; + +namespace Электронная_Фармация.Classes +{ + public sealed class InvoiceRequestService + { + public string CreateOrUpdateFromOrder(string orderId) + { + if (string.IsNullOrWhiteSpace(orderId)) + { + throw new ArgumentException("Не указан заказ для формирования накладной.", nameof(orderId)); + } + + using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) + { + con.Open(); + + string orderNumber; + string supplierName; + string consigneeName; + string orderState; + + using (var cmd = new SQLiteCommand(@" +select + o.[OrderNumber], + o.[SupplierName], + ifnull(o.[ConsigneeName], '') as [ConsigneeName], + ifnull(o.[OrderState], '') as [OrderState] +from [Orders] o +where o.[Id_Order] = @orderId;", con)) + { + cmd.Parameters.AddWithValue("@orderId", orderId); + using (var reader = cmd.ExecuteReader()) + { + if (!reader.Read()) + { + throw new InvalidOperationException("Выбранный заказ не найден."); + } + + orderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty; + supplierName = reader["SupplierName"]?.ToString() ?? string.Empty; + consigneeName = reader["ConsigneeName"]?.ToString() ?? string.Empty; + orderState = reader["OrderState"]?.ToString() ?? string.Empty; + } + } + + if (!string.Equals(orderState, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("Получить накладную можно только для заказа со статусом ОТПРАВЛЕН."); + } + + var items = new List(); + using (var cmd = new SQLiteCommand(@" +select + ifnull([es_code], 0) as [GoodCode], + [DrugName], + [Price], + ifnull([Zakaz], 0) as [Zakaz], + ifnull([RefuseItemsCount], 0) as [RefuseItemsCount], + ifnull([ExpiryPeriod], '') as [ExpiryPeriod] +from [OrderItems] +where [Id_Order] = @orderId +order by [DrugName];", con)) + { + cmd.Parameters.AddWithValue("@orderId", orderId); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + int orderedQty = SafeInt(reader["Zakaz"]); + int refusedQty = SafeInt(reader["RefuseItemsCount"]); + int arrivedQty = Math.Max(orderedQty - refusedQty, 0); + if (arrivedQty <= 0) + { + continue; + } + + var price = SafeDecimal(reader["Price"]); + items.Add(new InvoiceOrderItem + { + GoodCode = SafeInt(reader["GoodCode"]), + GoodName = reader["DrugName"]?.ToString() ?? string.Empty, + Price = price, + ArrivedQty = arrivedQty, + ExpiryPeriod = reader["ExpiryPeriod"]?.ToString() ?? string.Empty + }); + } + } + } + + if (items.Count == 0) + { + throw new InvalidOperationException("Для этого заказа нет фактически пришедших позиций."); + } + + int invoiceId; + string invoiceNumber; + using (var findCmd = new SQLiteCommand(@" +select [idInvoice], [InvoiceNumber] +from [Invoice] +where [SourceOrderId] = @orderId +limit 1;", con)) + { + findCmd.Parameters.AddWithValue("@orderId", orderId); + using (var reader = findCmd.ExecuteReader()) + { + if (reader.Read()) + { + invoiceId = Convert.ToInt32(reader["idInvoice"]); + invoiceNumber = reader["InvoiceNumber"]?.ToString() ?? string.Empty; + } + else + { + invoiceId = 0; + invoiceNumber = $"НКЛ-{orderNumber}"; + } + } + } + + var invoiceDate = DateTime.Now.ToString("yyyy-MM-dd"); + var invoiceSum = 0m; + foreach (var item in items) + { + invoiceSum += item.Price * item.ArrivedQty; + } + + if (invoiceId == 0) + { + using (var insertCmd = new SQLiteCommand(@" +insert into [Invoice] +( + [InvoiceNumber], + [InvoiceDate], + [CodeSupplier], + [CodeConsignees], + [SumWithoutNDS], + [SumWithNDS], + [SumNDS], + [SupplierName], + [ConsigneesName], + [InvoiceSum], + [RefuseSum], + [SourceOrderId], + [SourceOrderNumber], + [IsRequestedInvoice] +) +values +( + @number, + @date, + 0, + 0, + @sum, + @sum, + 0, + @supplier, + @consignee, + @invoiceSum, + 0, + @sourceOrderId, + @sourceOrderNumber, + 1 +); +select last_insert_rowid();", con)) + { + insertCmd.Parameters.AddWithValue("@number", invoiceNumber); + insertCmd.Parameters.AddWithValue("@date", invoiceDate); + insertCmd.Parameters.AddWithValue("@sum", invoiceSum); + insertCmd.Parameters.AddWithValue("@supplier", supplierName); + insertCmd.Parameters.AddWithValue("@consignee", consigneeName); + insertCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum); + insertCmd.Parameters.AddWithValue("@sourceOrderId", orderId); + insertCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber); + invoiceId = Convert.ToInt32(insertCmd.ExecuteScalar()); + } + } + else + { + using (var updateCmd = new SQLiteCommand(@" +update [Invoice] +set + [InvoiceDate] = @date, + [SumWithoutNDS] = @sum, + [SumWithNDS] = @sum, + [SumNDS] = 0, + [SupplierName] = @supplier, + [ConsigneesName] = @consignee, + [InvoiceSum] = @invoiceSum, + [RefuseSum] = 0, + [SourceOrderNumber] = @sourceOrderNumber, + [IsRequestedInvoice] = 1 +where [idInvoice] = @invoiceId;", con)) + { + updateCmd.Parameters.AddWithValue("@date", invoiceDate); + updateCmd.Parameters.AddWithValue("@sum", invoiceSum); + updateCmd.Parameters.AddWithValue("@supplier", supplierName); + updateCmd.Parameters.AddWithValue("@consignee", consigneeName); + updateCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum); + updateCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber); + updateCmd.Parameters.AddWithValue("@invoiceId", invoiceId); + updateCmd.ExecuteNonQuery(); + } + + using (var deleteItemsCmd = new SQLiteCommand("delete from [InvoiceItem] where [idInvoice] = @invoiceId;", con)) + { + deleteItemsCmd.Parameters.AddWithValue("@invoiceId", invoiceId); + deleteItemsCmd.ExecuteNonQuery(); + } + } + + foreach (var item in items) + { + decimal lineSum = item.Price * item.ArrivedQty; + using (var itemCmd = new SQLiteCommand(@" +insert into [InvoiceItem] +( + [idGood], + [GoodCode], + [Good], + [ProducerName], + [CountryName], + [idInvoice], + [GoodsGount], + [PriceSupplierWithoutNDS], + [PriceSupplierWithNDS], + [PriceProducerWithoutNDS], + [PriceProducerWithNDS], + [NDS], + [JNVLS], + [PriceReestr], + [SumWithoutNDS], + [SumWithNDS], + [SumNDS], + [Serial], + [BestBefore], + [Sertificate], + [Marked], + [GTIN] +) +values +( + null, + @goodCode, + @good, + '', + '', + @invoiceId, + @qty, + @price, + @price, + null, + null, + 0, + 0, + null, + @sum, + @sum, + 0, + '', + @bestBefore, + '', + 0, + '' +);", con)) + { + itemCmd.Parameters.AddWithValue("@goodCode", item.GoodCode); + itemCmd.Parameters.AddWithValue("@good", item.GoodName); + itemCmd.Parameters.AddWithValue("@invoiceId", invoiceId); + itemCmd.Parameters.AddWithValue("@qty", item.ArrivedQty); + itemCmd.Parameters.AddWithValue("@price", item.Price); + itemCmd.Parameters.AddWithValue("@sum", lineSum); + itemCmd.Parameters.AddWithValue("@bestBefore", item.ExpiryPeriod); + itemCmd.ExecuteNonQuery(); + } + } + + return orderId; + } + } + + private static int SafeInt(object value) + { + if (value == null || value == DBNull.Value) + { + return 0; + } + + int result; + return int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result) + || int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.CurrentCulture, out result) + ? result + : 0; + } + + private static decimal SafeDecimal(object value) + { + if (value == null || value == DBNull.Value) + { + return 0m; + } + + var text = value.ToString()?.Trim(); + if (string.IsNullOrWhiteSpace(text)) + { + return 0m; + } + + decimal result; + if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + text = text.Replace(',', '.'); + if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result)) + { + return result; + } + + if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out result)) + { + return result; + } + + return 0m; + } + + private sealed class InvoiceOrderItem + { + public int GoodCode { get; set; } + public string GoodName { get; set; } + public decimal Price { get; set; } + public int ArrivedQty { get; set; } + public string ExpiryPeriod { get; set; } + } + } +} diff --git a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs index e73634a..b8ab6d0 100644 --- a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs +++ b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs @@ -75,6 +75,14 @@ namespace Электронная_Фармация.Classes textBox.BorderStyle = BorderStyle.FixedSingle; textBox.Font = theme.FontRegular13; } + else if (control is RichTextBox richTextBox) + { + richTextBox.BackColor = theme.BackgroundPrimary; + richTextBox.ForeColor = theme.TextPrimary; + richTextBox.BorderStyle = BorderStyle.None; + richTextBox.Font = theme.FontRegular13; + richTextBox.ReadOnly = true; + } else if (control is NumericUpDown numeric) { numeric.BackColor = theme.InputBackground; diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index 9e67665..68e384c 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -103,8 +103,10 @@ + + diff --git a/src/ElectronicPharmacy/Form1.Designer.cs b/src/ElectronicPharmacy/Form1.Designer.cs index 1e3da04..92c74e5 100644 --- a/src/ElectronicPharmacy/Form1.Designer.cs +++ b/src/ElectronicPharmacy/Form1.Designer.cs @@ -230,7 +230,7 @@ this.tsItemInvoices.Name = "tsItemInvoices"; this.tsItemInvoices.Size = new System.Drawing.Size(224, 32); this.tsItemInvoices.Text = "Накладные"; - this.tsItemInvoices.Visible = false; + this.tsItemInvoices.Visible = true; this.tsItemInvoices.Click += new System.EventHandler(this.tsItemInvoices_Click); // // toolStripSeparator1 diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs index da3ddd5..5c6a08d 100644 --- a/src/ElectronicPharmacy/Form1.UiShell.cs +++ b/src/ElectronicPharmacy/Form1.UiShell.cs @@ -51,6 +51,7 @@ namespace Электронная_Фармация { new SidebarItem { Key = "price", Text = "ПРАЙС-ЛИСТ", IconGlyph = "\uE8FD" }, new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" }, + new SidebarItem { Key = "invoices", Text = "НАКЛАДНЫЕ", IconGlyph = "\uE8A5" }, new SidebarItem { Key = "upload", Text = "ЗАГРУЗИТЬ", IconGlyph = "\uE896" }, new SidebarItem { Key = "send", Text = "ОТПРАВИТЬ", IconGlyph = "\uE725" }, new SidebarItem { Key = "refusals", Text = "ОТКАЗЫ", IconGlyph = "\uE711" }, @@ -207,6 +208,9 @@ namespace Электронная_Фармация case "orders": OpenOrdersTab(); break; + case "invoices": + OpenInvoicesTab(); + break; case "upload": tsBtnDownloadData_Click(this, EventArgs.Empty); _appHeader.SetActive("upload"); @@ -300,6 +304,11 @@ namespace Электронная_Фармация return; } + if (sender is Control clicked && HasDedicatedContextMenu(clicked)) + { + return; + } + _documentCloseTabMenuItem.Enabled = !string.IsNullOrEmpty(_documentTabStrip.ActiveTabId); _documentCloseAllTabsMenuItem.Enabled = _documentTabStrip.Tabs.Count > 0; @@ -307,6 +316,19 @@ namespace Электронная_Фармация _documentTabMenu.Show(screenPoint); } + private static bool HasDedicatedContextMenu(Control control) + { + for (var current = control; current != null; current = current.Parent) + { + if (current.ContextMenuStrip != null) + { + return true; + } + } + + return false; + } + private void CloseDocumentTab(string tabId) { if (_documentContents.TryGetValue(tabId, out var control)) @@ -343,6 +365,24 @@ namespace Электронная_Фармация orders.Show(); } + public void OpenInvoicesTab(string sourceOrderId = null) + { + var existing = _documentTabStrip.FindByNavKey("invoices"); + if (existing != null && + _documentContents.TryGetValue(existing.Id, out var existingControl) && + existingControl is UCInvoices existingInvoices) + { + existingInvoices.SetSourceOrderFilter(sourceOrderId); + _documentTabStrip.SelectTab(existing.Id); + ShowDocumentTab(existing.Id); + _appHeader.SetActive("invoices"); + return; + } + + var invoices = new UCInvoices(sourceOrderId); + OpenDocumentTab("Накладные", "invoices", invoices, allowDuplicate: false); + } + private void OpenRefusalsTab() { var refusals = new UCRefusals(); diff --git a/src/ElectronicPharmacy/Form1.cs b/src/ElectronicPharmacy/Form1.cs index da97301..b3b25b9 100644 --- a/src/ElectronicPharmacy/Form1.cs +++ b/src/ElectronicPharmacy/Form1.cs @@ -146,8 +146,7 @@ namespace Электронная_Фармация private void tsItemInvoices_Click(object sender, EventArgs e) { - var ucInvoices = new UCInvoices(); - OpenDocumentTab("Накладные", "invoices", ucInvoices, allowDuplicate: true); + OpenInvoicesTab(); } private void tsItemPriceList_Click(object sender, EventArgs e) diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs index ac68457..1b867fe 100644 --- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs +++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs @@ -393,6 +393,13 @@ create table if not exists [OrderItems] ( cmdCreateNotExistingsTables.ExecuteNonQuery(); TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)"); + TryAddColumn(conForCheckTables, "Invoice", "SupplierName", "nvarchar(256)"); + TryAddColumn(conForCheckTables, "Invoice", "ConsigneesName", "nvarchar(128)"); + TryAddColumn(conForCheckTables, "Invoice", "InvoiceSum", "nvarchar(64)"); + TryAddColumn(conForCheckTables, "Invoice", "RefuseSum", "nvarchar(64)"); + TryAddColumn(conForCheckTables, "Invoice", "SourceOrderId", "integer"); + TryAddColumn(conForCheckTables, "Invoice", "SourceOrderNumber", "nvarchar(36)"); + TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)"); } catch (Exception ex) { diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 062bf0b..44af562 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -1,16 +1,8 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; using System.Data; using System.Data.SQLite; -using System.Drawing; -using System.Linq; using System.Net.Http; -using System.Text; -//using System.Net.Http.json; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Threading.Tasks; using System.Windows.Forms; using Электронная_Фармация.Classes; @@ -25,198 +17,165 @@ namespace Электронная_Фармация.HelpForms InitializeComponent(); } - string login = Settings.Default.stringLogin.ToString(); - string password = Settings.Default.stringPassword.ToString(); - - string connectionStringToLocalDB = AppConfig.SqliteConnectionString; + private readonly string _login = Settings.Default.stringLogin ?? string.Empty; + private readonly string _password = Settings.Default.stringPassword ?? string.Empty; + private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString; private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e) { UiThemeHelper.ApplyToControlTree(this); btnCloseThis.Enabled = false; - rtxtDebug.Visible = false; dataGridView1.Visible = false; + rtxtDebug.Visible = true; + rtxtDebug.ReadOnly = true; var client = new ApiClient(); + AppendStatus($"Сервер: {client.BaseUrl}"); + AppendStatus($"Лог: {AppDebugLog.CurrentLogFilePath}"); + AppDebugLog.Info("Download", $"Старт загрузки прайса. Сервер={client.BaseUrl}"); + try { - if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken)) - { - client.SetToken(Settings.Default.stringToken); - rtxtDebug.Text += "Используется сохранённый токен...\n"; - } - else - { - rtxtDebug.Text += "Выполняю авторизацию...\n"; - var token = await client.LoginAsync(login, password); - Settings.Default.stringToken = token; - Settings.Default.Save(); - rtxtDebug.Text += "Авторизация успешно пройдена\n"; - } + await EnsureAuthenticatedAsync(client); - rtxtDebug.Text += "\nЗагружаю сводный прайс для всех поставщиков..."; - var allSuppliersSummary = await client.GetPriceSummaryAsync( - supplierId: null, - regionId: string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId); - rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.Summary.Count} позиций\n"); + AppendStatus("Загружаю сводный прайс для всех поставщиков..."); + var allSuppliersSummary = await GetPriceSummaryWithRetryAsync(client); + AppendStatus($"Получено {allSuppliersSummary.Summary?.Count ?? 0} позиций"); - DataTable tablePrice = new DataTable(); + var tablePrice = BuildPriceTable(allSuppliersSummary); + SavePriceTable(tablePrice); - tablePrice.Columns.Add("supplier_price_id"); - tablePrice.Columns.Add("guid_es"); - tablePrice.Columns.Add("es_code"); - tablePrice.Columns.Add("supplier_id"); - tablePrice.Columns.Add("DrugName"); - tablePrice.Columns.Add("SupplierName"); - tablePrice.Columns.Add("Price"); - tablePrice.Columns.Add("Quantity"); + DialogResult = DialogResult.OK; + AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); + ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); + } + catch (UnauthorizedAccessException ex) + { + AppendStatus($"Ошибка авторизации: {ex.Message}"); + AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex); + ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); + } + catch (HttpRequestException ex) + { + AppendStatus($"Ошибка сети: {ex.Message}"); + AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex); + ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); + } + catch (Exception ex) + { + AppendStatus($"Ошибка загрузки прайса: {ex.Message}"); + AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex); + ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); + } - tablePrice.Columns.Add(new DataColumn - { - ColumnName = "ExpiryPeriod", - DataType = typeof(string), - AllowDBNull = true - }); + btnCloseThis.Enabled = true; + } - tablePrice.Columns.Add(new DataColumn - { - ColumnName = "Description", - DataType = typeof(string), - AllowDBNull = true - }); + private async Task EnsureAuthenticatedAsync(ApiClient client) + { + if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken)) + { + client.SetToken(Settings.Default.stringToken); + AppendStatus("Используется сохранённый токен..."); + return; + } - //DataColumn colExpiryPeriod = new DataColumn("ExpiryPeriod", typeof(DateTime)); - //DataColumn colDescription = new DataColumn("Description", typeof(string)); + await LoginAndSaveTokenAsync(client); + } - //colExpiryPeriod.AllowDBNull = true; - //colDescription.AllowDBNull = true; + private async Task LoginAndSaveTokenAsync(ApiClient client) + { + if (string.IsNullOrWhiteSpace(_login) || string.IsNullOrWhiteSpace(_password)) + { + throw new UnauthorizedAccessException( + "Не заданы логин и пароль. Выполните авторизацию в меню «Регистрация»."); + } + AppendStatus("Выполняю авторизацию..."); + var token = await client.LoginAsync(_login, _password); + Settings.Default.stringToken = token; + Settings.Default.Save(); + AppendStatus("Авторизация успешно пройдена"); + } - ////tablePrice.Columns.Add(colExpiryPeriod); - //tablePrice.Columns.Add(colDescription); + private async Task GetPriceSummaryWithRetryAsync(ApiClient client) + { + var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId) + ? null + : Settings.Default.RegionId; + try + { + return await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId); + } + catch (UnauthorizedAccessException) + { + AppendStatus("Токен недействителен, повторная авторизация..."); + Settings.Default.stringToken = string.Empty; + Settings.Default.Save(); + client.SetToken(null); + await LoginAndSaveTokenAsync(client); + return await client.GetPriceSummaryAsync(supplierId: null, regionId: regionId); + } + } - tablePrice.Columns.Add("Zakaz"); - tablePrice.Columns.Add("SummaZakaza"); + private static DataTable BuildPriceTable(PriceSummaryResponse allSuppliersSummary) + { + var tablePrice = new DataTable(); + tablePrice.Columns.Add("supplier_price_id"); + tablePrice.Columns.Add("guid_es"); + tablePrice.Columns.Add("es_code"); + tablePrice.Columns.Add("supplier_id"); + tablePrice.Columns.Add("DrugName"); + tablePrice.Columns.Add("SupplierName"); + tablePrice.Columns.Add("Price"); + tablePrice.Columns.Add("Quantity"); + tablePrice.Columns.Add(new DataColumn + { + ColumnName = "ExpiryPeriod", + DataType = typeof(string), + AllowDBNull = true + }); + tablePrice.Columns.Add(new DataColumn + { + ColumnName = "Description", + DataType = typeof(string), + AllowDBNull = true + }); + tablePrice.Columns.Add("Zakaz"); + tablePrice.Columns.Add("SummaZakaza"); + if (allSuppliersSummary?.Summary == null) + { + return tablePrice; + } - #region полная таблица - //tablePrice.Columns.Add("GuidEs"); - //tablePrice.Columns.Add("SupplierId"); - //tablePrice.Columns.Add("SupplierName"); - //tablePrice.Columns.Add("DrugName"); - //tablePrice.Columns.Add("Inn"); - //tablePrice.Columns.Add("CureForm"); - //tablePrice.Columns.Add("Barcode"); - //tablePrice.Columns.Add("Price"); - //tablePrice.Columns.Add("Quantity"); - //tablePrice.Columns.Add("RegionId"); - //tablePrice.Columns.Add("RegionName"); - //tablePrice.Columns.Add("LastPriceDate"); - //tablePrice.Columns.Add("MatchMethod"); - //tablePrice.Columns.Add("MatchConfidence"); - //tablePrice.Columns.Add("TradeName"); - //tablePrice.Columns.Add("Dosage"); - //tablePrice.Columns.Add("RegistryPrice"); - //tablePrice.Columns.Add("InstructionGuid"); - //tablePrice.Columns.Add("Description"); - //tablePrice.Columns.Add("StoringCondition"); - //tablePrice.Columns.Add("ExpiryPeriod"); - //tablePrice.Columns.Add("ProducerName"); - //tablePrice.Columns.Add("RegistryDate"); - //tablePrice.Columns.Add("EsCode"); - //tablePrice.Columns.Add("RegistryStatus"); - #endregion + foreach (var item in allSuppliersSummary.Summary) + { + var row = tablePrice.NewRow(); + row["supplier_price_id"] = item.SupplierPriceID.ToString(); + row["guid_es"] = item.GuidEs.ToString(); + row["es_code"] = item.EsCode.ToString(); + row["supplier_id"] = item.SupplierId.ToString(); + row["DrugName"] = item.DrugName.ToString(); + row["SupplierName"] = item.SupplierName.ToString(); + row["Price"] = item.Price.ToString(); + row["Quantity"] = item.Quantity.ToString(); + row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod.ToString(); + row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description.ToString(); + row["Zakaz"] = string.Empty; + row["SummaZakaza"] = string.Empty; + tablePrice.Rows.Add(row); + } - if (allSuppliersSummary.Summary != null) - { + return tablePrice; + } - foreach (var item in allSuppliersSummary.Summary) - { - - DataRow row = tablePrice.NewRow(); - - row["supplier_price_id"] = item.SupplierPriceID.ToString(); - row["guid_es"] = item.GuidEs.ToString(); - row["es_code"] = item.EsCode.ToString(); - row["supplier_id"] = item.SupplierId.ToString(); - row["DrugName"] = item.DrugName.ToString(); - row["SupplierName"] = item.SupplierName.ToString(); - row["Price"] = item.Price.ToString(); - row["Quantity"] = item.Quantity.ToString(); - - if (item.ExpiryPeriod == null) - { row["ExpiryPeriod"] = DBNull.Value; } - else { row["ExpiryPeriod"] = item.ExpiryPeriod.ToString(); } - ; - - if (item.Description == null) - { row["Description"] = DBNull.Value; } - else { row["Description"] = item.Description.ToString(); } - ; - - row["Zakaz"] = string.Empty; - row["SummaZakaza"] = string.Empty; - - tablePrice.Rows.Add(row); - - // DataSetPriceList.Tables.Add(tablePrice); - // DataSetPriceList.WriteXml("PriceList.xml", XmlWriteMode.WriteSchema); - - - - //SQLiteDataAdapter sqlite_data = new SQLiteDataAdapter(); - //new SQLiteCommandBuilder(sqlite_data); - ////sqlite_data.Fill(tablePrice); - //sqlite_data.Update(tablePrice); - //dataGridView1.DataSource = tablePrice; - - - - #region сводный прайс и статистика - //// Шаг 3: Получение сводного прайса для конкретного поставщика - //if (allSuppliersSummary.Summary.Any()) - //{ - // var firstSupplierId = allSuppliersSummary.Summary - // .FirstOrDefault(s => !string.IsNullOrEmpty(s.SupplierId))?.SupplierId; - - // if (!string.IsNullOrEmpty(firstSupplierId)) - // { - // Console.WriteLine($"\nЗагружаю прайс для поставщика {firstSupplierId}..."); - // var supplierSummary = await client.GetPriceSummaryAsync(supplierId: firstSupplierId); - // Console.WriteLine($"✓ Получено {supplierSummary.Summary.Count} позиций для поставщика\n"); - // } - //} - - //// Шаг 4: Статистика - //Console.WriteLine("\n=== Статистика ==="); - //var withPrice = allSuppliersSummary.Summary.Where(s => s.Price.HasValue).ToList(); - //var withQuantity = allSuppliersSummary.Summary.Where(s => s.Quantity.HasValue).ToList(); - - //Console.WriteLine($"Всего позиций: {allSuppliersSummary.Summary.Count}"); - //Console.WriteLine($"С ценой: {withPrice.Count}"); - //Console.WriteLine($"С количеством: {withQuantity.Count}"); - - - //if (withPrice.Any()) - //{ - // var avgPrice = withPrice.Average(s => s.Price.Value); - // var minPrice = withPrice.Min(s => s.Price.Value); - // var maxPrice = withPrice.Max(s => s.Price.Value); - - // Console.WriteLine($"Средняя цена: {avgPrice:F2} ₽"); - // Console.WriteLine($"Минимальная цена: {minPrice:F2} ₽"); - // Console.WriteLine($"Максимальная цена: {maxPrice:F2} ₽"); - //} - #endregion - } - } - - string commandToDelete = "delete from [PriceList]"; - string commandTODeleteTempOrder = "delete from [TempOrderItems]"; - - - string commandToInsert = @"INSERT INTO [PriceList] ( + private void SavePriceTable(DataTable tablePrice) + { + const string commandToDelete = "delete from [PriceList]"; + const string commandToDeleteTempOrder = "delete from [TempOrderItems]"; + const string commandToInsert = @"INSERT INTO [PriceList] ( [guid_es], [es_code], [supplier_id], @@ -244,73 +203,70 @@ VALUES @Zakaz, @SummaZakaza, @supplier_price_id -) -"; +)"; - using (SQLiteConnection sqliteCon = new SQLiteConnection(connectionStringToLocalDB)) + using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb)) + { + sqliteCon.Open(); + + using (var cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon)) + using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon)) + { + cmdDeleteBeforeInsert.ExecuteNonQuery(); + cmdDeleteTempOrder.ExecuteNonQuery(); + } + + using (var transaction = sqliteCon.BeginTransaction()) + { + foreach (DataRow row in tablePrice.Rows) + { + using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) { - sqliteCon.Open(); - - SQLiteCommand cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon); - SQLiteCommand cmdDeleteTempOrder = new SQLiteCommand(commandTODeleteTempOrder, sqliteCon); - - cmdDeleteBeforeInsert.ExecuteNonQuery(); - cmdDeleteTempOrder.ExecuteNonQuery(); - - using (var transaction = sqliteCon.BeginTransaction()) - { - foreach (DataRow row in tablePrice.Rows) - { - using (SQLiteCommand cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction)) - { - cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]); - cmd.Parameters.AddWithValue("@es_code", row["es_code"]); - cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]); - cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]); - cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]); - cmd.Parameters.AddWithValue("@Price", row["Price"]); - cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]); - cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]); - cmd.Parameters.AddWithValue("@Description", row["Description"]); - cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]); - cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]); - cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]); - cmd.ExecuteNonQuery(); - } - } - transaction.Commit(); - } - sqliteCon.Close(); + cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]); + cmd.Parameters.AddWithValue("@es_code", row["es_code"]); + cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]); + cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]); + cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]); + cmd.Parameters.AddWithValue("@Price", row["Price"]); + cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]); + cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]); + cmd.Parameters.AddWithValue("@Description", row["Description"]); + cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]); + cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]); + cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]); + cmd.ExecuteNonQuery(); } + } - DialogResult = DialogResult.OK; - ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); + transaction.Commit(); + } } - catch (UnauthorizedAccessException ex) + } + + private void AppendStatus(string message) + { + if (InvokeRequired) { - ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); - } - catch (HttpRequestException ex) - { - ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); - } - catch (Exception ex) - { - ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}"); + BeginInvoke(new Action(AppendStatus), message); + return; } - btnCloseThis.Enabled = true; + rtxtDebug.AppendText(message + Environment.NewLine); + rtxtDebug.SelectionStart = rtxtDebug.TextLength; + rtxtDebug.ScrollToCaret(); + rtxtDebug.Refresh(); + AppDebugLog.Info("Download", message); } private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e) { - this.Dispose(); - this.Close(); + Dispose(); + Close(); } private void btnCloseThis_Click(object sender, EventArgs e) { - this.Close(); + Close(); } } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs index bbd23c1..924ba2f 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_Registration.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_Registration.cs @@ -56,7 +56,8 @@ namespace Электронная_Фармация.HelpForms loginRequest.Username = strLogin.Trim(); loginRequest.Password = strPassword.Trim(); - var client = new ApiClient(apiUrl); + var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl)); + AppDebugLog.Info("Auth", $"Попытка авторизации. Сервер={client.BaseUrl}, login={loginRequest.Username}"); try { @@ -66,9 +67,10 @@ namespace Электронная_Фармация.HelpForms Settings.Default.stringPassword = strPassword; Settings.Default.stringToken = token; Settings.Default.LocationId = txtLocationId.Text.Trim(); - Settings.Default.ApiBaseUrl = txtApiUrl.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); @@ -79,13 +81,13 @@ namespace Электронная_Фармация.HelpForms } catch (UnauthorizedAccessException ex) { + AppDebugLog.Error("Auth", "Ошибка авторизации", ex); ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}"); - //MessageBox.Show($"Ошибка авторизации: {ex.Message}"); } catch (HttpRequestException ex) { + AppDebugLog.Error("Auth", "Ошибка сети при авторизации", ex); ToastNotification.ShowError($"Ошибка сети: {ex.Message}"); - //MessageBox.Show($"Ошибка сети: {ex.Message}"); } } else diff --git a/src/ElectronicPharmacy/Program.cs b/src/ElectronicPharmacy/Program.cs index 4dcc9e3..a6db27c 100644 --- a/src/ElectronicPharmacy/Program.cs +++ b/src/ElectronicPharmacy/Program.cs @@ -1,5 +1,7 @@ using System; using System.Windows.Forms; +using System.Net; +using Электронная_Фармация.Classes; namespace Электронная_Фармация { @@ -8,6 +10,9 @@ namespace Электронная_Фармация [STAThread] static void Main() { + ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + AppConfig.MigrateLegacySettings(); + AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}"); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new ElectroPharmacy()); diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs index e0ea4f7..c582df8 100644 --- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs +++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs @@ -85,7 +85,7 @@ namespace Электронная_Фармация.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("http://195.34.241.84:9988")] + [global::System.Configuration.DefaultSettingValueAttribute("https://24pharmdata.ru")] public string ApiBaseUrl { get { return ((string)(this["ApiBaseUrl"])); diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings index d747674..b2c624d 100644 --- a/src/ElectronicPharmacy/Properties/Settings.settings +++ b/src/ElectronicPharmacy/Properties/Settings.settings @@ -18,7 +18,7 @@ - http://195.34.241.84:9988 + https://24pharmdata.ru diff --git a/src/ElectronicPharmacy/UserControls/FOrders.cs b/src/ElectronicPharmacy/UserControls/FOrders.cs index 2ffea97..de59de3 100644 --- a/src/ElectronicPharmacy/UserControls/FOrders.cs +++ b/src/ElectronicPharmacy/UserControls/FOrders.cs @@ -46,13 +46,54 @@ namespace Электронная_Фармация.UserControls ApplyToolbarLayout(); ModernButtonStyles.ApplyClear(btnClearGoodSearch); ModernButtonStyles.ApplyClear(btnClearOrderNumberFilter); + InitializeOrderContextMenu(); label3.BringToFront(); panelToolbar.Resize += (_, __) => ApplyToolbarLayout(); } + private void InitializeOrderContextMenu() + { + if (_ordersMenu != null) + { + return; + } + + _getInvoiceMenuItem = new ToolStripMenuItem("Получить накладную"); + _getInvoiceMenuItem.Click += (_, __) => RequestInvoiceForSelectedOrder(); + + _ordersMenu = new ContextMenuStrip(); + _ordersMenu.Items.Add(_getInvoiceMenuItem); + _ordersMenu.Opening += OrdersMenu_Opening; + + DGVOrders.ContextMenuStrip = _ordersMenu; + } + + private void OrdersMenu_Opening(object sender, CancelEventArgs e) + { + var pt = DGVOrders.PointToClient(Cursor.Position); + var hit = DGVOrders.HitTest(pt.X, pt.Y); + if (hit.RowIndex < 0 || hit.RowIndex >= DGVOrders.Rows.Count) + { + e.Cancel = true; + return; + } + + DGVOrders.ClearSelection(); + DGVOrders.Rows[hit.RowIndex].Selected = true; + if (hit.ColumnIndex >= 0) + { + DGVOrders.CurrentCell = DGVOrders.Rows[hit.RowIndex].Cells[hit.ColumnIndex]; + } + + var status = DGVOrders.Rows[hit.RowIndex].Cells["Статус"]?.Value?.ToString(); + _getInvoiceMenuItem.Enabled = string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase); + } + private ModernComboBox _comboOrderStatus; private ModernComboBox _comboConsignees; private ModernButton _btnSendOrder; + private ContextMenuStrip _ordersMenu; + private ToolStripMenuItem _getInvoiceMenuItem; private void InitOrderToolbar() { @@ -319,6 +360,44 @@ namespace Электронная_Фармация.UserControls ApplyOrderFilters(); } + private void RequestInvoiceForSelectedOrder() + { + if (DGVOrders.SelectedRows.Count == 0) + { + ToastNotification.ShowCustom("Выберите отправленный заказ.", Color.DarkOrange, Color.White); + return; + } + + var selectedRow = DGVOrders.SelectedRows[0]; + var status = selectedRow.Cells["Статус"]?.Value?.ToString(); + if (!string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase)) + { + ToastNotification.ShowCustom("Накладную можно получить только для отправленного заказа.", Color.DarkOrange, Color.White); + return; + } + + var orderId = selectedRow.Cells["ИД заказа"]?.Value?.ToString(); + if (string.IsNullOrWhiteSpace(orderId)) + { + ToastNotification.ShowError("Не удалось определить идентификатор заказа."); + return; + } + + try + { + var service = new InvoiceRequestService(); + service.CreateOrUpdateFromOrder(orderId); + ToastNotification.ShowSuccess("Накладная добавлена в раздел \"Накладные\"."); + + var shell = FindForm() as ElectroPharmacy; + shell?.OpenInvoicesTab(orderId); + } + catch (Exception ex) + { + ToastNotification.ShowError($"Не удалось получить накладную: {ex.Message}"); + } + } + void LoadOrders(string dateFrom, string dateTo, string filters) { string connectionStringToLocalDB = AppConfig.SqliteConnectionString; diff --git a/src/ElectronicPharmacy/UserControls/UCInvoices.cs b/src/ElectronicPharmacy/UserControls/UCInvoices.cs index 9a2b20c..8d2a801 100644 --- a/src/ElectronicPharmacy/UserControls/UCInvoices.cs +++ b/src/ElectronicPharmacy/UserControls/UCInvoices.cs @@ -15,9 +15,28 @@ namespace Электронная_Фармация.UserControls { public partial class UCInvoices : UserControl { + private string _initialSourceOrderId; + private ContextMenuStrip _invoiceMenu; + private ToolStripMenuItem _exportInvoiceMenuItem; + public UCInvoices() + : this(null) + { + } + + public UCInvoices(string sourceOrderId) { InitializeComponent(); + _initialSourceOrderId = sourceOrderId; + } + + public void SetSourceOrderFilter(string sourceOrderId) + { + _initialSourceOrderId = sourceOrderId; + if (IsHandleCreated) + { + getDatesFromCalendar(); + } } void getDatesFromCalendar() @@ -94,52 +113,80 @@ namespace Электронная_Фармация.UserControls void loadInvoicesList(string InvoicesDateFrom, string InvoicesDateTo, string dateFromYearFormat) { - string connectionStringToLocalDB = AppConfig.SqliteConnectionString; - string filterAddons = $"where i.InvoiceDate between date('{InvoicesDateFrom}') and date('{InvoicesDateTo}')"; - - if (txtInvoiceNumber.Text != "" & txtInvoiceNumber.Text != "Номер накладной") - { - filterAddons += $" and i.InvoiceNumber like '%{txtInvoiceNumber.Text}%'"; - } - - if (comboSuppliers.Text != "" & comboSuppliers.Text != "Поставщик") - { - filterAddons += $" and s.SuppliersName like '%{comboSuppliers.Text}%'"; - } - - if (comboConsignees.Text != "" & comboConsignees.Text != "Грузополучатель") - { - filterAddons += $" and c.ConsigneesName like '%{comboConsignees.Text}%'"; - } - - string qShowMeListOfInvoices = $@" -select i.InvoiceNumber as [Номер], -i.invoiceDate as [Дата], -s.SuppliersName as [Поставщик], -c.ConsigneesName as [Грузополучатель], -c.ConsigneesAddress as [Адрес ГП], -i.SumWithoutNDS as [Сумма без НДС], -i.SumWithNDS as [Сумма с НДС], -i.SumNDS as [Сумма НДС] -from [Invoice] i -inner join [Suppliers] s on s.codeSuppliers = i.CodeSupplier -inner join [Consignees] c on c.codeConsignees = i.CodeConsignees -{filterAddons} -order by i.InvoiceDate asc -"; - using (SQLiteConnection conInvoices = new SQLiteConnection(connectionStringToLocalDB)) { try { conInvoices.Open(); - SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(qShowMeListOfInvoices, conInvoices); + var sql = @" +select + i.[idInvoice] as [ИД накладной], + i.[InvoiceNumber] as [Номер], + i.[InvoiceDate] as [Дата], + coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) as [Поставщик], + coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) as [Грузополучатель], + i.[SourceOrderNumber] as [Номер заказа], + coalesce(i.[InvoiceSum], i.[SumWithNDS], i.[SumWithoutNDS], 0) as [Сумма], + coalesce(i.[RefuseSum], 0) as [Сумма отказа] +from [Invoice] i +where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)"; + + if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной") + { + sql += " and i.[InvoiceNumber] like @invoiceNumber"; + } + + if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик") + { + sql += " and coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) like @supplierName"; + } + + if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель") + { + sql += " and coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) like @consigneeName"; + } + + if (!string.IsNullOrWhiteSpace(_initialSourceOrderId)) + { + sql += " and cast(i.[SourceOrderId] as text) = @sourceOrderId"; + } + + sql += " order by date(i.[InvoiceDate]) desc, i.[idInvoice] desc;"; + + SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(sql, conInvoices); + cmdShowInvoicesList.Parameters.AddWithValue("@dateFrom", InvoicesDateFrom); + cmdShowInvoicesList.Parameters.AddWithValue("@dateTo", InvoicesDateTo); + if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной") + { + cmdShowInvoicesList.Parameters.AddWithValue("@invoiceNumber", $"%{txtInvoiceNumber.Text}%"); + } + + if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик") + { + cmdShowInvoicesList.Parameters.AddWithValue("@supplierName", $"%{comboSuppliers.Text}%"); + } + + if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель") + { + cmdShowInvoicesList.Parameters.AddWithValue("@consigneeName", $"%{comboConsignees.Text}%"); + } + + if (!string.IsNullOrWhiteSpace(_initialSourceOrderId)) + { + cmdShowInvoicesList.Parameters.AddWithValue("@sourceOrderId", _initialSourceOrderId); + } + DataTable tableInvoices = new DataTable(); SQLiteDataAdapter daInvoicesList = new SQLiteDataAdapter(cmdShowInvoicesList); daInvoicesList.Fill(tableInvoices); dgvInvoice.DataSource = tableInvoices; + + if (dgvInvoice.Columns.Count > 0) + { + dgvInvoice.Columns["ИД накладной"].Visible = false; + } } catch (Exception ex) { @@ -162,6 +209,7 @@ order by i.InvoiceDate asc { UiThemeHelper.ApplyToControlTree(this); Font = new Font("Segoe UI", 9.75f, FontStyle.Regular); + InitializeInvoiceContextMenu(); button1.Visible = true; button1.Text = "Обновить с сервера"; @@ -196,6 +244,43 @@ order by i.InvoiceDate asc } } + private void InitializeInvoiceContextMenu() + { + if (_invoiceMenu != null) + { + return; + } + + _exportInvoiceMenuItem = new ToolStripMenuItem("Экспорт"); + _exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice(); + + _invoiceMenu = new ContextMenuStrip(); + _invoiceMenu.Items.Add(_exportInvoiceMenuItem); + _invoiceMenu.Opening += InvoiceMenu_Opening; + + dgvInvoice.ContextMenuStrip = _invoiceMenu; + } + + private void InvoiceMenu_Opening(object sender, CancelEventArgs e) + { + var pt = dgvInvoice.PointToClient(Cursor.Position); + var hit = dgvInvoice.HitTest(pt.X, pt.Y); + if (hit.RowIndex < 0 || hit.RowIndex >= dgvInvoice.Rows.Count) + { + e.Cancel = true; + return; + } + + dgvInvoice.ClearSelection(); + dgvInvoice.Rows[hit.RowIndex].Selected = true; + if (hit.ColumnIndex >= 0) + { + dgvInvoice.CurrentCell = dgvInvoice.Rows[hit.RowIndex].Cells[hit.ColumnIndex]; + } + + _exportInvoiceMenuItem.Enabled = true; + } + private void dtpDateTo_Leave(object sender, EventArgs e) { @@ -226,28 +311,22 @@ order by i.InvoiceDate asc void selectInvoice() { - if (dgvInvoice.Rows.Count > 0) + if (dgvInvoice.Rows.Count > 0 && dgvInvoice.CurrentCell != null) { int SelectedInvoiceIndex = dgvInvoice.CurrentCell.RowIndex; - string SelectedInvoiceNumber = dgvInvoice.Rows[SelectedInvoiceIndex].Cells[0].Value.ToString(); - loadInvoiceItems(SelectedInvoiceNumber); + string SelectedInvoiceId = dgvInvoice.Rows[SelectedInvoiceIndex].Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty; + loadInvoiceItems(SelectedInvoiceId); } } - void loadInvoiceItems(string InvoiceNumber) + void loadInvoiceItems(string invoiceId) { - string filterAddons = string.Empty; - - - string InvNum = InvoiceNumber; if (dgvInvoice.Rows.Count > 0) { - if (InvNum != "" | InvoiceNumber != string.Empty) + if (!string.IsNullOrWhiteSpace(invoiceId)) { - filterAddons = $"where i.InvoiceNumber = '{InvNum}'"; - string connectionStringToLocalDB = AppConfig.SqliteConnectionString; - string qShowMeInvoiceItems = $@" + string qShowMeInvoiceItems = @" select ii.GoodCode as [Код товара], ii.Good as [Товар], @@ -270,20 +349,22 @@ ii.Sertificate as [Сертификат], ii.Marked as [Признак маркировки], ii.GTIN as [GTIN] from InvoiceItem ii -inner join Invoice i on i.idInvoice = ii.idInvoice -{filterAddons} -"; +where ii.idInvoice = @invoiceId;"; using (SQLiteConnection conInvoiceItems = new SQLiteConnection(connectionStringToLocalDB)) { try { conInvoiceItems.Open(); SQLiteCommand cmdShowMeInvoiceItems = new SQLiteCommand(qShowMeInvoiceItems, conInvoiceItems); + cmdShowMeInvoiceItems.Parameters.AddWithValue("@invoiceId", invoiceId); DataTable tableInvoiceItems = new DataTable(); SQLiteDataAdapter daInvoiceItems = new SQLiteDataAdapter(cmdShowMeInvoiceItems); daInvoiceItems.Fill(tableInvoiceItems); dgvInvoiceItems.DataSource = tableInvoiceItems; - dgvInvoiceItems.Columns[10].ValueType = typeof(bool); + if (dgvInvoiceItems.Columns.Count > 10) + { + dgvInvoiceItems.Columns[10].ValueType = typeof(bool); + } } catch (Exception ex) { @@ -303,6 +384,21 @@ inner join Invoice i on i.idInvoice = ii.idInvoice } } + private void ExportSelectedInvoice() + { + if (dgvInvoice.SelectedRows.Count == 0) + { + ToastNotification.ShowCustom("Выберите накладную для экспорта.", Color.DarkOrange, Color.White); + return; + } + + var invoiceNumber = dgvInvoice.SelectedRows[0].Cells["Номер"]?.Value?.ToString() ?? string.Empty; + ToastNotification.ShowCustom( + $"Экспорт накладной {invoiceNumber} будет добавлен после согласования формата.", + Color.SteelBlue, + Color.White); + } + private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Enter) diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 4a7b5b4..a34dafb 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -824,15 +824,46 @@ and tempOrder.GoodName = PriceList.GoodName private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e) { - int RowIndex = dgvPriceList.SelectedRows[0].Index; + int RowIndex = -1; + string idPriceListItem = string.Empty; - string idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value.ToString(); + if (dgvPriceList.SelectedRows.Count > 0) + { + RowIndex = dgvPriceList.SelectedRows[0].Index; + if (RowIndex >= 0 && RowIndex < dgvPriceList.Rows.Count) + { + idPriceListItem = dgvPriceList.Rows[RowIndex].Cells[11].Value?.ToString() ?? string.Empty; + } + } + + if (e.KeyChar == (Char)Keys.Back && GoodsFilter.Length > 0) + { + GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); + txtSearchGoodNameInPriceList.Text = GoodsFilter; + ApplyPriceListFilters(); + + if (GoodsFilter.Length == 0) + { + timerForResetSearch.Enabled = false; + timerForResetSearch.Stop(); + } + else + { + timerForResetSearch.Enabled = true; + timerForResetSearch.Start(); + } + + return; + } if (timerForResetSearch.Enabled == false) { - deleteGood(RowIndex, idPriceListItem); - timerForResetSearch.Enabled = true; - timerForResetSearch.Start(); + 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)) @@ -874,6 +905,7 @@ and tempOrder.GoodName = PriceList.GoodName } #endregion } + return; } else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) || (e.KeyChar >= (Char)Keys.NumPad1 && e.KeyChar <= (Char)Keys.NumPad9) || (e.KeyChar == (Char)Keys.D0 && dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString().Length > 1) || (e.KeyChar == (Char)Keys.NumPad0 && dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString().Length > 1)) @@ -923,9 +955,11 @@ and tempOrder.GoodName = PriceList.GoodName txtSearchGoodNameInPriceList.Text = string.Empty; GoodsFilter = string.Empty; ApplyPriceListFilters(); + return; } else if (e.KeyChar == (Char)Keys.Back) { + if (RowIndex < 0) return; string countToBuy = dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString(); if (countToBuy.Length > 1) @@ -945,14 +979,16 @@ and tempOrder.GoodName = PriceList.GoodName } else if (e.KeyChar == (Char)Keys.Delete) { + if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return; deleteGood(RowIndex, idPriceListItem); } - else if (dgvPriceList.Rows[RowIndex].Cells[9].Value.ToString() == string.Empty && e.KeyChar == (Char)Keys.D0) + else if (RowIndex >= 0 && dgvPriceList.Rows[RowIndex].Cells[9].Value?.ToString() == string.Empty && e.KeyChar == (Char)Keys.D0) { } else if (e.KeyChar == (Char)Keys.Down || e.KeyChar == (Char)Keys.Up) { + if (RowIndex < 0 || string.IsNullOrEmpty(idPriceListItem)) return; timerForResetSearch.Enabled = false; timerForResetSearch.Stop(); @@ -966,7 +1002,9 @@ and tempOrder.GoodName = PriceList.GoodName } } - string summaZakaza = dgvPriceList.Rows[RowIndex].Cells[10].Value.ToString(); + if (RowIndex < 0 || RowIndex >= dgvPriceList.Rows.Count) return; + + string summaZakaza = dgvPriceList.Rows[RowIndex].Cells[10].Value?.ToString() ?? string.Empty; if (summaZakaza.Trim().Length > 0) @@ -1662,6 +1700,8 @@ and SumOrderedItems = '{SumOrder}'"; private void timerForResetSearch_Tick(object sender, EventArgs e) { txtSearchGoodNameInPriceList.Text = string.Empty; + GoodsFilter = string.Empty; + ApplyPriceListFilters(); timerForResetSearch.Enabled = false; timerForResetSearch.Stop(); }