From 800b2971b88f018f7cfb9647cf42557e5d1b088a Mon Sep 17 00:00:00 2001 From: Magomed Date: Tue, 14 Jul 2026 15:22:13 +0300 Subject: [PATCH] Add API documentation and remove unused single-price endpoint. Document all client HTTP calls in docs/API.md, link from README, and delete dead GetPriceItem code. Co-authored-by: Cursor --- README.md | 203 ++++++++---- docs/API.md | 290 ++++++++++++++++++ src/ElectronicPharmacy/Classes/ApiClient.cs | 64 ---- .../HelpForms/HF_DownloadDataFromServer.cs | 46 --- 4 files changed, 426 insertions(+), 177 deletions(-) create mode 100644 docs/API.md diff --git a/README.md b/README.md index ef6626b..5d097d1 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,136 @@ -# ЭльФиСА — Электронная Фармация - -WinForms-клиент «Электронная Фармация» с современным UI на базе библиотеки **Elfisa.UI** (тема **PharmaTrust**). - -## Структура - -``` -src/ - Elfisa.UI/ — UI-библиотека (ThemeManager, Modern* контролы) - ElectronicPharmacy/ — основное приложение (SQLite, API, бизнес-логика) -``` - -## Сборка - -Требуется .NET SDK 9+ (или MSBuild 16+) и .NET Framework 4.7.2. - -```powershell -dotnet build Elfisa.sln -c Release -``` - -Запуск: - -```powershell -.\src\ElectronicPharmacy\bin\Release\Электронная` Фармация.exe -``` - -База `efClient.db` копируется в output (`Data\efClient.db` или рядом с exe). Путь задаётся через `AppConfig.SqliteDbPath`. - -## Настройки - -В диалоге **Авторизация** (и в `Settings`): - -- **URL API** — базовый адрес сервера (по умолчанию из `AppConfig`) -- **Location ID** — UUID точки для отправки заказов -- **Region ID** — опциональный фильтр при загрузке прайса - -Переменная окружения `ELF_API_BASE_URL` переопределяет URL API. - -## Архив для заказчика - -```powershell -.\dist\build-release.ps1 -``` - -Создаёт `dist\Электронная-Фармация.zip` с Release-сборкой. - -## Основной цикл (V2) - -1. Старт → проверка БД → выбор грузополучателя → прайс-лист -2. **Загрузить** — прайс с API в SQLite, обновление открытых вкладок прайса -3. Фильтр поставщика, поиск, набор в корзину с проверкой остатка -4. **Сохранить заказ** — мин. сумма по поставщику, уникальный номер, грузополучатель в `Orders` -5. **Отправить** — POST `/api/buyer/orders`, статус `ОТПРАВЛЕН` только у отправленного заказа -6. **Заказы** — фильтры по статусу, поставщику, грузополучателю; отправка одного заказа -7. **Отказы / Накладные** — синхронизация с `/api/buyer/invoices` - -## Приёмочный чек-лист - -1. Старт → БД → выбор точки → прайс -2. Загрузить → прайс обновился, корзина очищена с предупреждением -3. Фильтр поставщика + поиск + количество + проверка остатка -4. Сохранить заказ (мин. сумма, корректный номер, грузополучатель) -5. Отправить → только выбранные заказы → статус / ошибка API -6. Журнал: фильтры, комментарий, удаление `НОВЫЙ` -7. Отказы: загрузка с API + детали позиций -8. Авторизация + `location_id` в настройках -9. Единый UI: `UiDialogs` / toast, без debug-панелей загрузки +# ЭльФиСА — Электронная Фармация + + + +WinForms-клиент «Электронная Фармация» с современным UI на базе библиотеки **Elfisa.UI** (тема **PharmaTrust**). + + + +## Структура + + + +``` + +src/ + + Elfisa.UI/ — UI-библиотека (ThemeManager, Modern* контролы) + + ElectronicPharmacy/ — основное приложение (SQLite, API, бизнес-логика) + +``` + + + +## Сборка + + + +Требуется .NET SDK 9+ (или MSBuild 16+) и .NET Framework 4.7.2. + + + +```powershell + +dotnet build Elfisa.sln -c Release + +``` + + + +Запуск: + + + +```powershell + +.\src\ElectronicPharmacy\bin\Release\Электронная` Фармация.exe + +``` + + + +База `efClient.db` копируется в output (`Data\efClient.db` или рядом с exe). Путь задаётся через `AppConfig.SqliteDbPath`. + + + +## Настройки + + + +В диалоге **Авторизация** (и в `Settings`): + + + +- **URL API** — базовый адрес сервера (по умолчанию из `AppConfig`) + +- **Location ID** — UUID точки для отправки заказов + +- **Region ID** — опциональный фильтр при загрузке прайса + + + +Переменная окружения `ELF_API_BASE_URL` переопределяет URL API. + +Подробное описание всех HTTP-запросов — в [docs/API.md](docs/API.md). + + + +## Архив для заказчика + + + +```powershell + +.\dist\build-release.ps1 + +``` + + + +Создаёт `dist\Электронная-Фармация.zip` с Release-сборкой. + + + +## Основной цикл (V2) + + + +1. Старт → проверка БД → выбор грузополучателя → прайс-лист + +2. **Загрузить** — прайс с API в SQLite, обновление открытых вкладок прайса + +3. Фильтр поставщика, поиск, набор в корзину с проверкой остатка + +4. **Сохранить заказ** — мин. сумма по поставщику, уникальный номер, грузополучатель в `Orders` + +5. **Отправить** — POST `/api/buyer/orders`, статус `ОТПРАВЛЕН` только у отправленного заказа + +6. **Заказы** — фильтры по статусу, поставщику, грузополучателю; отправка одного заказа + +7. **Отказы / Накладные** — синхронизация с `/api/buyer/invoices` + + + +## Приёмочный чек-лист + + + +1. Старт → БД → выбор точки → прайс + +2. Загрузить → прайс обновился, корзина очищена с предупреждением + +3. Фильтр поставщика + поиск + количество + проверка остатка + +4. Сохранить заказ (мин. сумма, корректный номер, грузополучатель) + +5. Отправить → только выбранные заказы → статус / ошибка API + +6. Журнал: фильтры, комментарий, удаление `НОВЫЙ` + +7. Отказы: загрузка с API + детали позиций + +8. Авторизация + `location_id` в настройках + +9. Единый UI: `UiDialogs` / toast, без debug-панелей загрузки + diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..ba27529 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,290 @@ +# API — Электронная Фармация + +Документация по HTTP-запросам, которые использует десктоп-клиент. + +Реализация: `src/ElectronicPharmacy/Classes/ApiClient.cs`, `DataSender.cs`, `InvoiceSyncService.cs`. + +--- + +## Базовый URL + +``` +{ApiBaseUrl} +``` + + +| Источник | Приоритет | +| -------------------------------------------------------------------- | --------------------------- | +| Настройки пользователя (`Settings.ApiBaseUrl`, окно **Авторизация**) | 1 | +| Переменная окружения `ELF_API_BASE_URL` | 2 | +| Значение по умолчанию | `http://195.34.241.84:9988` | + + +**Таймаут:** 30 секунд (`ApiClient`). Для отправки заказов (`DataSender`) таймаут не задан явно. + +**Формат:** JSON, кодировка UTF-8. + +--- + +## Авторизация + +Все запросы, кроме логина, требуют заголовок: + +``` +Authorization: Bearer {JWT} +``` + +Токен сохраняется в `Settings.stringToken` после успешного входа. + +--- + +## Эндпоинты + +### 1. Вход + + +| | | +| --------------- | ------------- | +| **Метод** | `POST` | +| **Путь** | `/auth/login` | +| **Авторизация** | не требуется | + + +**Где в приложении:** окно **Авторизация** (`HF_Registration`), загрузка прайса (`HF_DownloadDataFromServer` — если токен отсутствует). + +**Запрос:** + +```json +{ + "username": "логин", + "password": "пароль" +} +``` + +**Ответ `200`:** + +```json +{ + "token": "jwt..." +} +``` + +**Ошибка `401`:** + +```json +{ + "error": "описание ошибки" +} +``` + +--- + +### 2. Сводный прайс + + +| | | +| --------- | ---------------------------------------- | +| **Метод** | `GET` | +| **Путь** | `/api/supplier-prices/summary` | +| **Query** | `supplier_id` (опц.), `region_id` (опц.) | + + +**Где в приложении:** меню **Обмен → Загрузить**, кнопка обновления прайса в главном окне (`HF_DownloadDataFromServer`). + +**Примеры:** + +```http +GET /api/supplier-prices/summary +GET /api/supplier-prices/summary?region_id={RegionId} +GET /api/supplier-prices/summary?supplier_id={uuid} +``` + +**Ответ `200`:** + +```json +{ + "summary": [ + { + "supplier_price_id": "uuid", + "guid_es": "uuid", + "supplier_id": "uuid", + "supplier_name": "Катрен", + "drug_name": "Название препарата", + "inn": "МНН", + "cure_form": "таб.", + "barcode": "...", + "price": 123.45, + "quantity": 10, + "region_id": "...", + "region_name": "...", + "last_price_date": "2026-07-14T00:00:00Z", + "match_method": "BARCODE", + "match_confidence": 0.95, + "trade_name": "...", + "dosage": "...", + "registry_price": 100.00, + "instruction_guid": "...", + "description": "...", + "storing_condition": "...", + "expiry_period": "12.2027", + "producer_name": "...", + "registry_date": "...", + "es_code": 123456, + "registry_status": "..." + } + ] +} +``` + +**После ответа:** данные записываются в локальную SQLite-таблицу `PriceList` (полная перезапись). + +**Класс:** `ApiClient.GetPriceSummaryAsync` + +--- + +### 3. Отправка заказа + + +| | | +| ---------------- | ------------------- | +| **Метод** | `POST` | +| **Путь** | `/api/buyer/orders` | +| **Content-Type** | `application/json` | + + +**Где в приложении:** + +- меню **Обмен → Отправить** — все заказы со статусом `НОВЫЙ` (`DataSender.Main`); +- страница **Заказы**, кнопка **ОТПРАВИТЬ** — один выбранный заказ (`DataSender.SendSingleOrderAsync`). + +**Запрос** (один заказ = один POST): + +```json +{ + "location_id": "uuid точки из Settings.LocationId", + "comment": "комментарий или null", + "items": [ + { + "supplier_price_id": "uuid позиции из прайса", + "qty": 5 + } + ] +} +``` + +**Условия отправки:** + +- в локальной БД заказ имеет статус `НОВЫЙ`; +- у каждой позиции заполнен `supplier_price_id`; +- `qty` > 0; +- задан `location_id` в настройках. + +**При успехе:** локально `Orders.OrderState` → `ОТПРАВЛЕН` (только для отправленного заказа). + +**При ошибке:** toast с HTTP-кодом и телом ответа сервера. + +**Класс:** `DataSender` + +--- + +### 4. Накладные и отказы + + +| | | +| --------- | --------------------- | +| **Метод** | `GET` | +| **Путь** | `/api/buyer/invoices` | + + +**Где в приложении:** кнопка **Обновить с сервера** на вкладках **Накладные** и **Отказы** (`InvoiceSyncService`). + +**Ответ `200`:** массив объектов + +```json +[ + { + "InvoiceNumber": "12345", + "InvoiceDate": "2026-07-14", + "SupplierName": "Поставщик", + "ConsigneeName": "Грузополучатель", + "InvoiceSum": "10000.00", + "RefuseSum": "500.00" + } +] +``` + +**После ответа:** `INSERT OR REPLACE` в локальную таблицу `Invoice`. + +**Класс:** `ApiClient.GetInvoicesAsync` + +--- + +## Сводная таблица + + +| Действие в UI | HTTP | Эндпоинт | Класс / метод | +| --------------------------- | ------ | ------------------------------ | -------------------------------- | +| Авторизация | `POST` | `/auth/login` | `ApiClient.LoginAsync` | +| Загрузить прайс | `GET` | `/api/supplier-prices/summary` | `ApiClient.GetPriceSummaryAsync` | +| Отправить заказ(ы) | `POST` | `/api/buyer/orders` | `DataSender.SendOrderAsync` | +| Обновить накладные / отказы | `GET` | `/api/buyer/invoices` | `ApiClient.GetInvoicesAsync` | + + +--- + +## Обработка ошибок + + +| Ситуация | Поведение клиента | +| ------------------ | ---------------------------------------------------------------------------- | +| `401 Unauthorized` | Сообщение об ошибке авторизации; для прайса токен сбрасывается в `ApiClient` | +| Таймаут (30 с) | `HttpRequestException` с текстом о превышении времени ожидания | +| Прочие HTTP-ошибки | Тело `{ "error": "..." }` если сервер вернул JSON | +| Нет `location_id` | Отправка заказов блокируется до заполнения в настройках | +| Нет токена | Загрузка накладных и отправка заказов блокируются | + + +--- + +## Что не реализовано + +- `PUT`, `PATCH`, `DELETE` к API +- Получение статуса заказа с сервера +- Синхронизация справочников (поставщики, грузополучатели) через API +- Автоматический re-login при истечении токена + +--- + +## Примеры curl + +**Логин:** + +```bash +curl -X POST "{ApiBaseUrl}/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"user","password":"pass"}' +``` + +**Прайс:** + +```bash +curl "{ApiBaseUrl}/api/supplier-prices/summary" \ + -H "Authorization: Bearer {token}" +``` + +**Отправка заказа:** + +```bash +curl -X POST "{ApiBaseUrl}/api/buyer/orders" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{"location_id":"...","comment":null,"items":[{"supplier_price_id":"...","qty":1}]}' +``` + +**Накладные:** + +```bash +curl "{ApiBaseUrl}/api/buyer/invoices" \ + -H "Authorization: Bearer {token}" +``` + diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index a12e5fe..f121e08 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -83,55 +83,6 @@ namespace Электронная_Фармация.Classes /// public bool IsAuthenticated => !string.IsNullOrEmpty(_token); - - /// - /// Получает сводный прайс поставщиков - /// - /// ID поставщика (опционально) - /// ID региона (опционально) - /// Сводный прайс - /// - - public async Task GetPriceItem() - { - if (!IsAuthenticated) - { - throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); - } - - //var queryParams = new List(); - var url = "/api/supplier-prices/556EE09D-AD06-439E-91AF-D6CEBCD61AD3"; - - _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 секунд)"); - } - } - public async Task GetPriceSummaryAsync(string supplierId = null, string regionId = null) { if (!IsAuthenticated) @@ -139,7 +90,6 @@ namespace Электронная_Фармация.Classes throw new InvalidOperationException("Необходимо выполнить авторизацию перед запросом данных"); } - // Формируем URL с параметрами var queryParams = new List(); if (!string.IsNullOrEmpty(supplierId)) { @@ -149,20 +99,6 @@ namespace Электронная_Фармация.Classes { queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}"); } - //if (!string.IsNullOrEmpty(supplierPriceId)) - //{ - // queryParams.Add($"supplier_price_id={Uri.EscapeDataString(supplierPriceId)}"); - //} - - //var queryParams = new List(); - //if (!string.IsNullOrEmpty(supplierId)) - //{ - // queryParams.Add($"supplier_id={Uri.EscapeDataString(supplierId)}"); - //} - //if (!string.IsNullOrEmpty(regionId)) - //{ - // queryParams.Add($"region_id={Uri.EscapeDataString(regionId)}"); - //} var url = "/api/supplier-prices/summary"; if (queryParams.Count > 0) diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 169f215..062bf0b 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -60,12 +60,6 @@ namespace Электронная_Фармация.HelpForms regionId: string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId); rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.Summary.Count} позиций\n"); - - //загружаю один товар: - //rtxtDebug.Text += ("\nЗагружаю сводный прайс для всех поставщиков..."); - // var PriceItem = await client.GetPriceItem(); - //rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.DrugName.Count} позиций\n"); - DataTable tablePrice = new DataTable(); tablePrice.Columns.Add("supplier_price_id"); @@ -134,45 +128,6 @@ namespace Электронная_Фармация.HelpForms //tablePrice.Columns.Add("RegistryStatus"); #endregion - // Выводим первые 5 позиций - //Console.WriteLine("Первые 500 позиций:"); - - #region загрузка одного элемента - //if (PriceItem.DrugName != null | PriceItem.DrugName != "" | PriceItem.DrugName != string.Empty) - //{ - - // Console.WriteLine(PriceItem.ToString()); - - // DataRow row = tablePrice.NewRow(); - - // row["supplier_price_id"] = PriceItem.SupplierPriceID.ToString(); - // row["guid_es"] = PriceItem.GuidEs.ToString(); - // row["es_code"] = PriceItem.EsCode.ToString(); - // row["supplier_id"] = PriceItem.SupplierId.ToString(); - // row["DrugName"] = PriceItem.DrugName.ToString(); - // row["SupplierName"] = "Катрен";//PriceItem.SupplierName.ToString(); - // row["Price"] = PriceItem.Price.ToString(); - // row["Quantity"] = PriceItem.Quantity.ToString(); - - // if (PriceItem.ExpiryPeriod == null) - // { row["ExpiryPeriod"] = DBNull.Value; } - // else { row["ExpiryPeriod"] = PriceItem.ExpiryPeriod.ToString(); } - // ; - - // if (PriceItem.Description == null) - // { row["Description"] = DBNull.Value; } - // else { row["Description"] = PriceItem.Description.ToString(); } - // ; - - // row["Zakaz"] = string.Empty; - // row["SummaZakaza"] = string.Empty; - - // tablePrice.Rows.Add(row); - //} - #endregion - - - #region загрузка всего прайса РАБОЧАЯ if (allSuppliersSummary.Summary != null) { @@ -256,7 +211,6 @@ namespace Электронная_Фармация.HelpForms #endregion } } - #endregion string commandToDelete = "delete from [PriceList]"; string commandTODeleteTempOrder = "delete from [TempOrderItems]";