diff --git a/dist/Электронная-Фармация/Elfisa.UI.dll b/dist/Электронная-Фармация/Elfisa.UI.dll index 77d99fd..e07e50a 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 444b95b..fd820e8 100644 Binary files a/dist/Электронная-Фармация/Elfisa.UI.pdb and b/dist/Электронная-Фармация/Elfisa.UI.pdb differ diff --git a/dist/Электронная-Фармация/Электронная Фармация.exe b/dist/Электронная-Фармация/Электронная Фармация.exe index a11e7f8..0d18910 100644 Binary files a/dist/Электронная-Фармация/Электронная Фармация.exe and b/dist/Электронная-Фармация/Электронная Фармация.exe differ diff --git a/dist/Электронная-Фармация/Электронная Фармация.pdb b/dist/Электронная-Фармация/Электронная Фармация.pdb index a1e9ae0..c5bbd80 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 4482fc0..5abad0b 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -394,6 +394,64 @@ namespace Электронная_Фармация.Classes } } + /// + /// Отправляет свободный запрос («промт») ИИ-сервису и получает готовый + /// документ (Word/Excel). Токен покупателя уходит в теле — сервис ходит на + /// платформу от его имени и видит ТОЛЬКО его данные. + /// + public async Task 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> GetBuyerLocationsAsync() { EnsureAuthenticated(); @@ -778,6 +836,24 @@ namespace Электронная_Фармация.Classes public List 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")] diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index 96bae7c..0b30586 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -17,6 +17,10 @@ namespace Электронная_Фармация.Classes public const string DefaultInstallerDownloadUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe"; private const string LegacyApiBaseUrl = "http://195.34.241.84:9988"; private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL"; + /// ИИ-сервис отчётов (проксируется Caddy на Мак-мини). Можно переопределить + /// переменной ELF_AI_BASE_URL, напр. http://192.168.95.116:8808 для теста по локальной сети. + public const string DefaultAiBaseUrl = "https://24pharmdata.ru/ai"; + private const string AiBaseUrlEnvVar = "ELF_AI_BASE_URL"; private static bool _sqliteInteropPrepared; private static readonly object _sqliteInteropLock = new object(); @@ -111,6 +115,21 @@ namespace Электронная_Фармация.Classes } } + /// Базовый URL ИИ-сервиса отчётов. + 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) { if (string.IsNullOrWhiteSpace(url)) diff --git a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs index 08fd159..ef405ca 100644 --- a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs +++ b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs @@ -32,6 +32,6 @@ using System.Runtime.InteropServices; // пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.26.0")] -[assembly: AssemblyFileVersion("1.0.26.0")] +[assembly: AssemblyVersion("1.0.27.0")] +[assembly: AssemblyFileVersion("1.0.27.0")] diff --git a/src/ElectronicPharmacy/UserControls/UCReports.cs b/src/ElectronicPharmacy/UserControls/UCReports.cs index 20d01a1..f694cfa 100644 --- a/src/ElectronicPharmacy/UserControls/UCReports.cs +++ b/src/ElectronicPharmacy/UserControls/UCReports.cs @@ -1,7 +1,9 @@ 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; @@ -23,13 +25,16 @@ namespace Электронная_Фармация.UserControls 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 _lines = new List(); public UCReports() { Dock = DockStyle.Fill; - var top = new Panel { Dock = DockStyle.Top, Height = 46 }; + var top = new Panel { Dock = DockStyle.Top, Height = 90 }; var lblFrom = new Label { Text = "С", AutoSize = true, Left = 10, Top = 15 }; _dtFrom = new DateTimePicker { Format = DateTimePickerFormat.Short, Width = 110, Left = 30, Top = 11, Value = DateTime.Today.AddMonths(-1) }; @@ -43,6 +48,21 @@ namespace Электронная_Фармация.UserControls top.Controls.AddRange(new Control[] { lblFrom, _dtFrom, lblTo, _dtTo, _cboReport, _btnBuild, _lblTotals }); + // Вторая строка — свободный запрос к ИИ: клиент пишет что хочет, ИИ отдаёт Word/Excel. + var lblAi = new Label { Text = "Запрос ИИ:", AutoSize = true, Left = 10, Top = 57 }; + _txtPrompt = new TextBox { Left = 88, Top = 53, Width = 360, Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right }; + _cboFormat = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Left = 456, Top = 53, Width = 90, Anchor = AnchorStyles.Top | AnchorStyles.Right }; + _cboFormat.Items.AddRange(new object[] { "Word", "Excel" }); + _cboFormat.SelectedIndex = 0; + _btnAi = new Button { Text = "🤖 Сформировать ИИ", Left = 554, Top = 52, Width = 170, Height = 26, Anchor = AnchorStyles.Top | AnchorStyles.Right }; + var lblHint = new Label { AutoSize = true, Left = 88, Top = 76, ForeColor = Color.Gray, + Text = "Напр.: «закупки по поставщикам за июль», «топ товаров за 3 месяца», «заказы у Farmsnab»" }; + + var tip = new ToolTip(); + tip.SetToolTip(_txtPrompt, "Опишите нужный отчёт словами — ИИ подберёт тип, период и соберёт документ."); + + top.Controls.AddRange(new Control[] { lblAi, _txtPrompt, _cboFormat, _btnAi, lblHint }); + _grid = new DataGridView { Dock = DockStyle.Fill, @@ -61,6 +81,63 @@ namespace Электронная_Фармация.UserControls _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()