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()