Compare commits
No commits in common. "ui/clinical-redesign" and "v1.0.24" have entirely different histories.
ui/clinica
...
v1.0.24
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -29,9 +29,6 @@
|
|||||||
<setting name="IntDefaultPercentMarkup" serializeAs="String">
|
<setting name="IntDefaultPercentMarkup" serializeAs="String">
|
||||||
<value>0</value>
|
<value>0</value>
|
||||||
</setting>
|
</setting>
|
||||||
<setting name="ClientMarkupPercent" serializeAs="String">
|
|
||||||
<value>0</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="stringLogin" serializeAs="String">
|
<setting name="stringLogin" serializeAs="String">
|
||||||
<value />
|
<value />
|
||||||
</setting>
|
</setting>
|
||||||
@ -41,21 +38,6 @@
|
|||||||
<setting name="stringToken" serializeAs="String">
|
<setting name="stringToken" serializeAs="String">
|
||||||
<value />
|
<value />
|
||||||
</setting>
|
</setting>
|
||||||
<setting name="InvoiceExportPath" serializeAs="String">
|
|
||||||
<value />
|
|
||||||
</setting>
|
|
||||||
<setting name="UpdateCheckBaseUrl" serializeAs="String">
|
|
||||||
<value>https://git.24pharmdata.ru</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="UpdateRepo" serializeAs="String">
|
|
||||||
<value>pharmdata/elfisa-pharmacy</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="UpdateApiToken" serializeAs="String">
|
|
||||||
<value />
|
|
||||||
</setting>
|
|
||||||
<setting name="UpgradeSettings" serializeAs="String">
|
|
||||||
<value>True</value>
|
|
||||||
</setting>
|
|
||||||
</Электронная_Фармация.Properties.Settings>
|
</Электронная_Фармация.Properties.Settings>
|
||||||
</userSettings>
|
</userSettings>
|
||||||
<runtime>
|
<runtime>
|
||||||
|
|||||||
Binary file not shown.
@ -374,84 +374,6 @@ namespace Электронная_Фармация.Classes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отчёт покупателя: позиции его заказов за период. Сервер скоупит по JWT покупателя.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<List<BuyerReportLineDto>> GetBuyerReportAsync(DateTime from, DateTime to)
|
|
||||||
{
|
|
||||||
EnsureAuthenticated();
|
|
||||||
var path = $"/api/buyer/report?date_from={from:yyyy-MM-dd}&date_to={to:yyyy-MM-dd}";
|
|
||||||
var responseBody = await SendAuthorizedGetAsync(path, "формирование отчёта");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = JsonSerializer.Deserialize<BuyerReportResponse>(responseBody, JsonOptions);
|
|
||||||
return response?.Lines ?? new List<BuyerReportLineDto>();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать отчёт", ex);
|
|
||||||
throw new HttpRequestException($"Некорректный ответ сервера при формировании отчёта: {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Отправляет свободный запрос («промт») ИИ-сервису и получает готовый
|
|
||||||
/// документ (Word/Excel). Токен покупателя уходит в теле — сервис ходит на
|
|
||||||
/// платформу от его имени и видит ТОЛЬКО его данные.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<AiReportResult> GenerateAiReportAsync(string prompt, string format = "docx")
|
|
||||||
{
|
|
||||||
EnsureAuthenticated();
|
|
||||||
if (string.IsNullOrWhiteSpace(prompt))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Пустой запрос для ИИ.", nameof(prompt));
|
|
||||||
}
|
|
||||||
|
|
||||||
var aiBase = AppConfig.AiBaseUrl.TrimEnd('/');
|
|
||||||
var fullUrl = new Uri(new Uri(aiBase + "/"), "generate").ToString();
|
|
||||||
var payload = new AiReportRequest { Prompt = prompt, Token = _token, Format = format };
|
|
||||||
var requestJson = JsonSerializer.Serialize(payload, JsonOptions);
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
AppDebugLog.ApiRequest("POST", fullUrl, extra: $"ai prompt len={prompt.Length}, format={format}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl))
|
|
||||||
{
|
|
||||||
httpRequest.Content = new StringContent(requestJson, Encoding.UTF8, "application/json");
|
|
||||||
var response = await _httpClient.SendAsync(httpRequest);
|
|
||||||
stopwatch.Stop();
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var errBody = await response.Content.ReadAsStringAsync();
|
|
||||||
AppDebugLog.ApiHttpError("POST", fullUrl, (int)response.StatusCode, errBody, "ai-report");
|
|
||||||
var detail = TryDeserializeError(errBody)?.Error ?? errBody;
|
|
||||||
throw new HttpRequestException($"ИИ-сервис вернул {(int)response.StatusCode}: {detail}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
|
||||||
var fileName = response.Content.Headers.ContentDisposition?.FileNameStar
|
|
||||||
?? response.Content.Headers.ContentDisposition?.FileName
|
|
||||||
?? (format == "xlsx" ? "Отчёт.xlsx" : "Отчёт.docx");
|
|
||||||
fileName = fileName.Trim('"');
|
|
||||||
AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds,
|
|
||||||
$"ai file={fileName}, {bytes.Length} bytes");
|
|
||||||
return new AiReportResult { Content = bytes, FileName = fileName };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
stopwatch.Stop();
|
|
||||||
throw WrapNetworkError(ex, "формирование ИИ-отчёта");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
|
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
|
||||||
{
|
{
|
||||||
EnsureAuthenticated();
|
EnsureAuthenticated();
|
||||||
@ -836,66 +758,6 @@ namespace Электронная_Фармация.Classes
|
|||||||
public List<BuyerOrderListItemDto> Orders { get; set; }
|
public List<BuyerOrderListItemDto> Orders { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AiReportRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("prompt")]
|
|
||||||
public string Prompt { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("token")]
|
|
||||||
public string Token { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("format")]
|
|
||||||
public string Format { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class AiReportResult
|
|
||||||
{
|
|
||||||
public byte[] Content { get; set; }
|
|
||||||
public string FileName { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerReportResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("lines")]
|
|
||||||
public List<BuyerReportLineDto> Lines { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerReportLineDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("order_id")]
|
|
||||||
public string OrderID { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("global_sign")]
|
|
||||||
public string GlobalSign { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("order_date")]
|
|
||||||
public DateTime? OrderDate { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string Status { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("supplier")]
|
|
||||||
public string Supplier { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("location")]
|
|
||||||
public string Location { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("item_name")]
|
|
||||||
public string ItemName { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("item_code")]
|
|
||||||
public string ItemCode { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("qty")]
|
|
||||||
public decimal Qty { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("unit_price")]
|
|
||||||
public decimal UnitPrice { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("sum")]
|
|
||||||
public decimal Sum { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class BuyerLocationDto
|
public sealed class BuyerLocationDto
|
||||||
{
|
{
|
||||||
[JsonPropertyName("buyer_location_id")]
|
[JsonPropertyName("buyer_location_id")]
|
||||||
|
|||||||
@ -17,10 +17,6 @@ namespace Электронная_Фармация.Classes
|
|||||||
public const string DefaultInstallerDownloadUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
|
public const string DefaultInstallerDownloadUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
|
||||||
private const string LegacyApiBaseUrl = "http://195.34.241.84:9988";
|
private const string LegacyApiBaseUrl = "http://195.34.241.84:9988";
|
||||||
private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL";
|
private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL";
|
||||||
/// <summary>ИИ-сервис отчётов (проксируется Caddy на Мак-мини). Можно переопределить
|
|
||||||
/// переменной ELF_AI_BASE_URL, напр. http://192.168.95.116:8808 для теста по локальной сети.</summary>
|
|
||||||
public const string DefaultAiBaseUrl = "https://24pharmdata.ru/ai";
|
|
||||||
private const string AiBaseUrlEnvVar = "ELF_AI_BASE_URL";
|
|
||||||
private static bool _sqliteInteropPrepared;
|
private static bool _sqliteInteropPrepared;
|
||||||
private static readonly object _sqliteInteropLock = new object();
|
private static readonly object _sqliteInteropLock = new object();
|
||||||
|
|
||||||
@ -115,21 +111,6 @@ namespace Электронная_Фармация.Classes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Базовый URL ИИ-сервиса отчётов.</summary>
|
|
||||||
public static string AiBaseUrl
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var fromEnv = Environment.GetEnvironmentVariable(AiBaseUrlEnvVar);
|
|
||||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
|
||||||
{
|
|
||||||
return fromEnv.Trim().TrimEnd('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultAiBaseUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string NormalizeApiBaseUrl(string url)
|
public static string NormalizeApiBaseUrl(string url)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
|||||||
@ -74,24 +74,6 @@ namespace Электронная_Фармация.Classes
|
|||||||
throw new InvalidOperationException("В архиве не найден exe приложения.");
|
throw new InvalidOperationException("В архиве не найден exe приложения.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Переносим payload в путь без спецсимволов: имя корневой папки в архиве
|
|
||||||
// бывает в чужой кодировке, и такой путь ломает аргумент SRC у robocopy
|
|
||||||
// (особенно при запуске под elevated cmd с OEM-кодовой страницей).
|
|
||||||
var asciiPayload = Path.Combine(workRoot, "payload");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Directory.Exists(asciiPayload))
|
|
||||||
{
|
|
||||||
Directory.Delete(asciiPayload, true);
|
|
||||||
}
|
|
||||||
Directory.Move(payloadDir, asciiPayload);
|
|
||||||
payloadDir = asciiPayload;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Если перенос не удался — работаем с исходным путём.
|
|
||||||
}
|
|
||||||
|
|
||||||
var needsElevation = !IsDirectoryWritable(installDir);
|
var needsElevation = !IsDirectoryWritable(installDir);
|
||||||
var updaterBat = Path.Combine(workRoot, "apply-update.cmd");
|
var updaterBat = Path.Combine(workRoot, "apply-update.cmd");
|
||||||
WriteUpdaterScript(updaterBat);
|
WriteUpdaterScript(updaterBat);
|
||||||
@ -100,12 +82,12 @@ namespace Электронная_Фармация.Classes
|
|||||||
? "Нужны права администратора для установки в Program Files..."
|
? "Нужны права администратора для установки в Program Files..."
|
||||||
: "Подготовка перезапуска...");
|
: "Подготовка перезапуска...");
|
||||||
|
|
||||||
var batchArgs = Quote(payloadDir) + " " + Quote(installDir) + " " +
|
|
||||||
Quote(Path.GetFileName(currentExe)) + " " +
|
|
||||||
Process.GetCurrentProcess().Id;
|
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
|
FileName = updaterBat,
|
||||||
|
Arguments = Quote(payloadDir) + " " + Quote(installDir) + " " +
|
||||||
|
Quote(Path.GetFileName(currentExe)) + " " +
|
||||||
|
Process.GetCurrentProcess().Id,
|
||||||
UseShellExecute = true,
|
UseShellExecute = true,
|
||||||
WorkingDirectory = workRoot,
|
WorkingDirectory = workRoot,
|
||||||
WindowStyle = needsElevation ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
|
WindowStyle = needsElevation ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
|
||||||
@ -113,19 +95,9 @@ namespace Электронная_Фармация.Classes
|
|||||||
};
|
};
|
||||||
if (needsElevation)
|
if (needsElevation)
|
||||||
{
|
{
|
||||||
// ВАЖНО: повышаем права через cmd.exe (PE-исполняемый), а НЕ через
|
// UAC prompt — без этого robocopy в Program Files молча не заменяет exe.
|
||||||
// .cmd-файл напрямую. ShellExecute с verb=runas на .cmd срабатывает не
|
|
||||||
// на всех системах (у batch может не быть runas-ассоциации), из-за чего
|
|
||||||
// elevated-процесс молча не запускается и обновление не применяется.
|
|
||||||
psi.FileName = "cmd.exe";
|
|
||||||
psi.Arguments = "/c \"\"" + updaterBat + "\" " + batchArgs + "\"";
|
|
||||||
psi.Verb = "runas";
|
psi.Verb = "runas";
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
psi.FileName = updaterBat;
|
|
||||||
psi.Arguments = batchArgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -248,9 +248,6 @@
|
|||||||
<Compile Include="UserControls\UCRefusals.cs">
|
<Compile Include="UserControls\UCRefusals.cs">
|
||||||
<SubType>UserControl</SubType>
|
<SubType>UserControl</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="UserControls\UCReports.cs">
|
|
||||||
<SubType>UserControl</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="UserControls\UCPriceList.cs">
|
<Compile Include="UserControls\UCPriceList.cs">
|
||||||
<SubType>UserControl</SubType>
|
<SubType>UserControl</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|||||||
@ -61,7 +61,6 @@ namespace Электронная_Фармация
|
|||||||
new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" },
|
new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" },
|
||||||
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" },
|
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" },
|
||||||
new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" },
|
new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" },
|
||||||
new SidebarItem { Key = "reports", Text = "Отчёты", IconGlyph = "" },
|
|
||||||
new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" },
|
new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" },
|
||||||
new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" },
|
new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" },
|
||||||
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" },
|
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" },
|
||||||
@ -322,9 +321,6 @@ namespace Электронная_Фармация
|
|||||||
case "invoices":
|
case "invoices":
|
||||||
OpenInvoicesTab();
|
OpenInvoicesTab();
|
||||||
break;
|
break;
|
||||||
case "reports":
|
|
||||||
OpenReportsTab();
|
|
||||||
break;
|
|
||||||
case "upload":
|
case "upload":
|
||||||
tsBtnDownloadData_Click(this, EventArgs.Empty);
|
tsBtnDownloadData_Click(this, EventArgs.Empty);
|
||||||
_appHeader.SetActive("upload");
|
_appHeader.SetActive("upload");
|
||||||
@ -503,12 +499,6 @@ namespace Электронная_Фармация
|
|||||||
OpenDocumentTab("Отказы", "refusals", refusals, allowDuplicate: false);
|
OpenDocumentTab("Отказы", "refusals", refusals, allowDuplicate: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenReportsTab()
|
|
||||||
{
|
|
||||||
var reports = new UCReports();
|
|
||||||
OpenDocumentTab("Отчёты", "reports", reports, allowDuplicate: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void ShowShellLoading(string message)
|
partial void ShowShellLoading(string message)
|
||||||
{
|
{
|
||||||
_loadingOverlay?.Show(message);
|
_loadingOverlay?.Show(message);
|
||||||
|
|||||||
@ -32,6 +32,6 @@ using System.Runtime.InteropServices;
|
|||||||
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
|
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.29.0")]
|
[assembly: AssemblyVersion("1.0.24.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.29.0")]
|
[assembly: AssemblyFileVersion("1.0.24.0")]
|
||||||
|
|
||||||
|
|||||||
@ -1,318 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Электронная_Фармация.Classes;
|
|
||||||
using Электронная_Фармация.Properties;
|
|
||||||
|
|
||||||
namespace Электронная_Фармация.UserControls
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Раздел «Отчёты» покупателя. Данные тянутся с сервера (/api/buyer/report),
|
|
||||||
/// который скоупит их по JWT покупателя — клиент видит ТОЛЬКО свои заказы.
|
|
||||||
/// Все 4 отчёта агрегируются на клиенте из одного набора строк.
|
|
||||||
/// </summary>
|
|
||||||
public class UCReports : UserControl
|
|
||||||
{
|
|
||||||
private readonly DateTimePicker _dtFrom;
|
|
||||||
private readonly DateTimePicker _dtTo;
|
|
||||||
private readonly ComboBox _cboReport;
|
|
||||||
private readonly Button _btnBuild;
|
|
||||||
private readonly Label _lblTotals;
|
|
||||||
private readonly DataGridView _grid;
|
|
||||||
private readonly TextBox _txtPrompt;
|
|
||||||
private readonly ComboBox _cboFormat;
|
|
||||||
private readonly Button _btnAi;
|
|
||||||
private List<BuyerReportLineDto> _lines = new List<BuyerReportLineDto>();
|
|
||||||
|
|
||||||
// Палитра под общий стиль приложения (бирюза шапки #0F766E).
|
|
||||||
private static readonly Color Accent = Color.FromArgb(15, 118, 110);
|
|
||||||
private static readonly Color AccentHover = Color.FromArgb(13, 148, 136);
|
|
||||||
private static readonly Color TextMain = Color.FromArgb(31, 41, 55);
|
|
||||||
private static readonly Color TextMuted = Color.FromArgb(120, 128, 134);
|
|
||||||
private static readonly Color RowAlt = Color.FromArgb(247, 250, 250);
|
|
||||||
private static readonly Color SelBack = Color.FromArgb(178, 240, 230);
|
|
||||||
private static readonly Color GridLine = Color.FromArgb(230, 234, 236);
|
|
||||||
|
|
||||||
private static void StylePrimaryButton(Button b)
|
|
||||||
{
|
|
||||||
b.FlatStyle = FlatStyle.Flat;
|
|
||||||
b.FlatAppearance.BorderSize = 0;
|
|
||||||
b.FlatAppearance.MouseOverBackColor = AccentHover;
|
|
||||||
b.FlatAppearance.MouseDownBackColor = AccentHover;
|
|
||||||
b.BackColor = Accent;
|
|
||||||
b.ForeColor = Color.White;
|
|
||||||
b.Cursor = Cursors.Hand;
|
|
||||||
b.Font = new Font("Segoe UI", 9F, FontStyle.Regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UCReports()
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill;
|
|
||||||
|
|
||||||
var uiFont = new Font("Segoe UI", 9F);
|
|
||||||
var top = new Panel { Dock = DockStyle.Top, Height = 100, BackColor = Color.White };
|
|
||||||
|
|
||||||
// --- строка 1: период + готовый отчёт по данным ---
|
|
||||||
var lblFrom = new Label { Text = "С", AutoSize = true, Left = 12, Top = 17, ForeColor = TextMain, Font = uiFont };
|
|
||||||
_dtFrom = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 112, Left = 32, Top = 13, Value = DateTime.Today.AddMonths(-1), Font = uiFont };
|
|
||||||
var lblTo = new Label { Text = "по", AutoSize = true, Left = 150, Top = 17, ForeColor = TextMain, Font = uiFont };
|
|
||||||
_dtTo = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 112, Left = 176, Top = 13, Value = DateTime.Today, Font = uiFont };
|
|
||||||
_cboReport = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 190, Left = 298, Top = 13, Font = uiFont, FlatStyle = FlatStyle.Flat };
|
|
||||||
_cboReport.Items.AddRange(new object[] { "Мои заказы", "По поставщикам", "По товарам", "По аптекам" });
|
|
||||||
_cboReport.SelectedIndex = 0;
|
|
||||||
_btnBuild = new Button { Text = "Сформировать", Left = 498, Top = 12, Width = 140, Height = 28 };
|
|
||||||
_lblTotals = new Label { AutoSize = true, Left = 654, Top = 17, Text = "", ForeColor = Accent, Font = new Font("Segoe UI", 9F, FontStyle.Bold) };
|
|
||||||
|
|
||||||
top.Controls.AddRange(new Control[] { lblFrom, _dtFrom, lblTo, _dtTo, _cboReport, _btnBuild, _lblTotals });
|
|
||||||
|
|
||||||
// --- строка 2: свободный запрос к ИИ (клиент пишет словами -> Word/Excel) ---
|
|
||||||
var lblAi = new Label { Text = "Запрос ИИ:", AutoSize = true, Left = 12, Top = 61, ForeColor = TextMain, Font = uiFont };
|
|
||||||
_txtPrompt = new TextBox { Left = 92, Top = 57, Width = 470, Font = uiFont, BorderStyle = BorderStyle.FixedSingle };
|
|
||||||
_cboFormat = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Left = 574, Top = 57, Width = 92, Font = uiFont, FlatStyle = FlatStyle.Flat };
|
|
||||||
_cboFormat.Items.AddRange(new object[] { "Word", "Excel" });
|
|
||||||
_cboFormat.SelectedIndex = 0;
|
|
||||||
_btnAi = new Button { Text = "🤖 Сформировать ИИ", Left = 676, Top = 56, Width = 170, Height = 28 };
|
|
||||||
_btnAi.Text = "Сформировать ИИ"; // без эмодзи (криво рендерился в кнопке)
|
|
||||||
var lblHint = new Label { AutoSize = true, Left = 92, Top = 83, ForeColor = TextMuted, Font = new Font("Segoe UI", 8F),
|
|
||||||
Text = "Напр.: «закупки по поставщикам за июль», «топ товаров за 3 месяца», «заказы у Farmsnab»" };
|
|
||||||
|
|
||||||
StylePrimaryButton(_btnBuild);
|
|
||||||
StylePrimaryButton(_btnAi);
|
|
||||||
|
|
||||||
var tip = new ToolTip();
|
|
||||||
tip.SetToolTip(_txtPrompt, "Опишите нужный отчёт словами — ИИ подберёт тип, период и соберёт документ.");
|
|
||||||
|
|
||||||
top.Controls.AddRange(new Control[] { lblAi, _txtPrompt, _cboFormat, _btnAi, lblHint });
|
|
||||||
|
|
||||||
_grid = new DataGridView
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill,
|
|
||||||
ReadOnly = true,
|
|
||||||
AllowUserToAddRows = false,
|
|
||||||
AllowUserToDeleteRows = false,
|
|
||||||
AllowUserToResizeRows = false,
|
|
||||||
RowHeadersVisible = false,
|
|
||||||
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
|
||||||
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
|
||||||
BorderStyle = BorderStyle.None,
|
|
||||||
BackgroundColor = Color.White,
|
|
||||||
EnableHeadersVisualStyles = false,
|
|
||||||
ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.None,
|
|
||||||
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing,
|
|
||||||
ColumnHeadersHeight = 34,
|
|
||||||
CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal,
|
|
||||||
GridColor = GridLine,
|
|
||||||
RowHeadersBorderStyle = DataGridViewHeaderBorderStyle.None
|
|
||||||
};
|
|
||||||
_grid.ColumnHeadersDefaultCellStyle.BackColor = Accent;
|
|
||||||
_grid.ColumnHeadersDefaultCellStyle.ForeColor = Color.White;
|
|
||||||
_grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", 9.5F, FontStyle.Bold);
|
|
||||||
_grid.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
|
|
||||||
_grid.ColumnHeadersDefaultCellStyle.Padding = new Padding(6, 0, 0, 0);
|
|
||||||
_grid.DefaultCellStyle.Font = new Font("Segoe UI", 9.5F);
|
|
||||||
_grid.DefaultCellStyle.ForeColor = TextMain;
|
|
||||||
_grid.DefaultCellStyle.SelectionBackColor = SelBack;
|
|
||||||
_grid.DefaultCellStyle.SelectionForeColor = TextMain;
|
|
||||||
_grid.DefaultCellStyle.Padding = new Padding(6, 0, 6, 0);
|
|
||||||
_grid.AlternatingRowsDefaultCellStyle.BackColor = RowAlt;
|
|
||||||
_grid.RowTemplate.Height = 30;
|
|
||||||
|
|
||||||
Controls.Add(_grid);
|
|
||||||
Controls.Add(top);
|
|
||||||
|
|
||||||
_btnBuild.Click += async (_, __) => await LoadAsync();
|
|
||||||
_cboReport.SelectedIndexChanged += (_, __) => Render();
|
|
||||||
_btnAi.Click += async (_, __) => await BuildAiAsync();
|
|
||||||
_txtPrompt.KeyDown += async (_, e) =>
|
|
||||||
{
|
|
||||||
if (e.KeyCode == Keys.Enter) { e.SuppressKeyPress = true; await BuildAiAsync(); }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task BuildAiAsync()
|
|
||||||
{
|
|
||||||
var prompt = _txtPrompt.Text?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(prompt))
|
|
||||||
{
|
|
||||||
ToastNotification.ShowError("Введите запрос для ИИ.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = Settings.Default.stringToken;
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
|
||||||
ToastNotification.ShowError("Не найден токен авторизации. Войдите в систему.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_btnAi.Enabled = false;
|
|
||||||
_btnAi.Text = "Формирую…";
|
|
||||||
|
|
||||||
var fmt = _cboFormat.SelectedIndex == 1 ? "xlsx" : "docx";
|
|
||||||
var client = new ApiClient();
|
|
||||||
client.SetToken(token);
|
|
||||||
var result = await client.GenerateAiReportAsync(prompt, fmt);
|
|
||||||
|
|
||||||
var dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
|
||||||
var safe = string.Join("_", (result.FileName ?? "Отчёт").Split(Path.GetInvalidFileNameChars()));
|
|
||||||
var path = Path.Combine(dir, safe);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
var name = Path.GetFileNameWithoutExtension(safe);
|
|
||||||
var ext = Path.GetExtension(safe);
|
|
||||||
path = Path.Combine(dir, $"{name}_{DateTime.Now:yyyyMMdd_HHmmss}{ext}");
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllBytes(path, result.Content);
|
|
||||||
ToastNotification.ShowCustom("Отчёт готов, открываю…", Color.SeaGreen, Color.White);
|
|
||||||
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppDebugLog.Error("UCReports", "Ошибка ИИ-отчёта", ex);
|
|
||||||
ToastNotification.ShowError("Ошибка ИИ-отчёта: " + ex.Message);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_btnAi.Enabled = true;
|
|
||||||
_btnAi.Text = "🤖 Сформировать ИИ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_btnBuild.Enabled = false;
|
|
||||||
var token = Settings.Default.stringToken;
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
|
||||||
ToastNotification.ShowError("Не найден токен авторизации. Войдите в систему.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = new ApiClient();
|
|
||||||
client.SetToken(token);
|
|
||||||
_lines = await client.GetBuyerReportAsync(_dtFrom.Value.Date, _dtTo.Value.Date)
|
|
||||||
?? new List<BuyerReportLineDto>();
|
|
||||||
Render();
|
|
||||||
|
|
||||||
if (_lines.Count == 0)
|
|
||||||
{
|
|
||||||
ToastNotification.ShowCustom("За выбранный период заказов нет.", Color.DarkOrange, Color.White);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AppDebugLog.Error("UCReports", "Ошибка формирования отчёта", ex);
|
|
||||||
ToastNotification.ShowError("Ошибка отчёта: " + ex.Message);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_btnBuild.Enabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Render()
|
|
||||||
{
|
|
||||||
var dt = new DataTable();
|
|
||||||
|
|
||||||
switch (_cboReport.SelectedIndex)
|
|
||||||
{
|
|
||||||
case 0: // Мои заказы (по заказу)
|
|
||||||
dt.Columns.Add("Дата", typeof(string));
|
|
||||||
dt.Columns.Add("Номер", typeof(string));
|
|
||||||
dt.Columns.Add("Поставщик", typeof(string));
|
|
||||||
dt.Columns.Add("Аптека", typeof(string));
|
|
||||||
dt.Columns.Add("Позиций", typeof(int));
|
|
||||||
dt.Columns.Add("Сумма", typeof(decimal));
|
|
||||||
dt.Columns.Add("Статус", typeof(string));
|
|
||||||
foreach (var g in _lines.GroupBy(l => l.OrderID)
|
|
||||||
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
|
||||||
{
|
|
||||||
var f = g.First();
|
|
||||||
var suppliers = string.Join(", ", g.Select(x => x.Supplier)
|
|
||||||
.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct());
|
|
||||||
dt.Rows.Add(
|
|
||||||
f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
|
||||||
f.GlobalSign,
|
|
||||||
suppliers,
|
|
||||||
f.Location,
|
|
||||||
g.Count(),
|
|
||||||
g.Sum(x => x.Sum),
|
|
||||||
f.Status);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1: // По поставщикам
|
|
||||||
dt.Columns.Add("Поставщик", typeof(string));
|
|
||||||
dt.Columns.Add("Заказов", typeof(int));
|
|
||||||
dt.Columns.Add("Позиций", typeof(int));
|
|
||||||
dt.Columns.Add("Сумма", typeof(decimal));
|
|
||||||
foreach (var g in _lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Supplier) ? "(не указан)" : l.Supplier)
|
|
||||||
.OrderByDescending(x => x.Sum(y => y.Sum)))
|
|
||||||
{
|
|
||||||
dt.Rows.Add(g.Key, g.Select(x => x.OrderID).Distinct().Count(), g.Count(), g.Sum(x => x.Sum));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2: // По товарам
|
|
||||||
dt.Columns.Add("Товар", typeof(string));
|
|
||||||
dt.Columns.Add("Код", typeof(string));
|
|
||||||
dt.Columns.Add("Кол-во", typeof(decimal));
|
|
||||||
dt.Columns.Add("Сумма", typeof(decimal));
|
|
||||||
foreach (var g in _lines.GroupBy(l => ((l.ItemName ?? "").Trim() + "|" + (l.ItemCode ?? "")))
|
|
||||||
.OrderByDescending(x => x.Sum(y => y.Sum)))
|
|
||||||
{
|
|
||||||
var f = g.First();
|
|
||||||
dt.Rows.Add(f.ItemName, f.ItemCode, g.Sum(x => x.Qty), g.Sum(x => x.Sum));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 3: // По аптекам (грузополучателям)
|
|
||||||
dt.Columns.Add("Аптека", typeof(string));
|
|
||||||
dt.Columns.Add("Заказов", typeof(int));
|
|
||||||
dt.Columns.Add("Позиций", typeof(int));
|
|
||||||
dt.Columns.Add("Сумма", typeof(decimal));
|
|
||||||
foreach (var g in _lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Location) ? "(не указана)" : l.Location)
|
|
||||||
.OrderByDescending(x => x.Sum(y => y.Sum)))
|
|
||||||
{
|
|
||||||
dt.Rows.Add(g.Key, g.Select(x => x.OrderID).Distinct().Count(), g.Count(), g.Sum(x => x.Sum));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_grid.DataSource = dt;
|
|
||||||
foreach (DataGridViewColumn c in _grid.Columns)
|
|
||||||
{
|
|
||||||
bool numeric = c.Name == "Сумма" || c.Name == "Кол-во" || c.Name == "Позиций" || c.Name == "Заказов";
|
|
||||||
if (c.Name == "Сумма") c.DefaultCellStyle.Format = "N2";
|
|
||||||
if (c.Name == "Кол-во") c.DefaultCellStyle.Format = "N3";
|
|
||||||
if (numeric)
|
|
||||||
{
|
|
||||||
c.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
|
|
||||||
c.HeaderCell.Style.Alignment = DataGridViewContentAlignment.MiddleRight;
|
|
||||||
c.HeaderCell.Style.Padding = new Padding(0, 0, 6, 0);
|
|
||||||
}
|
|
||||||
// Узкие числовые колонки, широкие — текстовые (Fill распределит по весу).
|
|
||||||
if (c.Name == "Позиций" || c.Name == "Заказов") c.FillWeight = 45;
|
|
||||||
else if (c.Name == "Сумма" || c.Name == "Кол-во") c.FillWeight = 70;
|
|
||||||
else if (c.Name == "Дата" || c.Name == "Статус" || c.Name == "Код") c.FillWeight = 75;
|
|
||||||
else if (c.Name == "Номер") c.FillWeight = 90;
|
|
||||||
else c.FillWeight = 150; // Поставщик / Товар / Аптека
|
|
||||||
}
|
|
||||||
|
|
||||||
int orders = _lines.Select(l => l.OrderID).Distinct().Count();
|
|
||||||
decimal totalSum = _lines.Sum(l => l.Sum);
|
|
||||||
_lblTotals.Text = $"Заказов: {orders} Позиций: {_lines.Count} Сумма: {totalSum:N2}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user